【问题标题】:Symfony DI : Circular service reference with Doctrine event subscriberSymfony DI:带有 Doctrine 事件订阅者的循环服务参考
【发布时间】:2017-02-07 17:56:34
【问题描述】:

为了重构有关工单通知系统的代码,我创建了一个 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...
}

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

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

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 上出现此错误?

谢谢。

【问题讨论】:

  • 您是否在不同的环境中运行 cli 和 web?例如。控制台 --env dev 和 web 正在访问 app.php?如果是,请手动清除缓存,然后重试。
  • 错误消息有点暗示 TicketMailer 是问题所在。它与数据库有任何连接吗?
  • Vladmir: 相同的环境并且已经删除了缓存,这没有任何改变。
  • Cerad:TicketMailer 对 twig 有依赖关系,它对身份验证管理器有依赖关系,而对 fos_user 管理器有依赖关系,它对...学说连接有依赖关系! \o/ 但如果邮件程序是问题所在,我会感到惊讶。添加 NotificationManager 后出现此错误...
  • 我无法解释为什么您没有在您的网络中看到这一点,但原则实体管理器服务依赖于它的所有侦听器。就像 Symfony 实现事物的方式一样。因此,您的票据原则侦听器不能依赖于使用原则实体管理器或连接的任何内容。只需要重新考虑您的依赖关系,或者可能手动将您的学说侦听器添加到实体管理器中,而不是对其进行标记。您可能会逃脱 TicketMailer::setAuthenticationManager 但正如您所看到的,事情开始变得混乱和混乱。

标签: php symfony dependency-injection doctrine-orm


【解决方案1】:

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

  • 域事件(例如TicketWasClosed)
  • Doctrine 的生命周期事件(例如PostPersist)

Doctrine 的事件系统旨在连接到持久化流,处理与保存到数据库和从数据库加载直接相关的内容。它不应该用于其他任何事情。

在我看来,你想要发生的事情是:

当工单关闭时,发送通知。

这与一般的教义或持久性无关。您需要的是另一个专用于领域事件的事件系统。

您仍然可以使用 Doctrine 中的 EventManager,但请确保创建第二个用于域事件的实例。

你也可以使用其他东西。例如 Symfony 的 EventDispatcher。如果你使用 Symfony 框架,同样的事情也适用于这里:不要使用 Symfony 的实例,为领域事件创建你自己的。

我个人喜欢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
}

从这里我们可能想要两件事:

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

使用 Doctrine ORM,您可以订阅 Doctrine 的 OnFlush 事件的侦听器,该事件将在所有刷新的实体上调用 recordedEvents()(以收集域事件),PostFlush 可以将它们传递给调度程序/publisher(仅在成功时)。

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

【讨论】:

  • 好的,但是如何触发TicketWasClosed?我的意思是,在哪里?票可以在多个地方关闭,我不想在这些地方手动触发此事件。在这种情况下,恕我直言,事件概念将毫无用处。还是直接来自教义事件?
  • 好的,我明白了。所以它触发了新的自定义事件,但来自于教义监听器。可以工作,但是因为依赖问题而不得不创建一个新系统是非常糟糕的,恕我直言......感谢您的帮助!
  • 恐怕你误解了我的推理。 IMO 您应该为单独的概念/上下文/责任创建一个单独的事件系统:一个专用于 域事件(您可能希望将其发布到其他(外部)系统),另一个专用于Doctrine 生命周期事件(你永远不想泄露给外界)。解决你的循环依赖问题是一个副作用;)
  • 我最终选择了您的解决方案:pastebin.com/Mkez82WG 然后我的域事件侦听器完成了剩下的工作。再次感谢!
【解决方案2】:

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

【讨论】:

  • 我给了你赏金 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”。似乎是另一回事...感谢您的帮助。
猜你喜欢
  • 2018-02-02
  • 2011-07-14
  • 2018-12-27
  • 1970-01-01
  • 2017-02-06
  • 1970-01-01
  • 2018-04-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多