Symfony DI : Circular service reference with Doctrine event subscriber
为了重构有关工单通知系统的代码,我创建了一个 Doctrine 监听器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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... } |
目标是在使用 Doctrine SQL 创建或更新 Ticket 或 TicketMessage 实体时通过邮件、Slack 和内部通知发送通知。
我已经遇到了 Doctrine 的循环依赖问题,所以我从事件 args 中注入了实体管理器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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... } |
管理器从
注入
1 2 3 4 5 6 | public function postPersist(LifecycleEventArgs $args) { // Must be lazy set from here to avoid circular dependency. $this->notificationManager->setEntityManager($args->getEntityManager()); $entity = $args->getEntity(); } |
Web 应用程序正在运行,但是当我尝试运行像
1 2 | [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". |
但这与 vendor服务有关。
如何解决这个问题?为什么我只在 cli 上出现此错误?
谢谢。
最近遇到了同样的架构问题,假设你使用 Doctrine
假设两个实体的行为应该相同,您甚至可以创建一个侦听器并为两个实体配置它。注释看起来像这样:
1 2 3 4 5 | /** * @ORM\\Entity() * @ORM\\EntityListeners({"AppBundle\\Entity\\TicketNotificationListener"}) */ class TicketMessage |
之后您可以创建
1 2 3 4 5 6 7 | 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 } |
你甚至可能不需要实体管理器,因为实体本身可以直接通过
1 2 3 4 5 6 7 | /** * @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
恕我直言,您在这里混合了 2 个不同的概念:
-
领域事件(例如
TicketWasClosed ) -
Doctrine 的生命周期事件(例如
postPersist )
Doctrine 的事件系统旨在连接到持久性流程,处理与保存到数据库和从数据库加载直接相关的内容。它不应该用于其他任何事情。
在我看来,你想要发生的事情是:
When a ticket was closed, send a notification.
这与一般的教义或坚持无关。您需要的是另一个专用于领域事件的事件系统。
您仍然可以使用 Doctrine 中的 EventManager,但请确保创建用于域事件的第二个实例。
你也可以用别的东西。例如 Symfony 的 EventDispatcher。如果你使用 Symfony 框架,同样的事情也适用于这里:不要使用 Symfony 的实例,为领域事件创建你自己的。
我个人喜欢 SimpleBus,它使用对象作为事件而不是字符串(使用对象作为"参数")。它还遵循消息总线和中间件模式,为自定义提供了更多选项。
PS:有很多关于领域事件的非常好的文章。谷歌是你的朋友 :)
例子
当对实体执行操作时,通常会在实体本身内记录领域事件。所以
1 2 3 4 5 6 | public function close() { // insert logic to close ticket here $this->record(new TicketWasClosed($this->id)); } |
这确保实体对其状态和行为负全部责任,保护它们的不变量。
当然,我们需要一种方法将记录的领域事件从实体中取出:
1 2 3 4 5 | /** @return object[] */ public function recordedEvents() { // return recorded events } |
从这里我们可能想要两件事:
- 将这些事件收集到单个调度程序/发布程序中。
- 仅在成功交易后调度/发布这些事件。
使用 Doctrine ORM,您可以订阅 Doctrine 的
SimpleBus 提供了一个提供此功能的 DoctrineORMBridge。