Lección 33 de 45 15 min de lectura

Eventos y Listeners

Los eventos permiten desacoplar partes de tu aplicación. En lugar de ejecutar código directamente cuando algo ocurre (un usuario se registra, se crea un pedido), disparas un evento y uno o más listeners reaccionan a él. Esto hace tu código más limpio, testeable y fácil de mantener.

¿Por qué usar eventos?

Imagina que cuando un usuario se registra quieres: enviar un email de bienvenida, notificar al equipo de ventas y registrar la acción en un log. Sin eventos, el controlador se llenaría de código:

php
<?php

// Sin eventos: controlador acoplado y difícil de mantener
public function register(Request $request): RedirectResponse
{
    $user = User::create($validated);

    // Enviar email de bienvenida
    Mail::to($user)->send(new WelcomeMail($user));

    // Notificar al equipo de ventas
    Notification::send($salesTeam, new NewUserNotification($user));

    // Registrar en el log
    Log::info('Nuevo usuario registrado', ['user_id' => $user->id]);

    // Y si mañana quieres añadir más acciones...

    return redirect()->route('dashboard');
}

Con eventos, el controlador solo dispara un evento y cada listener se encarga de una tarea específica:

php
<?php

// Con eventos: controlador limpio y desacoplado
public function register(Request $request): RedirectResponse
{
    $user = User::create($validated);

    event(new UserRegistered($user));

    return redirect()->route('dashboard');
}

Crear un evento

Un evento es una clase simple que contiene los datos relacionados con lo que ha ocurrido:

bash
php artisan make:event UserRegistered
php
<?php

declare(strict_types=1);

namespace App\Events;

use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class UserRegistered
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
        public User $user
    ) {}
}

El evento simplemente almacena el usuario que se registró. Los traits que incluye son:

  • Dispatchable: Permite disparar el evento con UserRegistered::dispatch($user)
  • SerializesModels: Serializa correctamente los modelos Eloquent si el evento se encola
  • InteractsWithSockets: Necesario si quieres transmitir el evento por WebSockets

Crear un listener

Un listener es una clase que reacciona a un evento y ejecuta una acción específica:

bash
php artisan make:listener SendWelcomeEmail --event=UserRegistered
php
<?php

declare(strict_types=1);

namespace App\Listeners;

use App\Events\UserRegistered;
use App\Mail\WelcomeMail;
use Illuminate\Support\Facades\Mail;

class SendWelcomeEmail
{
    public function handle(UserRegistered $event): void
    {
        Mail::to($event->user)->send(new WelcomeMail($event->user));
    }
}

El método handle recibe el evento como parámetro y tiene acceso a todos sus datos (en este caso, $event->user).

Descubrimiento automático de listeners

Laravel descubre automáticamente los listeners gracias al type-hint del método handle. Si el método recibe un evento como parámetro, Laravel lo registra automáticamente:

php
<?php

declare(strict_types=1);

namespace App\Listeners;

use App\Events\UserRegistered;
use App\Mail\WelcomeMail;
use Illuminate\Support\Facades\Mail;

class SendWelcomeEmail
{
    // Laravel detecta automáticamente que este listener
    // escucha UserRegistered gracias al type-hint
    public function handle(UserRegistered $event): void
    {
        Mail::to($event->user)->send(new WelcomeMail($event->user));
    }
}

Laravel escanea el directorio app/Listeners y registra automáticamente cada listener con su evento correspondiente. No necesitas configuración adicional.

Importante: El descubrimiento automático funciona por defecto en Laravel 11+. Los listeners deben estar en app/Listeners y el método handle debe tener el evento como type-hint.

Registrar listeners manualmente

En la mayoría de casos no necesitas registrar nada: el autodiscovery se encarga. Solo usa registro manual si tus listeners están fuera de app/Listeners o necesitas configuración especial:

php
<?php

declare(strict_types=1);

namespace App\Providers;

use App\Events\UserRegistered;
use App\Listeners\LogUserRegistration;
use App\Listeners\NotifySalesTeam;
use App\Listeners\SendWelcomeEmail;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Event::listen(UserRegistered::class, [
            SendWelcomeEmail::class,
            NotifySalesTeam::class,
            LogUserRegistration::class,
        ]);
    }
}

Puedes pasar un array de listeners para registrar varios a la vez. Se ejecutan en el orden del array.

Disparar eventos

