zfvjsq
Last Updated: February 25, 2016
·
793
· zdenekdrahos
Avatar

Dispatch Symfony events in transactions

Symfony dispatcher is powerful tool, but as in any framework maybe it's too much designed for general purposes. One missing use case is dispatching event in transaction. For example when one listener failed, then you don't want to insert rows to database. Adding flush listener with the lowest priority is very risky. I'd rather have dispatcher which begins a transaction and based on dispatch result it commits or rollbacks the transaction.

Transactional dispatcher

Dispatching events is not different from classic Symfony dispatcher. For sake of simplicity I directly create dispatcher in code example, but you should use DI in production code.

$dispatcher = new TransactionalDispatcher(
    $container->get('event_dispatcher'),
    $container->get('doctrine.orm.entity_manager')
);
$dispatcher->dispatch('event', new ...Event());

Source code

Dispatcher class, tests and XML/YAML service configuration is available in the gist. You can use it and modify it as you please. Here is the Dispatcher class:

<?php

use Doctrine\ORM\EntityManager;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class TransactionalDispatcher
{
    private $dispatcher;
    private $entityManager;
    private $event;

    public function __construct(EventDispatcherInterface $d, EntityManager $m)
    {
        $this->dispatcher = $d;
        $this->entityManager = $m;
    }

    public function dispatch($eventName, Event $event)
    {
        $this->beginTransaction();
        $this->dispatchEvent($eventName, $event);
        $this->endTransaction();
        return $this->hasSucceed();
    }

    private function beginTransaction()
    {
        $this->entityManager->beginTransaction();
    }

    private function dispatchEvent($eventName, Event $event)
    {
        $this->event = $this->dispatcher->dispatch($eventName, $event);
    }

    private function endTransaction()
    {
        if ($this->hasSucceed()) {
            $this->entityManager->commit();
        } else {
            $this->entityManager->rollback();
        }
    }

    private function hasSucceed()
    {
        return !$this->event->isPropagationStopped();
    }
}

Why dispatcher doesn't implement EventDispatcherInterface?

1. Symfony dispatcher returns Event

When I call dispatcher I check if propagation was *NOT *stopped. For me Symfony approach has two disadvantages which are caused by the fact that event returns true when propagation failed:

  1. Negation in if: if (!$dispatcher→dispatch(...)→isPropagationStopped())
  2. Mocking dispatcher is harder, because you must stub object with isPropagationStopped method

My tests ended up like in the code below. Now I have only one such test in TransactionalDispatcherTest.

class ExampleTest extends \PHPUnit_Framework_TestCase
{
    private $hasDispatcherSucceed = true;

    public function testWhichExpectsDispatcherFailure()
    {
        $this->hasDispatcherSucceed = false;
        // ...
        $this->executeSomething();
    }

    private function executeSomething()
    {
        $event = Mockery::mock('Symfony\Component\EventDispatcher\Event');
        $event->shouldReceive('isPropagationStopped')->andReturn(!$this->hasDispatcherSucceed );

        $dispatcher = Mockery::mock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
        $dispatcher->shouldReceive('dispatch')->once()->andReturn($event);

        // pass dispatcher to tested class, execute, assert expectations
    }
}

2. Unused methods in Dispatcher (composition over inheritance)

I usually only need dispatch method. In rare cases I dynamically add listener (with max priority). But I never used getListeners, hasListeners, removeListener. Additionally I really don't like subscribers, I prefer registering listeners somewhere in YAML configuration.

Say Thanks
Respond