Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Symfony DI : Circular service reference with Doctrine event subscriber

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.

like image 271
Soullivaneuh Avatar asked Sep 29 '16 12:09

Soullivaneuh


2 Answers

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

like image 153
Rvanlaak Avatar answered Oct 21 '22 07:10

Rvanlaak


IMHO you are mixing 2 different concepts here:

  • Domain Events (TicketWasClosed for example)
  • Doctrine's Life-cycle Events (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:

  • Collect these events into a single dispatcher/publisher.
  • Only dispatch/publish these events after a successful transaction.

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.

like image 31
Jasper N. Brouwer Avatar answered Oct 21 '22 05:10

Jasper N. Brouwer