Eventos y listeners en Laravel 13: desacopla tu lógica de negocio
Publicar un post encadena emails, invalidación de caché y logs: en Laravel 13 los eventos y listeners implementan el patrón observer para que el controller solo diga qué ha pasado, y con el event discovery ni siquiera necesitas registrarlos a mano.
El problema: controllers saturados de responsabilidades
Un controller que publica un post y además envía el email de aviso, invalida la caché de la página y escribe en el log se convierte en una lista interminable de llamadas acopladas. Cada vez que alguien añade una tarea nueva al flujo, hay que tocar el controller, y el controller acaba sabiendo demasiado sobre el resto de la aplicación. Es exactamente el problema que resuelve el sistema de eventos de Laravel 13, lanzado el 17 de marzo de 2026.
El patrón observer: el emisor no conoce a los oyentes
El sistema de eventos de Laravel es una implementación del patrón observer: el evento describe qué ha ocurrido en la aplicación y los listeners reaccionan a ese hecho, sin que el emisor tenga que saber quién escucha ni cuántos son. Por convención, las clases de eventos viven en app/Events y los listeners en app/Listeners, aunque puedes ubicarlos donde prefieras mientras el autoloading los encuentre.
Cuándo usar eventos (y cuándo no)
Los eventos brillan cuando un hecho del dominio tiene varios reaccionarios o cuando sabes que en el futuro aparecerán más: publicar contenido, registrar un usuario, cobrar un pedido. No los uses para lógica síncrona trivial que solo lee el controller, porque añadirías una capa de indirección sin beneficio. Y no los confundas con sus vecinos: un job es una tarea encolada concreta, una notificación es el mecanismo de envío (email, base de datos, Slack) que suele actuar como listener, y el broadcasting difunde eventos al navegador, una capa aparte sobre el mismo sistema.
Crear un evento y un listener en Laravel 13
La forma más rápida de empezar es con los generadores de artisan, que crean las clases y las dejan listas para rellenar.
php artisan make:event y make:listener --event
php artisan make:event PostPublished
php artisan make:listener SendPostNotification --event=PostPublishedEl segundo comando hace el trabajo pesado: importa la clase del evento y type-hintea el parámetro del método handle automáticamente, de modo que el listener queda conectado al evento sin que escribas una línea de más. Además, los listeners generados ya traen importada la interfaz ShouldQueue, lista para cuando quieras encolarlos.
La estructura de app/Events y app/Listeners
El evento es una clase simple: su constructor recibe los datos del hecho ocurrido y normalmente expone propiedades públicas de solo lectura. El listener recibe la instancia del evento en su método handle y ejecuta la reacción. Mantener esa separación es lo que te permite añadir reacciones nuevas sin tocar el emisor.
Registrar listeners: el event discovery
En Laravel 13 no hace falta registrar los listeners a mano en la mayoría de los casos: el event discovery se encarga.
Cómo el escaneo de app/Listeners registra métodos handle e __invoke
Por defecto, Laravel escanea el directorio app/Listeners y registra automáticamente todo método que empiece por handle o sea __invoke y cuyo parámetro type-hintee un evento. Si el listener recibe PostPublished, Laravel lo asocia a ese evento sin configuración adicional. Es el comportamiento predeterminado, así que solo tienes que crear las clases y funcionan.
Registro manual en EventServiceProvider cuando haga falta
El registro manual sigue disponible y conviene cuando el listener no está en app/Listeners, viene de un paquete o prefieres la lista explícita. En el EventServiceProvider:
protected $listen = [
PostPublished::class => [
SendPostNotification::class,
],
];Los eventos listados aquí se registran igualmente aunque exista el discovery, así que ambas vías conviven sin problema.
Lanzar el evento
Una vez creado el evento, lanzarlo desde cualquier punto de la aplicación es una sola línea.
event() y dispatch(): con o sin la interfaz ShouldDispatch
El helper event() funciona siempre y es la vía más directa:
event(new PostPublished($post));Si la clase del evento implementa la interfaz ShouldDispatch, puedes lanzarlo además con el método estático dispatch(), que resulta cómodo en controladores y tests porque elimina el new:
PostPublished::dispatch($post);Pasar datos al evento y leerlos en el listener
Los datos viajan por el constructor del evento y se leen desde la instancia en el listener:
class PostPublished
{
public function __construct(public Post $post) {}
}
class SendPostNotification
{
public function handle(PostPublished $event): void
{
// $event->post está disponible aquí
}
}Listeners en cola: la interfaz ShouldQueue
Cuando la reacción es lenta o no necesita bloquear la respuesta (enviar un email, llamar a un servicio externo), el listener debe ir a la cola.
Encolar un listener sin tocar la lógica
Basta con implementar la interfaz ShouldQueue, que no obliga a declarar ningún método:
class SendPostNotification implements ShouldQueue
{
public function handle(PostPublished $event): void
{
// se ejecutará en la cola, no en la petición
}
}Laravel detecta la interfaz y envía el listener a la cola automáticamente en lugar de ejecutarlo en la misma petición. El código del método no cambia nada.
Retrasos, cola específica y manejo de fallos con failed jobs
Puedes declarar propiedades públicas para afinar el comportamiento, como el retraso o la cola concreta:
class SendPostNotification implements ShouldQueue
{
public int $delay = 10;
public string $queue = 'notifications';
public function handle(PostPublished $event): void {}
}Si el listener falla tras varios intentos, el trabajo acaba en la tabla de failed jobs igual que cualquier job encolado, así que el monitorizado y los reintentos siguen el flujo estándar de colas de Laravel.
Listeners wildcard y subscribers
Para casos donde un listener debe reaccionar a muchos eventos, Laravel ofrece dos herramientas.
Escuchar varios eventos con '*'
Un listener wildcard se registra con el comodín * y recibe el nombre del evento como primer argumento:
$events->listen('*', function (string $eventName, array $data) {
// reacciona a cualquier evento
});Es útil para auditoría, métricas o logs globales, aunque conviene usarlo con moderación para no acoplar todo el sistema a un único oyente.
Agrupar listeners en una clase subscriber
Un subscriber agrupa varios listeners en una sola clase y los registra todos desde su método subscribe:
class PostEventSubscriber
{
public function subscribe(Dispatcher $events): void
{
$events->listen(PostPublished::class, [$this, 'onPublished']);
$events->listen(PostDeleted::class, [$this, 'onDeleted']);
}
}Después se registra el subscriber en el EventServiceProvider con la propiedad $subscribe, y todas las escuchas quedan declaradas en un solo lugar, lo que facilita la lectura cuando un evento tiene muchas reacciones.
Testing de eventos: Event::fake()
El sistema de eventos se testea sin efectos secundarios gracias a Event::fake(), que sustituye los listeners reales por un registro de lo que se lanza.
assertDispatched y assertListening en PHPUnit/Pest
En el test, reemplaza los listeners antes de la acción y verifica el contrato después:
Event::fake();
$this->actingAs($user)->post('/posts', $data);
Event::assertDispatched(PostPublished::class);
Event::assertListening(
PostPublished::class,
SendPostNotification::class
);assertDispatched comprueba que el evento se lanzó y assertListening que el listener está conectado al evento, sin ejecutar el email ni invalidar caché de verdad. Es la forma más limpia de probar el contrato del evento.
Ejemplo real: PostPublished en un blog Laravel 13
El ejemplo conductor de esta guía es un blog en Laravel 13 como el que estás leyendo: al publicar un post hay que invalidar la caché de la página, notificar a los suscriptores y escribir un log.
El controller antes: tres tareas acopladas
Sin eventos, el método store hace las tres cosas él mismo y conoce los detalles de caché, notificaciones y logging:
public function store(PostRequest $request)
{
$post = Post::create($request->validated());
Cache::forget('posts.index');
Notification::send($this->subscribers(), new NewPost($post));
Log::info('Post publicado', ['id' => $post->id]);
return redirect()->route('posts.show', $post);
}Funciona, pero cada nueva tarea engorda el controller y cualquier cambio en una de ellas obliga a tocarlo.
El controller después: un evento y tres listeners (caché, notificación, log)
Con eventos, el controller solo lanza PostPublished y cada reacción vive en su propia clase:
public function store(PostRequest $request)
{
$post = Post::create($request->validated());
event(new PostPublished($post));
return redirect()->route('posts.show', $post);
}Los tres listeners (InvalidatePageCache, NotifySubscribers y LogPostPublished) se encargan cada uno de su tarea, se registran solos gracias al event discovery y pueden encolarse implementando ShouldQueue sin tocar el controller. Añadir una reacción nueva es crear una clase más, nada más.
Conclusión
Los eventos y listeners de Laravel 13 desacoplan tu lógica de negocio con el patrón observer: el controller dice qué ha pasado y los listeners reaccionan, con event discovery, colas con ShouldQueue, subscribers y tests con Event::fake(). Empieza aplicándolo al flujo de publicación de tu blog y verás cómo los controllers adelgazan y las nuevas tareas dejan de tocar el código existente. Si te ha resultado útil, sigue leyendo el blog para más guías de Laravel 13 y desarrollo web.