在某些情况下,请防止发生理论上的postLoad事件

时间:2018-07-27 08:25:24

标签: symfony events doctrine

我有一个实体BlogPost,其属性为status。此状态属性取决于通过规范postLoad事件处理的外部API调用。所有其他属性都存储在本地数据库中。

public function postLoad(BlogPost $post)
{
    $this->postHandler->calculateStatus($post);
}

问题是,在某些情况下,我根本不想计算状态。例如,如果我只想获得所有博客文章中的description

使用上面的代码,即使我只是想从本地数据库获取值,所有正在加载的博客实体都将触发postLoad事件。那是非常昂贵的,不能接受的。

例如,在我的存储库类中,我想获取所有具有网站的BlogPost而不调用postLoad事件。

public function findBlogPosts()
{
    $qb = $this->getEntityManager()->createQueryBuilder();
    $qb->select('bp')
        ->from('AppBundle:BlogPosts', 'bp')
        ->innerJoin('bp.website', 'w');
    return $qb->getQuery()->getResult();
}

有没有办法说“是的,请加载BlogPost集合,但不要触发事件!” ???

还有其他方法吗?自定义事件?

谢谢

2 个答案:

答案 0 :(得分:0)

为什么不只是将此逻辑移到发布实体和事件侦听器之外?如果您知道何时需要计算状态,则可以明确进行。 例如

$post = $this->entityManager->find(BlogPost::class, $postId);
$status = $this->postHandler->calculateStatus($post);

我可以建议的另一种方法不好,但是可行。您可以使用惰性计算,而不必在postLoad事件侦听器中调用$this->postHandler->calculateStatus($this),而可以将postHandler服务注入实体并在实际需要时执行计算。 例如,如果您在调用$blogPost->getStatus()方法时需要计算,则可以这样进行:

interface PostHandlerAwareInterface
{
    public function setPostHandler(PostHandlerInterface $postHandler): void;
}

class EntityServiceInjectorEventSubscriber implements EventSubscriber
{
    /** @var PostHandlerInterface */
    private $postHandler;

    public function postLoad($entity): void
    {
        $this->injectServices($entity);
    }

    public function postPersist($entity): void
    {
        $this->injectServices($entity);
    }

    private function injectServices($entity): void
    {
        if ($entity instanceof PostHandlerAwareInterface) {
            $entity->setPostHandler($this->postHandler);
        }
    }
}

class BlogPost extends PostHandlerAwareInterface
{
    /** @var PostHandlerInterface */
    private $postHandler;

    private $status;

    public function setPostHandler(PostHandlerInterface $postHandler): void
    {
        $this->postHandler = $postHandler;
    }

    public function getStatus()
    {
        if (null === $this->status) {
            $this->postHandler->calculateStatus($this);
        }

        return $this->status;
    }
}

如果您不喜欢这个主意,您仍然可以通过将标志设置为实体事件侦听器的方式来管理它,但我强烈建议不要这样做。 您可以在获取数据之前将实体事件监听器注入代码并设置标志:

class BlogPostCalculateStatusListener
{
    /** @var bool */
    private $calculationEnabled = true;

    public function suspendCalculation(): void
    {
        $this->calculationEnabled = false;
    }

    public function resumeCalculation(): void
    {
        $this->calculationEnabled = true;
    }

    public function postLoad(BlogPost $post): void
    {
        if ($this->calculationEnabled) {
            $this->postHandler->calculateStatus($post);
        }
    }
}

$this->calculateStatusListener->suspendCalculation();
$blogPosts = $blogPostRepository->findBlogPosts();
$this->calculateStatusListener->resumeCalculation();

希望这会有所帮助。

PS。如果您只想获取所有博客文章的描述,可以这样做:

class BlogPostRepository
{
    public function findBlogPosts()
    {
        $qb = $this->getEntityManager()->createQueryBuilder();
        $qb->select('bp.description')
            ->from('AppBundle:BlogPosts', 'bp')
            ->innerJoin('bp.website', 'w');

        return $qb->getQuery()->getArrayResult();
    }
}

getArrayResult不会调用生命周期回调。

答案 1 :(得分:0)

由于我没有在互联网上找到真正的类似用例,因此我将寻求以下解决方案,这对我来说似乎是最简单,最可接受的解决方案。也许其他人会发现这很有用。

  1. 实现TransientLoadable接口

    interface TransientLoadable
    {
        public function isLoaded() : bool;
        public function setLoaded(bool $loaded) : TransientLoadable;
        public function setTransientLoadingFunction(\Closure $loadingFunction) :
        TransientLoadable;
    }
    
  2. 实施实体

    class BlogPost implements TransientLoadable
    {
     ...
    }
    
  3. 在PostLoad事件上设置加载功能

    public function postLoad(BlogPost $post)
    {
        $func = function() use ($postHandler, $post) 
        {
            //Since there may be another fields being loaded from the same API, catch them also since data is anyway in the same request
            $postHandler->setAllDataFromAPI($post)
            //Set the loading state to true to prevent calling the API again for the next property which may also be transient
            $post->setLoaded(true);
        }
        $post->setTransientLoadingFunction($func)
    }
    
  4. 仅在需要时使用内置的延迟加载机制从API获取属性

    class BlogPost implements TransientLoadable
    {
         private function getStatus() : int
         {
             if (!$this->isLoaded) {
                 call_user_function($this->loadingFunction)
             }
             return $this->status;
         }
    
         private function getVisitorCount() : int
         {
             if (!$this->isLoaded) {
                 call_user_function($this->loadingFunction)
             }
             return $this->visitorCount;
         }
    }
    

那是怎么回事?假设我们要获取状态和访问者人数,这两者都是通过单个外部API调用加载的。

如果需要该实体的某些api依赖属性,那么也会加载所有其他属性(因为我们不想为每个属性都拥有另一个调用)。这是通过loaded接口的TransientLoadable功能来确保的。 setAllDataFromAPI函数将加载所有数据,该函数作为闭包函数注入。

我认为这不是最干净的解决方案。加载stuf应该在实体类的顶部额外一层进行。由于sonata admin不会处理此类层,因此我认为此解决方案比直接将加载机制直接写入实体类更干净。

我愿意接受其他建议或反馈

谢谢