Despachar eventos Symfony em transações

Symfony dispatcher é uma ferramenta poderosa, mas como em qualquer framework, talvez seja muito projetado para propósitos gerais. Um caso de uso ausente é o despacho de evento na transação. Por exemplo, quando um ouvinte falha, você não deseja inserir linhas no banco de dados. Adicionar ouvinte de liberação com a prioridade mais baixa é muito arriscado. Eu prefiro ter o despachante que inicia uma transação e com base no resultado do despacho ele confirma ou reverte a transação.

Despachante transacional

Despachar eventos não é diferente do despachante Symfony clássico. Para simplificar, eu crio o despachante diretamente no exemplo de código, mas você deve usar DI no código de produção.

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

Código fonte

A classe do despachante, os testes e a configuração do serviço XML / YAML estão disponíveis na essência . Você pode usá-lo e modificá-lo como quiser. Aqui está a classe Dispatcher:

<?php

use DoctrineORMEntityManager;
use SymfonyComponentEventDispatcherEvent;
use SymfonyComponentEventDispatcherEventDispatcherInterface;

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();
}
}

Por que o dispatcher não implementa EventDispatcherInterface?

1. Symfony dispatcher retorna evento

Quando eu chamo o despachante, eu verifico se a propagação * NÃO * foi interrompida. Para mim, a abordagem do Symfony tem duas desvantagens que são causadas pelo fato de que o evento retorna verdadeiro quando a propagação falha:

  1. Negação em se: if (!$dispatcherdispatch(...)→isPropagationStopped())
  2. Zombar do despachante é mais difícil, porque você deve stub o objeto com o isPropagationStoppedmétodo

Meus testes terminaram como no código abaixo. Agora eu tenho apenas um desses testes em TransactionalDispatcherTest .

class ExampleTest extends PHPUnit_Framework_TestCase
{
private $hasDispatcherSucceed = true;

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

private function executeSomething()
{
$event
= Mockery::mock('SymfonyComponentEventDispatcherEvent');
$event
->shouldReceive('isPropagationStopped')->andReturn(!$this->hasDispatcherSucceed );

$dispatcher
= Mockery::mock('SymfonyComponentEventDispatcherEventDispatcherInterface');
$dispatcher
->shouldReceive('dispatch')->once()->andReturn($event);

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

2. Métodos não utilizados no Dispatcher (composição sobre herança)

Normalmente só preciso do método de envio. Em casos raros, eu adiciono ouvinte dinamicamente (com prioridade máxima). Mas eu nunca usei getListeners, hasListeners, removeListener. Além disso, eu realmente não gosto subscribers, prefiro registrar ouvintes em algum lugar na configuração YAML.