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.