Hay varias formas de disparar un evento:

php
<?php

use App\Events\UserRegistered;

// Usando el helper event()
event(new UserRegistered($user));

// Usando el método estático dispatch (gracias al trait Dispatchable)
UserRegistered::dispatch($user);

// Usando la facade Event
Event::dispatch(new UserRegistered($user));

La forma recomendada es UserRegistered::dispatch($user) porque es más explícita y fácil de buscar en el código.

Listeners en cola

Si un listener realiza una tarea lenta (enviar email, llamar a una API externa), puedes ejecutarlo en segundo plano implementando ShouldQueue:

php
<?php

declare(strict_types=1);

namespace App\Listeners;

use App\Events\UserRegistered;
use App\Mail\WelcomeMail;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Mail;

class SendWelcomeEmail implements ShouldQueue
{
    public function handle(UserRegistered $event): void
    {
        Mail::to($event->user)->send(new WelcomeMail($event->user));
    }
}

Con solo implementar la interfaz ShouldQueue, el listener se ejecutará en segundo plano. Laravel serializará el evento y lo procesará cuando el worker de colas esté disponible.

Importante: Para que los listeners encolados funcionen, necesitas tener un worker ejecutándose. Aprenderás más sobre esto en la siguiente lección sobre Colas y Jobs.

Eventos de modelos Eloquent

Eloquent dispara eventos automáticamente cuando ocurren acciones en los modelos:

  • creating / created: Antes/después de crear
  • updating / updated: Antes/después de actualizar
  • saving / saved: Antes/después de crear o actualizar
  • deleting / deleted: Antes/después de eliminar
  • restoring / restored: Antes/después de restaurar (soft deletes)

Puedes escuchar estos eventos directamente en el modelo usando el método booted:

php
<?php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

class Post extends Model
{
    protected static function booted(): void
    {
        static::creating(function (Post $post) {
            $post->slug = Str::slug($post->title);
        });

        static::deleting(function (Post $post) {
            // Eliminar archivos asociados
            $post->images()->each->delete();
        });
    }
}

Observers

Si tienes muchos eventos para un modelo, puedes agruparlos en un Observer:

bash
php artisan make:observer PostObserver --model=Post
php
<?php

declare(strict_types=1);

namespace App\Observers;

use App\Models\Post;
use Illuminate\Support\Str;

class PostObserver
{
    public function creating(Post $post): void
    {
        $post->slug = Str::slug($post->title);
        $post->user_id = auth()->id();
    }

    public function updating(Post $post): void
    {
        if ($post->isDirty('title')) {
            $post->slug = Str::slug($post->title);
        }
    }

    public function deleting(Post $post): void
    {
        $post->comments()->delete();
    }
}

Registra el observer usando el atributo ObservedBy en el modelo:

php
<?php

declare(strict_types=1);

namespace App\Models;

use App\Observers\PostObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;

#[ObservedBy(PostObserver::class)]
class Post extends Model
{
    // ...
}

Detener la propagación

Si un listener devuelve false, se detiene la ejecución de los siguientes listeners para ese evento:

php
<?php

class ValidateUserEmail
{
    public function handle(UserRegistered $event): bool
    {
        if ($this->isDisposableEmail($event->user->email)) {
            $event->user->delete();

            return false; // Detiene la propagación
        }

        return true; // Continúa con los siguientes listeners
    }
}

Resumen

  • Los eventos son clases que representan algo que ha ocurrido
  • Los listeners son clases que reaccionan a eventos y ejecutan acciones
  • Laravel descubre listeners automáticamente por el type-hint del método handle
  • Implementa ShouldQueue para ejecutar listeners en segundo plano
  • Eloquent dispara eventos automáticos (created, updated, deleted, etc.)
  • Los Observers agrupan múltiples eventos de un modelo en una clase
  • Usa el atributo #[ObservedBy] para registrar observers en el modelo

Ejercicios

Ejercicio 1: Evento y listener básico

Crea un evento OrderPlaced que contenga un pedido (Order) y un listener SendOrderConfirmation que simule el envío de un email de confirmación (usa Log::info en lugar de enviar un email real).

Ver solución
php artisan make:event OrderPlaced
php artisan make:listener SendOrderConfirmation --event=OrderPlaced
<?php

// app/Events/OrderPlaced.php

declare(strict_types=1);

namespace App\Events;

