IronWoods.es

Desarrollo web

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.

Ubicar listeners y eventos

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',
        ])

Ejemplo práctico

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:

1. Crear el evento

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;

2. Crear la clase listener asociando el evento

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

Invocar eventos

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.

Métodos para invocar un evento

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:

> event()

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).

> 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).

Evento lanzado, ¿y ahora qué?

Hay 3 opciones: usar el evento para emitir a cliente externo (broadcasting), y/o usar un escuchador (listener) o bien un subscriptor (subscriber).

Evento y broadcasting

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.

Evento con escuchador asociado

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.

Evento con subscriptor asociado

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:

  1. Gestionar el evento preexistente GroupInvitationEvent donde se invita al usuario a unirse a un grupo
  2. Gestionar el evento GroupApplicationEvent 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.

Escuchar eventos

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.

Ver los eventos registrados (cacheados)

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.