Symfony DI:带有Doctrine事件订阅者的循环服务引用


Symfony DI : Circular service reference with Doctrine event subscriber

为了重构票证通知系统的代码,我创建了一个Doctrine侦听器:

final class TicketNotificationListener implements EventSubscriber
{
    /**
     * @var TicketMailer
     */
    private $mailer;
    /**
     * @var TicketSlackSender
     */
    private $slackSender;
    /**
     * @var NotificationManager
     */
    private $notificationManager;
    /**
     * We must wait the flush to send closing notification in order to
     * be sure to have the latest message of the ticket.
     *
     * @var Ticket[]|ArrayCollection
     */
    private $closedTickets;
    /**
     * @param TicketMailer        $mailer
     * @param TicketSlackSender   $slackSender
     * @param NotificationManager $notificationManager
     */
    public function __construct(TicketMailer $mailer, TicketSlackSender $slackSender, NotificationManager $notificationManager)
    {
        $this->mailer = $mailer;
        $this->slackSender = $slackSender;
        $this->notificationManager = $notificationManager;
        $this->closedTickets = new ArrayCollection();
    }
    // Stuff...
}

目标是在Ticket或TicketMessage实体通过邮件、Slack和内部通知创建或更新时,使用Doctrine SQL分发通知。

我已经有了Doctrine的循环依赖问题,所以我从事件参数中注入了实体管理器:

class NotificationManager
{
    /**
     * Must be set instead of extending the EntityManagerDecorator class to avoid circular dependency.
     *
     * @var EntityManagerInterface
     */
    private $entityManager;
    /**
     * @var NotificationRepository
     */
    private $notificationRepository;
    /**
     * @var RouterInterface
     */
    private $router;
    /**
     * @param RouterInterface $router
     */
    public function __construct(RouterInterface $router)
    {
        $this->router = $router;
    }
    /**
     * @param EntityManagerInterface $entityManager
     */
    public function setEntityManager(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
        $this->notificationRepository = $this->entityManager->getRepository('AppBundle:Notification');
    }
    // Stuff...
}

TicketNotificationListener注入管理器

public function postPersist(LifecycleEventArgs $args)
{
    // Must be lazy set from here to avoid circular dependency.
    $this->notificationManager->setEntityManager($args->getEntityManager());
    $entity = $args->getEntity();
}

web应用程序正在工作,但当我尝试运行命令像doctrine:database:drop例如,我得到这个:

[Symfony'Component'DependencyInjection'Exception'ServiceCircularReferenceException]                                                                                                                                                                                            
  Circular reference detected for service "doctrine.dbal.default_connection", path: "doctrine.dbal.default_connection -> mailer.ticket -> twig -> security.authorization_checker -> security.authentication.manager -> fos_user.user_provider.username_email -> fos_user.user_manager".

但是这是关于供应商服务的。

如何解决这个问题?为什么我只有在cli上才有这个错误?

谢谢。

最近遇到了同样的架构问题,假设您使用Doctrine 2.4+,最好不要使用EventSubscriber(它触发所有事件),而是对您提到的两个实体使用EntityListeners

假设两个实体的行为应该相同,您甚至可以创建一个侦听器并为两个实体配置它。注释看起来像这样:

/** 
* @ORM'Entity()
* @ORM'EntityListeners({"AppBundle'Entity'TicketNotificationListener"})
*/
class TicketMessage

之后,您可以创建TicketNotificationListener类,并让服务定义完成其余的工作:

app.entity.ticket_notification_listener:
    class: AppBundle'Entity'TicketNotificationListener
    calls:
        - [ setDoctrine, ['@doctrine.orm.entity_manager'] ]
        - [ setSlackSender, ['@app.your_slack_sender'] ]
    tags:
        - { name: doctrine.orm.entity_listener }

您可能在这里甚至不需要实体管理器,因为实体本身可以通过postPersist方法直接获得:

/**
 * @ORM'PostPersist()
 */
public function postPersist($entity, LifecycleEventArgs $event)
{
    $this->slackSender->doSomething($entity);
}

关于Doctrine实体监听器的更多信息:http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#entity-listeners

恕我直言,你在这里混合了两个不同的概念:

  • 域事件(以TicketWasClosed为例)
  • 原则生命周期事件(以PostPersist为例)
Doctrine的事件系统旨在与持久化流挂钩,以处理与保存和加载数据库直接相关的事情。它不应该用于其他任何地方。

对我来说,你想要发生的事情是:

当票被关闭时,发送通知。

这与一般的原则或持久性无关。你需要的是另一个专门用于域事件的事件系统。

你仍然可以使用Doctrine中的EventManager,但要确保你创建了一个用于Domain Events的第二个实例。

你也可以用别的东西。例如Symfony的EventDispatcher。如果你正在使用Symfony框架,同样的事情也适用于这里:不要使用Symfony的实例,为Domain Events创建你自己的实例。

我个人喜欢SimpleBus,它使用对象作为事件而不是字符串(用对象作为"参数")。它还遵循消息总线和中间件模式,这些模式为自定义提供了更多选项。

PS:有很多关于领域事件的好文章。谷歌是你的朋友:)

通常,在对实体执行操作时,域事件被记录在实体本身中。因此,Ticket实体将具有如下方法:

public function close()
{
    // insert logic to close ticket here
    $this->record(new TicketWasClosed($this->id));
}

这确保实体对它们的状态和行为完全负责,保护它们的不变量。

当然,我们需要一种方法从实体中获取记录的域事件:

/** @return object[] */
public function recordedEvents()
{
    // return recorded events
}

从这里我们可能需要两个东西:

  • 将这些事件收集到单个dispatcher/publisher中。
  • 仅在事务成功后才调度/发布这些事件。

使用Doctrine ORM,你可以订阅一个监听器到Doctrine的OnFlush事件,它将在所有刷新的实体上调用recordedEvents()(收集域事件),PostFlush可以将这些传递给调度程序/发布者(只有在成功时)。

SimpleBus提供了一个提供此功能的DoctrineORMBridge。