Event Dispatcher en PHP: eventos y listeners con symfony/event-dispatcher

El patrón evento/listener permite que distintos servicios reaccionen a acciones sin acoplarse entre sí. symfony/event-dispatcher es la implementación PHP más extendida de este patrón y puede usarse como componente independiente.

Instalación

composer require symfony/event-dispatcher

Crear un evento

<?php
use SymfonyContractsEventDispatcherEvent;

class PedidoCreado extends Event
{
    public const NAME = 'pedido.creado';

    public function __construct(
        public readonly int    $pedidoId,
        public readonly string $emailCliente,
        public readonly float  $total
    ) {}
}
?>

Crear un listener

<?php
class EnviarConfirmacionListener
{
    public function __invoke(PedidoCreado $evento): void
    {
        echo "Enviando email a {$evento->emailCliente} para pedido #{$evento->pedidoId}n";
        // mail($evento->emailCliente, 'Pedido confirmado', '...');
    }
}

class ActualizarStockListener
{
    public function __invoke(PedidoCreado $evento): void
    {
        echo "Actualizando stock tras pedido #{$evento->pedidoId}n";
    }
}
?>

Registrar listeners y despachar el evento

<?php
use SymfonyComponentEventDispatcherEventDispatcher;

$dispatcher = new EventDispatcher();

$dispatcher->addListener(PedidoCreado::NAME, new EnviarConfirmacionListener());
$dispatcher->addListener(PedidoCreado::NAME, new ActualizarStockListener());

// Despachar
$evento = new PedidoCreado(pedidoId: 42, emailCliente: '[email protected]', total: 149.99);
$dispatcher->dispatch($evento, PedidoCreado::NAME);
?>

EventSubscriberInterface

Un subscriber agrupa múltiples listeners en una sola clase y declara a qué eventos responde:

<?php
use SymfonyComponentEventDispatcherEventSubscriberInterface;

class PedidoSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            PedidoCreado::NAME   => 'alCrearPedido',
            PedidoCancelado::NAME => ['alCancelarPedido', 10], // prioridad 10
        ];
    }

    public function alCrearPedido(PedidoCreado $evento): void
    {
        echo "Subscriber: pedido #{$evento->pedidoId} creadon";
    }

    public function alCancelarPedido(PedidoCancelado $evento): void
    {
        echo "Subscriber: pedido #{$evento->pedidoId} canceladon";
    }
}

$dispatcher->addSubscriber(new PedidoSubscriber());
?>

Controlar prioridades

El tercer parámetro de addListener() es la prioridad (mayor número = primero):

<?php
// Se ejecuta antes (prioridad 20)
$dispatcher->addListener(PedidoCreado::NAME, new ValidarStockListener(),       20);
// Se ejecuta después (prioridad 0, por defecto)
$dispatcher->addListener(PedidoCreado::NAME, new EnviarConfirmacionListener(),  0);
// Se ejecuta al final (prioridad negativa)
$dispatcher->addListener(PedidoCreado::NAME, new RegistrarLogListener(),       -10);
?>

Detener la propagación con stopPropagation()

<?php
class ValidarStockListener
{
    public function __invoke(PedidoCreado $evento): void
    {
        $stockSuficiente = false; // lógica real aquí

        if (!$stockSuficiente) {
            echo "Stock insuficiente: deteniendo propagaciónn";
            $evento->stopPropagation();
            // Los listeners de menor prioridad no se ejecutarán
        }
    }
}
?>

Eventos con datos modificables

Un patrón útil es que los listeners enriquezcan el evento con datos calculados:

<?php
class PrecioCalculado extends Event
{
    private float $precioFinal;

    public function __construct(
        public readonly float $precioBase,
        public readonly string $codigoCliente
    ) {
        $this->precioFinal = $precioBase;
    }

    public function aplicarDescuento(float $porcentaje): void
    {
        $this->precioFinal *= (1 - $porcentaje / 100);
    }

    public function getPrecioFinal(): float
    {
        return $this->precioFinal;
    }
}

class DescuentoVIPListener
{
    public function __invoke(PrecioCalculado $evento): void
    {
        if ($evento->codigoCliente === 'VIP') {
            $evento->aplicarDescuento(15);
        }
    }
}
?>

Errores comunes

  • Nombre de evento incorrecto: si el listener está registrado con un nombre distinto al que se despacha, no se ejecutará nunca sin ningún error. Usa constantes de clase para evitar typos.
  • stopPropagation() no detiene listeners de otro evento: solo afecta a los listeners del evento actual en esa llamada a dispatch().
  • Dependencias circulares en subscribers: si el subscriber A despacha un evento que a su vez activa el subscriber B que despacha un evento que activa A, entrarás en un bucle infinito.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP