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

Sou*_*euh 8 php dependency-injection symfony doctrine-orm

为了重构有关票证通知系统的代码,我创建了一个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...
}
Run Code Online (Sandbox Code Playgroud)

目标是在使用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...
}
Run Code Online (Sandbox Code Playgroud)

经理是注入的 TicketNotificationListener

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

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".
Run Code Online (Sandbox Code Playgroud)

但这与供应商服务有关.

怎么解决这个?为什么我只在cli上出现此错误?

谢谢.

Rva*_*aak 5

最近有相同的架构问题,假设你使用Doctrine 2.4+最好的办法就是不使用EventSubscriber(触发所有事件),而是使用EntityListeners你提到的两个实体.

假设两个实体的行为应该相同,您甚至可以创建一个侦听器并为两个实体配置它.注释如下所示:

/** 
* @ORM\Entity()
* @ORM\EntityListeners({"AppBundle\Entity\TicketNotificationListener"})
*/
class TicketMessage
Run Code Online (Sandbox Code Playgroud)

此后,您可以创建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 }
Run Code Online (Sandbox Code Playgroud)

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

/**
 * @ORM\PostPersist()
 */
public function postPersist($entity, LifecycleEventArgs $event)
{
    $this->slackSender->doSomething($entity);
}
Run Code Online (Sandbox Code Playgroud)

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


Jas*_*wer 4

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

  • 领域事件(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));
}
Run Code Online (Sandbox Code Playgroud)

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

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

/** @return object[] */
public function recordedEvents()
{
    // return recorded events
}
Run Code Online (Sandbox Code Playgroud)

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

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

使用 Doctrine ORM,您可以订阅 DoctrineOnFlush事件的侦听器,该侦听器将调用recordedEvents()所有已刷新的实体(以收集域事件),并且PostFlush可以将这些实体传递给调度程序/发布程序(仅在成功时)。

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