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; /** /** /** /** $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; /** /** /** /** // Stuff… |
管理器从 TicketNotificationListener
注入
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 应用程序正在运行,但是当我尝试运行像 doctrine:database:drop 这样的命令时,我得到了这个:
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 上出现此错误?
谢谢。
- 您是否在不同的环境中运行 cli 和 web?例如。控制台 –env dev 和 web 正在访问 app.php?如果是,请手动清除缓存,然后重试。
- 错误消息有点暗示 TicketMailer 是问题所在。它与数据库有任何连接吗?
- Vladmir:相同的环境并且已经删除了缓存,这没有任何改变。
- Cerad:TicketMailer 依赖于 twig,它依赖于身份验证管理器,它依赖于 fos_user 管理器,后者依赖于…学说连接! \\\\o/ 但如果邮件程序是问题,我会感到惊讶。添加 NotificationManager 后出现此错误…
- 我无法解释为什么您没有在您的网络中看到这一点,但原则实体管理器服务依赖于它的所有侦听器。就像 Symfony 实现事物的方式一样。因此,您的票据原则侦听器不能依赖于使用原则实体管理器或连接的任何内容。只需要重新考虑您的依赖关系,或者可能手动将您的学说侦听器添加到实体管理器中,而不是对其进行标记。您可能会逃脱 TicketMailer::setAuthenticationManager 但正如您所见,事情开始变得混乱和混乱。
- 顺便说一句,回复评论时使用@UserName。 @ 触发通知。
- 作为一个”脏”修复,你可以注入整个服务容器并从那里获取它们。
最近遇到了同样的架构问题,假设你使用 Doctrine 2.4+ 最好的办法是不要使用 EventSubscriber (触发所有事件),而是在你提到的两个实体上使用 EntityListeners。
假设两个实体的行为应该相同,您甚至可以创建一个侦听器并为两个实体配置它。注释看起来像这样:
1
2 3 4 5 |
/**
* @ORM\\Entity() * @ORM\\EntityListeners({“AppBundle\\Entity\\TicketNotificationListener”}) */ class TicketMessage |
之后您可以创建 TicketNotificationListener 类并让服务定义完成剩下的工作:
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 } |
你甚至可能不需要实体管理器,因为实体本身可以直接通过 postPersist 方法获得:
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
- 我给你赏金b/c 我认为这可能是最务实的一个€|不知道它是否有效,太糟糕了€|
- 我们实际上有这种方法在我们的 prod 环境中工作。我建议不要注入实体管理器,基本上听众不应该需要它。如果您希望我详细说明任何具体行为,请告诉我。
- 使用 EntityListener 看起来确实更好。但现在我有另一个循环问题:检测到服务”doctrine.orm.default_entity_listener_resolver”的循环引用,路径:”doctrine.orm.default_entity_listener_resolver -> mailer.ticket -> twig -> security.authorization_checker -> security.authentication.manager – > fos_user.user_provider.username_email -> fos_user.user_manager”。似乎是另一回事…感谢您的帮助。
恕我直言,您在这里混合了 2 个不同的概念:
- 领域事件(例如 TicketWasClosed)
- Doctrine 的生命周期事件(例如 postPersist)
Doctrine 的事件系统旨在连接到持久性流程,处理与保存到数据库和从数据库加载直接相关的内容。它不应该用于其他任何事情。
在我看来,你想要发生的事情是:
When a ticket was closed, send a notification.
这与一般的教义或坚持无关。您需要的是另一个专用于领域事件的事件系统。
您仍然可以使用 Doctrine 中的 EventManager,但请确保创建用于域事件的第二个实例。
你也可以用别的东西。例如 Symfony 的 EventDispatcher。如果你使用 Symfony 框架,同样的事情也适用于这里:不要使用 Symfony 的实例,为领域事件创建你自己的。
我个人喜欢 SimpleBus,它使用对象作为事件而不是字符串(使用对象作为”参数”)。它还遵循消息总线和中间件模式,为自定义提供了更多选项。
PS:有很多关于领域事件的非常好的文章。谷歌是你的朋友 :)
例子
当对实体执行操作时,通常会在实体本身内记录领域事件。所以 Ticket 实体会有一个类似的方法:
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 的 OnFlush 事件的侦听器,该事件将在所有刷新的实体上调用 recordedEvents()(以收集域事件),而 PostFlush 可以将这些实体传递给调度程序/publisher(仅在成功时)。
SimpleBus 提供了一个提供此功能的 DoctrineORMBridge。
- 好的,但是如何触发 TicketWasClosed 呢?我的意思是,在哪里?票可以在多个地方关闭,我不想在这些地方手动触发此事件。在这种情况下,恕我直言,事件概念将毫无用处。还是直接来自教义事件?
- 好的我明白了。所以它触发了新的自定义事件,但来自于教义监听器。可以工作,但是仅仅因为依赖问题而不得不创建一个新系统是非常糟糕的,恕我直言……感谢您的帮助!
- 恐怕你误解了我的推理。 IMO,您应该为单独的概念/上下文/责任创建一个单独的事件系统:一个专用于域事件(您可能希望将其发布到其他(外部)系统),另一个专用于 Doctrine 生命周期事件(您从未想泄露给外界)。解决你的循环依赖问题是一个副作用;)
- 我最终选择了您的解决方案:pastebin.com/Mkez82WG 然后我的域事件侦听器完成其余的工作。再次感谢!
来源:https://www.codenong.com/39769762/