use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderPlaced
{
    use Dispatchable, SerializesModels;

    public function __construct(
        public Order $order
    ) {}
}
<?php

// app/Listeners/SendOrderConfirmation.php

declare(strict_types=1);

namespace App\Listeners;

use App\Events\OrderPlaced;
use Illuminate\Support\Facades\Log;

class SendOrderConfirmation
{
    // Laravel detecta automáticamente el evento por el type-hint
    public function handle(OrderPlaced $event): void
    {
        Log::info('Enviando confirmación de pedido', [
            'order_id' => $event->order->id,
            'user_email' => $event->order->user->email,
            'total' => $event->order->total,
        ]);
    }
}
<?php

// Uso en el controlador
use App\Events\OrderPlaced;

public function store(Request $request): RedirectResponse
{
    $order = Order::create([...]);

    OrderPlaced::dispatch($order);

    return redirect()->route('orders.show', $order);
}

Ejercicio 2: Observer para un modelo

Crea un observer para el modelo Article que: genere automáticamente el slug a partir del título cuando se crea, actualice el slug si el título cambia, y registre en el log cuando un artículo se elimina.

Ver solución
php artisan make:observer ArticleObserver --model=Article
<?php

// app/Observers/ArticleObserver.php

declare(strict_types=1);

namespace App\Observers;

use App\Models\Article;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;

class ArticleObserver
{
    public function creating(Article $article): void
    {
        $article->slug = Str::slug($article->title);
    }

    public function updating(Article $article): void
    {
        if ($article->isDirty('title')) {
            $article->slug = Str::slug($article->title);
        }
    }

    public function deleted(Article $article): void
    {
        Log::info('Artículo eliminado', [
            'id' => $article->id,
            'title' => $article->title,
            'deleted_by' => auth()->id(),
        ]);
    }
}
<?php

// app/Models/Article.php

declare(strict_types=1);

namespace App\Models;

use App\Observers\ArticleObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;

#[ObservedBy(ArticleObserver::class)]
class Article extends Model
{
    protected $fillable = ['title', 'content', 'slug'];
}

Ejercicio 3: Múltiples listeners para un evento

Para el evento UserRegistered, crea tres listeners: SendWelcomeEmail, CreateDefaultSettings y NotifyAdmins. Registra los tres en AppServiceProvider.

Ver solución
php artisan make:listener SendWelcomeEmail --event=UserRegistered
php artisan make:listener CreateDefaultSettings --event=UserRegistered
php artisan make:listener NotifyAdmins --event=UserRegistered
<?php

// app/Listeners/SendWelcomeEmail.php

declare(strict_types=1);

namespace App\Listeners;

use App\Events\UserRegistered;
use Illuminate\Support\Facades\Log;

class SendWelcomeEmail
{
    public function handle(UserRegistered $event): void
    {
        Log::info('Enviando email de bienvenida a ' . $event->user->email);
    }
}
<?php

// app/Listeners/CreateDefaultSettings.php

declare(strict_types=1);

namespace App\Listeners;

use App\Events\UserRegistered;

class CreateDefaultSettings
{
    public function handle(UserRegistered $event): void
    {
        $event->user->settings()->create([
            'notifications_enabled' => true,
            'theme' => 'light',
            'language' => 'es',
        ]);
    }
}
<?php

// app/Listeners/NotifyAdmins.php

declare(strict_types=1);

namespace App\Listeners;

use App\Events\UserRegistered;
use App\Models\User;
use Illuminate\Support\Facades\Log;

class NotifyAdmins
{
    public function handle(UserRegistered $event): void
    {
        $adminCount = User::where('is_admin', true)->count();

        Log::info("Notificando a {$adminCount} administradores sobre nuevo usuario");
    }
}
<?php

// app/Providers/AppServiceProvider.php

declare(strict_types=1);

namespace App\Providers;

use App\Events\UserRegistered;
use App\Listeners\CreateDefaultSettings;
use App\Listeners\NotifyAdmins;
use App\Listeners\SendWelcomeEmail;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Event::listen(UserRegistered::class, [
            SendWelcomeEmail::class,
            CreateDefaultSettings::class,
            NotifyAdmins::class,
        ]);
    }
}

¿Quieres dominar Laravel avanzado?

Aprende arquitectura de software, patrones de diseño y técnicas avanzadas de Laravel en nuestros cursos premium con proyectos reales.

Ver cursos premium