Symfony DI: круговая ссылка на службу с подписчиком события 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...
}
цель состоит в том, чтобы отправлять уведомления, когда билет или объект TicketMessage создается или обновляется через почту, слабину и внутреннее уведомление, используя доктрину SQL.
у меня уже была проблема с циклическими зависимостями с доктриной, поэтому я ввел entity manager из 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...
}
менеджер вводится форма the TicketNotificationListener
public function postPersist(LifecycleEventArgs $args)
{
// Must be lazy set from here to avoid circular dependency.
$this->notificationManager->setEntityManager($args->getEntityManager());
$entity = $args->getEntity();
}
веб-приложение работает, но когда я пытаюсь выполнить команду doctrine:database:drop
например, я получил это:
[SymfonyComponentDependencyInjectionExceptionServiceCircularReferenceException]
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".
но это касается поставщиков услуг.
как решить эту проблему? Почему у меня эта ошибка только на Кинк?
спасибо.
2 ответов
IMHO вы смешиваете 2 разных понятия здесь:
- Домен Событий (
TicketWasClosed
например) - события жизненного цикла доктрины (
PostPersist
например)
система событий доктрины предназначена для подключения к потоку персистентности, чтобы иметь дело с вещами, непосредственно связанными с сохранением и загрузкой из базы данных. Его нельзя использовать ни для чего другого.
мне кажется, что вы хотите, чтобы произошло следующее:
когда билет был закрыт, отправьте уведомление.
это не имеет ничего общего с доктриной или настойчивостью в целом. Вам нужна другая система событий, посвященная событиям домена.
можно использовать EventManager из доктрины, но убедитесь, что вы создаете второй экземпляр, который вы используете для событий домена.
вы также можете использовать что-то другое. В Symfony это EventDispatcher например. Если вы используете то же самое относится и к Symfony framework: не используйте экземпляр Symfony, создайте свой собственный для событий домена.
лично мне нравится SimpleBus, который использует объекты в качестве событий вместо строки (с объектом в качестве "аргументов"). Он также следует за шиной сообщений и шаблонами промежуточного программного обеспечения, которые дают гораздо больше возможностей для настройки.
PS: есть много действительно хороших статей о событиях домена там. Google - ваш друг :)
пример
обычно события домена записываются внутри самих сущностей при выполнении над ними действия. Так что Ticket
сущность будет иметь такой метод, как:
public function close()
{
// insert logic to close ticket here
$this->record(new TicketWasClosed($this->id));
}
это гарантирует, что сущности остаются полностью ответственными за свое состояние и поведение, охраняя свои инварианты.
конечно, нам нужен способ получить записанные доменные события из сущности:
/** @return object[] */
public function recordedEvents()
{
// return recorded events
}
отсюда мы, вероятно, хотим 2 вещи:
- соберите эти события в один диспетчер / издатель.
- только отправка / публикация этих событий после успешных сделки.
С доктриной ORM вы можете подписаться слушателя доктрины OnFlush
событие, которое назовет recordedEvents()
на всех сущностях, которые сбрасываются (для сбора событий домена), и PostFlush
это может передать их диспетчеру / издателю (только когда успешный.)
SimpleBus обеспечивает DoctrineORMBridge это обеспечивает эту функциональность.
имел ту же архитектурную проблему в последнее время, предполагая, что вы используете доктрину 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);
}
подробнее о сущности учения слушателей: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#entity-listeners