In order to refactor the code about the ticket notification systems, I created a Doctrine listener:
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...
}
The goal is to dispatch notifications when a Ticket or a TicketMessage entity is created or updated trough mail, Slack and internal notification, using Doctrine SQL.
I already had a circular dependencies issue with Doctrine, so I injected the entity manager from the event args instead:
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 manager is injected form 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();
}
The web application is working, but when I try to run a command like doctrine:database:drop
for example, I got this:
[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".
But this is concerning vendor services.
How to solve this one? Why I have this error only on cli?
Thanks.
Had the same architectural problem lately, assuming you use Doctrine 2.4+
the best thing to do is not use the EventSubscriber
(which triggers for all events), but use EntityListeners
on the two entities you mention.
Assuming that the behavior of both entities should be the same, you could even create one listener and configure it for both entities. The annotation looks like this:
/**
* @ORM\Entity()
* @ORM\EntityListeners({"AppBundle\Entity\TicketNotificationListener"})
*/
class TicketMessage
Thereafter you can create the TicketNotificationListener
class and let a service definition do the rest:
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 }
You might not even need the entity manager here, because the entity itself is available via the postPersist
method directly:
/**
* @ORM\PostPersist()
*/
public function postPersist($entity, LifecycleEventArgs $event)
{
$this->slackSender->doSomething($entity);
}
More info on Doctrine entity listeners: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#entity-listeners
IMHO you are mixing 2 different concepts here:
TicketWasClosed
for example)PostPersist
for example)Doctrine's event system is meant to hook into the persistence flow, to deal stuff directly related to saving to and loading from the database. It shouldn't be used for anything else.
To me it looks like what you want to happen is:
When a ticket was closed, send a notification.
This has nothing to do with Doctrine or persistence in general. What you need is another event system dedicated to Domain Events.
You can still use the EventManager from Doctrine, but make sure you create a second instance which you use for Domain Events.
You can also use something else. Symfony's EventDispatcher for example. If you're using the Symfony framework, the same thing applies here as well: don't use Symfony's instance, create your own for Domain Events.
Personally I like SimpleBus, which uses objects as events instead of a string (with an object as "arguments"). It also follows the Message Bus and Middleware patterns, which give a lot more options for customization.
PS: There are a lot of really good articles on Domain Events out there. Google is your friend :)
Example
Usually Domain Events are recorded within entities themselves, when performing an action on them. So the Ticket
entity would have a method like:
public function close()
{
// insert logic to close ticket here
$this->record(new TicketWasClosed($this->id));
}
This ensures the entities remain fully responsible for their state and behavior, guarding their invariants.
Of course we need a way to get the recorded Domain Events out of the entity:
/** @return object[] */
public function recordedEvents()
{
// return recorded events
}
From here we probably want 2 things:
With the Doctrine ORM you can subscribe a listener to Doctrine's OnFlush
event, that will call recordedEvents()
on all entities that are flushed (to collect the Domain Events), and PostFlush
that can pass those to a dispatcher/publisher (only when successful).
SimpleBus provides a DoctrineORMBridge that supplies this functionality.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With