Blog / Laravel / Eventos y Listeners en Laravel 11
Los eventos se usan para desacoplar lógica de los controladores o pŕacticas concretas como el uso de websockets.
Los eventos se usan para desacoplar lógica de los controladores o pŕacticas concretas como el uso de websockets.
Si usamos eventos, éstos irán generalmente acompañados de listeners.
Por defecto los listeners se añadirán en "app/Listeners/" y los eventos en "app/Events/". Si creamos las clases con Artisan se situarán en esos directorios. Además, el sistema de autodescubrimiento de Laravel busca los listeners automáticamente en el directorio correspondiente, por ello, debemos indicarlo explícitamente si vamos a usar un directorio diferente.
Incluiré mi listener en "domain/Foo/Listeners/", además más adelante pretendo ubicar otros listeners siguiendo el patrón, por ejemplo, en "domain/Baz/Listeners/".
Registro el directorio para mis listeners en "bootstrap/app.php", usando un comodín para indicar el uso de diferentes nombres de directorio dentro de "domain/":
->withEvents(discover: [
__DIR__ . '/../domain/*/Listeners',
])
Si, además, se van a mantener listeners en el directorio original, debe indicarse:
->withEvents(discover: [
__DIR__ . '/../app/Listeners',
__DIR__ . '/../domain/*/Listeners',
])
Se va a gestionar una invitación para unirse a un grupo a un usuario.
Creo un evento y un listener asociado al mismo:
php artisan make:event FooEvent
php artisan make:listener FooListener --event=FooEvent
Se crean una clase de evento y la clase listener correspondiente:
<?php
namespace App\Listeners;
use App\Events\FooEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class FooListener
{
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(FooEvent $event): void
{
//
}
}
Donde el evento se asocia al listener mediante el método
handle
del listener.
El evento y el escuchador se crean en los directorios por defecto. Hay que moverlos a sus directorios de destino y ajustar los namespaces.
Siguiendo con mi ejemplo:
php artisan make:event GroupInvitationEvent
Muevo el fichero "app/Events/GroupInvitationEvent.php",
a "Domain/Group/Events/GroupInvitationEvent.php" y ajusto el namespace a:
namespace Domain\Group\Events;
php artisan make:listener InvitationListener
--event=GroupInvitationEvent
Muevo el fichero "app/Listeners/InvitationListener.php",
a "Domain/Group/Listeners/InvitationListener.php" y ajusto el namespace a:
namespace Domain\Group\Listeners;
Comprobar que nombres de clases y sus ubicaciones sean correctas:
composer dump-autoload
Usaré el evento para desacoplar una determinada lógica de la acción recibida en el controlador, es ahí donde indico cuando se invocará el evento.
Siguiendo mi ejemplo, en mi controlador tengo un método público:
public function invite(int $userId, int $groupId): JsonResponse
Recibe los identificadores del usuario a invitar y del grupo.
Internamente, se recupera el usuario que envió la invitación, se comprueban restricciones, etc.
Para lanzar el evento que gestionará la invitación, pueden usarse *dos métodos, la forma orientada a objetos:
GroupInvitationEvent::dispatch(@event_params)
o usando el helper event()
:
event(new GroupInvitationEvent(@event_params));
Para el primero, existen alternativas "condicionales" (desde Laravel 9):
GroupInvitationEvent::dispatchIf($condition, $event_params);
GroupInvitationEvent::dispatchUnless($condition, $event_params);
*y también podríamos llegar a verlo como:
// Illuminate\Support\Facades\Event;
Event::dispatch(new GroupInvitationEvent(@event_params));
Para usar la forma orientada a objetos la clase evento
debe usar el trait Illuminate\Foundation\Events\Dispatchable
y el evento recibirá los parámetros en su constructor.
En mi caso, le paso a GroupInvitationEvent
, el usuario de destino,
el grupo y el nombre del usuario que invito a unirse al mismo:
/**
* Create a new event instance.
*/
public function __construct(
public readonly User $destinationUser,
public readonly Group $game,
public readonly string $invitedByName
)
{}
En la clase evento no voy a añadir nada más por el momento, sólo es un contenedor que recoge algunos parámetros.
He indicado que para lanzar un evento se usa MyEvent::dispatch()
o bien event(new MyEvent())
.
Ambos métodos son equivalentes, si bien MyEvent::dispatch()
es la forma orientada a objectos, crea la instancia del evento internamente
y requiere usar el trait Illuminate\Foundation\Events\Dispatchable
en la clase evento.
Además del helper event
también se puede invocar un evento
con: broadcast(new MyEvent())
Usar
event
o broadcast
dependerá del propósito del evento:
Se usa para activar listeners o suscriptores dentro de Laravel.
No permite hacer Broadcasting, a menos que implemente explícitamente `ShouldBroadcast`.
Ejemplo de uso:
Al registra un usuario se enviara un email de bienvenida (gestionado por un listener) y se notifica al front del panel de administrador (broadcast).
Se usa sólo para broadcasting o emisión de eventos hacia sistemas externos mediante WebSockets (Pusher, Redis, etc.).
No ejecuta listeners.
Debería implementar `ShouldBroadcast`.
Ejemplo de uso:
Se usa para avisar de un nuevo mensaje de chat: se notifica al front via WebSockets (no requiere usar listeners en el backend).
event() |
broadcast() |
|
---|---|---|
Ejecuta listeners | ✅ Sí | ❌ No |
Emite a clientes | ✅ Cuando el evento implementa broadcasting | ✅ Siempre (si está bien configurado) |
Uso recomendado | Lógica interna del backend | Notificaciones en tiempo real al frontend |
Además, de lo anterior, existen métodos especiales para testear eventos o se puede demorar la ejecución del evento a que se de cierta situación, como que se confirme la transacción en la base de datos (desde Laravel 10).
Hay 3 opciones: usar el evento para emitir a cliente externo (broadcasting), y/o usar un escuchador (listener) o bien un subscriptor (subscriber).
El evento puede hacer uso de broadcasting y quedarse ahí
(no habrá una clase listener / subscriber).
Hemos llamado al evento con broadcast()
y dentro del evento,
al implementar ShouldBroadcastNow
debemos usar broadcastOn
.
En la propiedad pública $message
incluidos un string
con la información trasmitir.
Si queremos cambiar el nombre del evento que se va a trasmitir,
en lugar del namespace de la clase de evento, se usa el método
broadcastAs()
que recibe como parámetro un string con
el nombre a trasmitir.
Hemos llamado al evento con event
e hicimos o no uso de broadcasting,
ahora usaremos un listener
para ejecutar una determinada lógica asociada al evento dentro
de la aplicación.
Cada escuchador estará asociado a un sólo evento y es,
a partir de su método único handle
o __invoke
donde se implementará la lógica necesaria. Hay más detalles en el
siguiente apartado.
En mi ejemplo, uso broadcasting para enviar la invitación al usuario, esto añadirá una notificación en su panel de control. Además, le envío un correo electrónico para avisarle, es un caso de uso típico donde usar un listener.
Los subscribers, que comparten directorio y namespace con los listeners se diferencian de estos en que se destinan a gestionar o escuchar múltiples eventos, los listeners pasan entonces de ser clases a métodos del subscriptor.
Una forma de usar un subscriptor en mi ejemplo seria añadir
un evento donde el usuario solicita unirse a un grupo, lo llamaré
GroupApplicationEvent
.
Ya no vamos a necesitar InvitationListener
,
reemplazamos esta clase por InvitationSubscriber
donde
hay dos métodos de gestión de eventos o handlers
que se ocuparan de:
GroupInvitationEvent
donde se invita al usuario a unirse a un grupoGroupApplicationEvent
que invocamos cuando el usuario solicita unirse a un grupo.El subscriptor, además incluye un método donde se registran los listeners. La clase podría ser similar a la siguiente:
// domain/Group/Listeners/InvitationSubscriber.php
class InvitationSubscriber
{
public function handleUserInvitation(GroupInvitationEvent $event): void
{
// ...
}
public function handleUserApply(GroupApplicationEvent $event): void
{
// ...
}
/**
* Register the listeners for the subscriber.
*
* @return array
*/
public function subscribe(Dispatcher $events): array
{
return [
GroupInvitationEvent::class => 'handleUserInvitation',
GroupApplicationEvent::class => 'handleUserApply',
];
}
}
Nos queda
registrar el subscriptor; la forma varia
de cómo se registran los listeners.
Lo haremos en el AppServiceProvider
, en el método boot()
:
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Event::subscribe(InvitationSubscriber::class);
}
NOTA: al registrar los subscribers manualmente
puede ocurrir que se registren doblemente y los métodos handle
se ejecutan más de una vez, algo fácil de detectar añadiéndoles
un trazado. En este caso, eliminar el registro manual,
ya que Laravel los está registrando automáticamente.
NOTA: los métodos handle
de listeners
y subscribers que implementan ShouldQueue
pueden ejecutarse
más de una vez cuando los intentos previos fallan.
Este comportamiento es "normal" en este caso.
En el controlador se indica cuando se produce el evento y se le pasan algunos datos, ahora hay que escuchar cuando se invoca ese evento y añadir la lógica.
Para escuchar el evento, el listener usa el método
handle
(or __invoke
):
/**
* Handle the event.
*/
public function handle(GroupInvitationEvent $event): void
{
// logic here
}
Al invocar el evento le pase, entre otros argumentos, el grupo. Siendo una propiedad pública del evento puede ser accedida a través del mismo.
Dentro del método handle
del listener podemos,
por ejemplo, hacer un debug para probar que se ejecuta:
/**
*Handle the event.
*/
public function handle(GroupInvitationEvent $event): void
{
dump('InvitationListener@handle - Group ID: ' . $event->group->id););
}
Hacemos la llamada al método del controlador que desencadena el evento. Si no hay errores, tampoco deberíamos ver el debug, primero deberemos cachear el nuevo evento.
Para ver el listado de eventos/listeners registrados en la aplicación:
php artisan event:list
esto debe indicar nuestro evento, además aparecen múltiples eventos del framework y paquetes que hallamos instalado, para filtrar y ver si nuestro evento esta registrado:
php artisan event:list --event=Invitation
Si no aparece, "regenerar la cache" de eventos:
php artisan event:cache
o bien, optimizar:
php artisan optimize
Volvemos a listar:
php artisan event:list --event=Invitation
Debería aparecer algo como:
Domain\Group\Events\GroupInvitationEvent ............................ ⇂ Domain\Group\Listeners\InvitationListener@handle
Con el evento/listener cacheado ya deberíamos ver el debug al llamar al método del controlador.
Además del método handle
, el constructor de la clase listener
nos permite inyectar automáticamente las dependencias que se requiera,
ya que los listeners son resueltos por el service container
de Laravel.
28-06-2025