关于 php:Symfony DI:Doctrine 事件订阅者的循环服务参考

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...
}

管理器从 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 上出现此错误?

谢谢。


最近遇到了同样的架构问题,假设你使用 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


恕我直言,您在这里混合了 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。