草庐IT

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

codeneng 2023-03-28 原文

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

谢谢。

  • 您是否在不同的环境中运行 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 然后我的域事件侦听器完成其余的工作。再次感谢!

有关关于 php:Symfony DI:Doctrine 事件订阅者的循环服务参考的更多相关文章

  1. ruby - 树顶语法无限循环 - 2

    我脑子里浮现出一些关于一种新编程语言的想法,所以我想我会尝试实现它。一位friend建议我尝试使用Treetop(Rubygem)来创建一个解析器。Treetop的文档很少,我以前从未做过这种事情。我的解析器表现得好像有一个无限循环,但没有堆栈跟踪;事实证明很难追踪到。有人可以指出入门级解析/AST指南的方向吗?我真的需要一些列出规则、常见用法等的东西来使用像Treetop这样的工具。我的语法分析器在GitHub上,以防有人希望帮助我改进它。class{initialize=lambda(name){receiver.name=name}greet=lambda{IO.puts("He

  2. ruby-on-rails - 在 Ruby 中循环遍历多个数组 - 2

    我有多个ActiveRecord子类Item的实例数组,我需要根据最早的事件循环打印。在这种情况下,我需要打印付款和维护日期,如下所示:ItemAmaintenancerequiredin5daysItemBpaymentrequiredin6daysItemApaymentrequiredin7daysItemBmaintenancerequiredin8days我目前有两个查询,用于查找maintenance和payment项目(非排他性查询),并输出如下内容:paymentrequiredin...maintenancerequiredin...有什么方法可以改善上述(丑陋的)代

  3. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  4. ruby-on-rails - 事件管理员日期过滤器日期格式自定义 - 2

    是否有简单的方法来更改默认ISO格式(yyyy-mm-dd)的ActiveAdmin日期过滤器显示格式? 最佳答案 您可以像这样为日期选择器提供额外的选项,而不是覆盖js:=f.input:my_date,as::datepicker,datepicker_options:{dateFormat:"mm/dd/yy"} 关于ruby-on-rails-事件管理员日期过滤器日期格式自定义,我们在StackOverflow上找到一个类似的问题: https://s

  5. ruby-on-rails - 事件记录 : Select max of limit - 2

    我正在尝试将以下SQL查询转换为ActiveRecord,它正在融化我的大脑。deletefromtablewhereid有什么想法吗?我想做的是限制表中的行数。所以,我想删除少于最近10个条目的所有内容。编辑:通过结合以下几个答案找到了解决方案。Temperature.where('id这给我留下了最新的10个条目。 最佳答案 从您的SQL来看,您似乎想要从表中删除前10条记录。我相信到目前为止的大多数答案都会如此。这里有两个额外的选择:基于MurifoX的版本:Table.where(:id=>Table.order(:id).

  6. ruby - Ruby 中的闭包和 for 循环 - 2

    我是Ruby的新手,有些闭包逻辑让我感到困惑。考虑这段代码:array=[]foriin(1..5)array[5,5,5,5,5]这对我来说很有意义,因为i被绑定(bind)在循环之外,所以每次循环都会捕获相同的变量。使用每个block可以解决这个问题对我来说也很有意义:array=[](1..5).each{|i|array[1,2,3,4,5]...因为现在每次通过时都单独声明i。但现在我迷路了:为什么我不能通过引入一个中间变量来修复它?array=[]foriin1..5j=iarray[5,5,5,5,5]因为j每次循环都是新的,我认为每次循环都会捕获不同的变量。例如,这绝对

  7. ruby-on-rails - 事件管理员和自定义方法 - 2

    这是我在ActiveAdmin中的自定义页面ActiveAdmin.register_page"Settings"doaction_itemdolink_to('Importprojects','settings/importprojects')endcontentdopara"Text"endcontrollerdodefimportprojectssystem"rakedataspider:import_projects_ninja"para"OK"endendend我想做的是,当我单击“导入项目”按钮时,我想在Controller中执行rake任务。但是我无法访问该方法。可能是什

  8. ruby-on-rails - 关于 Ruby 的一般问题 - 2

    我在我的rails应用程序中安装了来自github.com的acts_as_versioned插件,但有一段代码我不完全理解,我希望有人能帮我解决这个问题class_eval我知道block内的方法(或任何它是什么)被定义为类内的实例方法,但我在插件的任何地方都找不到定义为常量的CLASS_METHODS,而且我也不确定是什么here,并且有问题的代码从lib/acts_as_versioned.rb的第199行开始。如果有人愿意告诉我这里的内幕,我将不胜感激。谢谢-C 最佳答案 这是一个异端。http://en.wikipedia

  9. ruby-on-rails - 在不重新查询数据库的情况下重新排序 Rails 中的事件记录? - 2

    例如,假设我有一个名为Products的模型,并且在ProductsController中,我有以下代码用于product_listView以显示已排序的产品。@products=Product.order(params[:order_by])让我们想象一下,在product_listView中,用户可以使用下拉菜单按价格、评级、重量等进行排序。数据库中的产品不会经常更改。我很难理解的是,每次用户选择新的order_by过滤器时,rails是否必须查询,或者rails是否能够以某种方式缓存事件记录以在服务器端重新排序?有没有一种方法可以编写它,以便在用户排序时rails不会重新查询结果

  10. ruby-on-rails - Ruby 长时间运行的进程对队列事件使用react - 2

    我有一个将某些事件写入队列的Rails3应用。现在我想在服务器上创建一个服务,每x秒轮询一次队列,并按计划执行其他任务。除了创建ruby​​脚本并通过cron作业运行它之外,还有其他稳定的替代方案吗? 最佳答案 尽管启动基于Rails的持久任务是一种选择,但您可能希望查看更有序的系统,例如delayed_job或Starling管理您的工作量。我建议不要在cron中运行某些东西,因为启动整个Rails堆栈的开销可能很大。每隔几秒运行一次它是不切实际的,因为Rails上的启动时间通常为5-15秒,具体取决于您的硬件。不过,每天这样做几

随机推荐