Events i Listeners
Com utilitzar el sistema d'events de Laravel per desacoblar la lògica: events, listeners, subscribers i events de model.
Per què events?#
Quan una acció passa a la teva aplicació, sovint necessites que desencadeni múltiples reaccions. Quan un usuari es registra, potser vols enviar un correu de benvinguda, crear un registre d'activitat, assignar-li un pla per defecte i notificar l'equip de vendes. Si poses tota aquesta lògica al controlador de registre, acabes amb un mètode de 100 línies que fa massa coses i és difícil de mantenir. Si més endavant vols afegir una nova reacció (per exemple, enviar una notificació push), has de modificar el controlador, cosa que viola el principi d'obertura/tancament.
Els events resolen aquest problema separant "què ha passat" de "què cal fer quan passa". L'event és un simple objecte que diu "un usuari s'ha registrat" i conté les dades rellevants (l'usuari). Els listeners són classes independents que reaccionen a aquest event: un envia el correu, un altre crea el registre, un altre assigna el pla. Cada listener fa una sola cosa i no sap res dels altres. Si vols afegir una nova reacció, crees un nou listener sense tocar cap codi existent.
Aquesta separació té beneficis pràctics molt clars. El codi és més fàcil de testejar perquè pots testejar cada listener per separat. És més fàcil de mantenir perquè cada classe fa una sola cosa. I és més fàcil d'estendre perquè afegir funcionalitat nova no requereix modificar codi existent, només afegir codi nou.
Crear events i listeners#
Per crear un event i un listener, Laravel proporciona comandes Artisan dedicades. L'event és una classe simple que conté les dades de l'acció que ha passat. El listener és una classe que conté la lògica que s'ha d'executar quan l'event es dispara:
# Crear un event
php artisan make:event OrderPlaced
# Crear un listener vinculat a un event
php artisan make:listener SendOrderConfirmation --event=OrderPlaced
# Crear un listener que s'executa en segon pla
php artisan make:listener ProcessOrderAnalytics --event=OrderPlaced --queuedL'opció --event al crear el listener és una comoditat: afegeix automàticament el type-hint correcte al mètode handle. Si no l'especifiques, hauràs d'afegir-lo manualment. L'opció --queued fa que el listener implementi la interfície ShouldQueue, cosa que significa que s'executarà en segon pla a través de les cues en lloc de bloquejar la petició HTTP.
Estructura d'un event#
Un event és una classe PHP que actua com a contenidor de dades. No conté lògica de negoci: el seu únic propòsit és transportar informació des del punt on es dispara fins als listeners que l'escolten. Les propietats públiques de l'event són les dades que els listeners necessitaran per fer la seva feina.
La convenció és fer les propietats públiques perquè els listeners hi puguin accedir directament amb $event->order. Això simplifica el codi i evita la necessitat de getters:
// app/Events/OrderPlaced.php
namespace App\Events;
use App\Models\Order;
use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderPlaced
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public Order $order,
public User $user,
public float $total
) {}
}El trait SerializesModels és important si l'event s'utilitza amb listeners encuats. Quan un listener s'executa en segon pla, l'event ha de ser serialitzat per ser emmagatzemat a la cua. SerializesModels gestiona automàticament la serialització dels models Eloquent: en lloc de serialitzar tot l'objecte del model, només emmagatzema l'ID i el torna a carregar de la base de dades quan el listener es processa. Això evita problemes amb dades obsoletes i objectes massa grans.
El trait Dispatchable proporciona mètodes estàtics per disparar l'event, com OrderPlaced::dispatch($order, $user, $total). Sense aquest trait, hauries d'utilitzar el helper event() o la facade Event.
Events amb dades calculades#
De vegades, l'event necessita transportar dades que no són directament un model, sinó valors calculats o informació contextual. Per exemple, un event de canvi de preu podria necessitar tant el preu antic com el nou, perquè els listeners puguin comparar-los:
class ProductPriceChanged
{
use Dispatchable, SerializesModels;
public function __construct(
public Product $product,
public float $oldPrice,
public float $newPrice,
public User $changedBy
) {}
public function percentageChange(): float
{
if ($this->oldPrice === 0.0) {
return 0;
}
return round((($this->newPrice - $this->oldPrice) / $this->oldPrice) * 100, 2);
}
}Encara que els events principalment transportin dades, poden tenir mètodes auxiliars per facilitar la feina dels listeners. En l'exemple, percentageChange() calcula el canvi percentual, cosa que evita que cada listener hagi de fer el càlcul per separat.
Estructura d'un listener#
Un listener és una classe que conté la lògica que s'executa quan es dispara un event. El mètode handle rep l'event com a paràmetre (amb type-hint) i fa la feina. Laravel utilitza el type-hint per saber quin event escolta cada listener, cosa que permet el descobriment automàtic:
// app/Listeners/SendOrderConfirmation.php
namespace App\Listeners;
use App\Events\OrderPlaced;
use App\Notifications\OrderConfirmed;
class SendOrderConfirmation
{
public function handle(OrderPlaced $event): void
{
$event->user->notify(
new OrderConfirmed($event->order)
);
}
}La bellesa d'aquest patró és la seva simplicitat. El listener fa exactament una cosa: enviar una notificació de confirmació. No sap qui més escolta l'event OrderPlaced, no sap com s'ha creat la comanda, no sap res del controlador. Només sap que una comanda s'ha col·locat i ha d'enviar una confirmació.
Pots tenir múltiples listeners per al mateix event, cadascun fent una tasca diferent. Tots s'executaran quan l'event es dispari:
// app/Listeners/UpdateInventory.php
class UpdateInventory
{
public function handle(OrderPlaced $event): void
{
foreach ($event->order->items as $item) {
$item->product->decrement('stock', $item->quantity);
}
}
}
// app/Listeners/LogOrderActivity.php
class LogOrderActivity
{
public function handle(OrderPlaced $event): void
{
activity()
->performedOn($event->order)
->causedBy($event->user)
->withProperties(['total' => $event->total])
->log('Comanda realitzada');
}
}
// app/Listeners/NotifySalesTeam.php
class NotifySalesTeam
{
public function handle(OrderPlaced $event): void
{
if ($event->total > 1000) {
Notification::route('slack', config('services.slack.sales'))
->notify(new HighValueOrder($event->order));
}
}
}Fixa't que cada listener pren decisions independentment. NotifySalesTeam només actua si la comanda supera els 1000 euros, però això no afecta els altres listeners. Si demà vols que les comandes superiors a 5000 euros també enviïn un correu al director, crees un nou listener sense tocar res.
Descobriment automàtic#
Laravel descobreix automàticament els listeners basant-se en el type-hint del mètode handle. Si una classe té un mètode handle que accepta un OrderPlaced com a paràmetre, Laravel registra automàticament aquesta classe com a listener de l'event OrderPlaced. No cal registrar res manualment.
Perquè el descobriment automàtic funcioni, els listeners han d'estar dins del directori app/Listeners (o un subdirectori). Laravel escaneja aquest directori durant el boot de l'aplicació i registra tots els listeners que troba. En producció, per millorar el rendiment, pots cachejar els events descoberts:
# Cachejar els events descoberts
php artisan event:cache
# Netejar la cache d'events
php artisan event:clear
# Llistar tots els events i els seus listeners
php artisan event:listSi necessites registrar listeners manualment (per exemple, listeners que no estan a app/Listeners o que tenen una configuració especial), pots fer-ho al EventServiceProvider:
// app/Providers/EventServiceProvider.php
namespace App\Providers;
use App\Events\OrderPlaced;
use App\Listeners\SendOrderConfirmation;
use App\Listeners\UpdateInventory;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
OrderPlaced::class => [
SendOrderConfirmation::class,
UpdateInventory::class,
],
];
}El registre manual i el descobriment automàtic poden coexistir. Els listeners registrats manualment tenen preferència, i els descoberts automàticament es complementen. En la majoria de casos, el descobriment automàtic és suficient i no cal registrar res manualment.
Disparar events#
Hi ha tres maneres de disparar un event a Laravel. Totes fan exactament el mateix, així que l'elecció és una qüestió d'estil i preferència personal. El helper event() és la manera més concisa. El mètode estàtic dispatch() és la manera més explícita. La facade Event és la manera més formal:
use App\Events\OrderPlaced;
use Illuminate\Support\Facades\Event;
// Amb el helper event()
event(new OrderPlaced($order, $user, $order->total));
// Amb el mètode estàtic dispatch (requereix el trait Dispatchable)
OrderPlaced::dispatch($order, $user, $order->total);
// Amb la facade Event
Event::dispatch(new OrderPlaced($order, $user, $order->total));La diferència entre event() i dispatch() és pràcticament nul·la. El helper event() crea una instància de l'event i la passa al dispatcher. El mètode estàtic dispatch() fa el mateix però amb una sintaxi lleugerament diferent: els arguments es passen directament al mètode estàtic en lloc del constructor. Al final, tots dos criden el dispatcher amb el mateix event.
Disparar des d'un controlador#
El lloc més habitual per disparar events és dins d'un controlador, després de completar l'acció principal. Per exemple, després de crear una comanda, disparar l'event OrderPlaced perquè els listeners s'encarreguin de les tasques secundàries:
class OrderController extends Controller
{
public function store(OrderRequest $request)
{
$order = Order::create([
'user_id' => $request->user()->id,
'total' => $request->total,
'status' => 'pending',
]);
$order->items()->createMany($request->items);
// Disparar l'event: els listeners s'encarreguen de la resta
OrderPlaced::dispatch($order, $request->user(), $order->total);
return response()->json($order, 201);
}
}El controlador és net i concís: crea la comanda i dispara l'event. No sap ni li importa que s'enviï un correu, que s'actualitzi l'inventari o que es notifiqui l'equip de vendes. Tota aquesta lògica viu als listeners, cadascun a la seva classe independent.
Listeners encuats#
Per defecte, els listeners s'executen síncronament dins de la petició HTTP. Si un listener envia un correu que triga 2 segons, la resposta al client es retarda 2 segons. Per a operacions que no han de ser immediates, és millor encuar el listener perquè s'executi en segon pla.
Per fer que un listener s'executi en segon pla, implementa la interfície ShouldQueue. Laravel detectarà la interfície i encuarà automàticament el listener en lloc d'executar-lo síncronament:
namespace App\Listeners;
use App\Events\OrderPlaced;
use App\Notifications\OrderConfirmed;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendOrderConfirmation implements ShouldQueue
{
// Connexió de la cua
public string $connection = 'redis';
// Nom de la cua
public string $queue = 'emails';
// Retard abans d'executar (en segons)
public int $delay = 10;
// Nombre d'intents
public int $tries = 3;
public function handle(OrderPlaced $event): void
{
$event->user->notify(
new OrderConfirmed($event->order)
);
}
public function failed(OrderPlaced $event, \Throwable $exception): void
{
Log::error('Error enviant confirmació de comanda', [
'order_id' => $event->order->id,
'error' => $exception->getMessage(),
]);
}
}Les propietats $connection, $queue, $delay i $tries funcionen exactament com als jobs. Pots especificar a quina connexió i cua s'envia el listener, quant de temps ha d'esperar abans d'executar-se i quantes vegades es pot reintentar si falla.
Executar després del commit#
Quan un listener encuat es dispara dins d'una transacció de base de dades, hi ha un problema potencial. El listener es pot processar abans que la transacció es confirmi (commit). Si el listener intenta accedir a les dades creades dins de la transacció, pot trobar-se amb que no existeixen encara. La propietat $afterCommit resol això: el listener no s'encua fins que la transacció s'hagi confirmat:
class SendOrderConfirmation implements ShouldQueue
{
public bool $afterCommit = true;
public function handle(OrderPlaced $event): void
{
// Garantit que la comanda ja existeix a la base de dades
$event->user->notify(
new OrderConfirmed($event->order)
);
}
}Decidir condicionalment si encuar#
De vegades, un listener encuat hauria de decidir si realment cal encuar-lo basant-se en les dades de l'event. El mètode shouldQueue permet aquesta decisió. Si retorna false, el listener no s'encua i no s'executa:
class NotifySalesTeam implements ShouldQueue
{
public function shouldQueue(OrderPlaced $event): bool
{
// Només notificar per comandes superiors a 500 euros
return $event->total > 500;
}
public function handle(OrderPlaced $event): void
{
Notification::route('slack', config('services.slack.sales'))
->notify(new HighValueOrder($event->order));
}
}Això és diferent de posar un if dins del handle. Amb shouldQueue, el listener directament no s'encua, cosa que estalvia recursos de la cua. Amb un if al handle, el listener s'encua, el worker el processa i llavors decideix no fer res, cosa que consumeix recursos innecessàriament.
Event subscribers#
Un subscriber és una classe que pot escoltar múltiples events. En lloc de crear un listener per event, pots agrupar listeners relacionats en un sol subscriber. Això és útil quan tens una sèrie d'events que estan temàticament relacionats i la seva lògica de resposta pertany al mateix context.
Per exemple, tots els events relacionats amb l'autenticació d'un usuari (login, logout, registre, verificació) podrien tenir els seus listeners agrupats en un sol subscriber:
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Events\Verified;
use Illuminate\Events\Dispatcher;
class UserEventSubscriber
{
public function handleUserRegistered(Registered $event): void
{
// Registrar l'activitat
activity()
->performedOn($event->user)
->log('Usuari registrat');
// Assignar un pla per defecte
$event->user->assignPlan('free');
}
public function handleUserLogin(Login $event): void
{
$event->user->update([
'last_login_at' => now(),
'last_login_ip' => request()->ip(),
]);
}
public function handleUserLogout(Logout $event): void
{
activity()
->performedOn($event->user)
->log('Sessió tancada');
}
public function handleUserVerified(Verified $event): void
{
$event->user->notify(new WelcomeNotification);
}
public function subscribe(Dispatcher $events): array
{
return [
Registered::class => 'handleUserRegistered',
Login::class => 'handleUserLogin',
Logout::class => 'handleUserLogout',
Verified::class => 'handleUserVerified',
];
}
}El mètode subscribe retorna un array que mapeja cada event al mètode que l'ha de gestionar. Fixa't que els noms dels mètodes són descriptius: handleUserLogin, handleUserLogout, etc. Això fa que la classe sigui fàcil de llegir i entendre d'un cop d'ull.
Per registrar el subscriber, afegeix-lo a la propietat $subscribe de l'EventServiceProvider:
// app/Providers/EventServiceProvider.php
protected $subscribe = [
UserEventSubscriber::class,
];Un subscriber també pot retornar un array de closures, cosa que pot ser útil quan la lògica és senzilla i no justifica un mètode separat:
public function subscribe(Dispatcher $events): array
{
return [
Login::class => [
function (Login $event) {
$event->user->update(['last_login_at' => now()]);
},
],
];
}Events de model#
Eloquent dispara events automàticament durant el cicle de vida dels models. Quan crees, actualitzes o elimines un model, Eloquent dispara events abans i després de cada operació. Això permet executar lògica addicional en moments clau sense modificar el codi que manipula els models.
Els events de model disponibles són: creating, created, updating, updated, saving, saved, deleting, deleted, trashed, forceDeleting, forceDeleted, restoring, restored, replicating. Els events que acaben en "-ing" (com creating) es disparen abans de l'operació i poden cancel·lar-la si retornen false. Els events que acaben en "-ed" (com created) es disparen després de l'operació.
La diferència entre saving/saved i creating/created (o updating/updated) és subtil però important. Els events saving i saved es disparen tant per creació com per actualització. Els events creating/created només es disparen per creació, i updating/updated només per actualització. Si necessites una lògica que s'executi tant en crear com en actualitzar (per exemple, sanititzar un camp), utilitza saving. Si la lògica és específica de la creació (per exemple, generar un UUID), utilitza creating.
Listeners per a events de model#
Pots escoltar events de model com qualsevol altre event. El nom complet de l'event és eloquent.{acció}: {Model}:
// Al EventServiceProvider o amb descobriment automàtic
class SetDefaultValues
{
public function handle(Creating $event): void
{
// Nota: amb Laravel 11+, pots utilitzar la closure directament al model
}
}La manera més habitual d'escoltar events de model és directament al model, amb closures al mètode booted:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
protected static function booted(): void
{
static::creating(function (Article $article) {
$article->slug = Str::slug($article->title);
$article->user_id = auth()->id();
});
static::updating(function (Article $article) {
if ($article->isDirty('title')) {
$article->slug = Str::slug($article->title);
}
});
static::deleting(function (Article $article) {
// Eliminar les imatges associades
Storage::delete($article->images->pluck('path')->toArray());
});
}
}Aquesta aproximació és senzilla i funciona bé per a lògica lleugera. Però si els callbacks creixen massa, el model es torna difícil de mantenir. Per a lògica més complexa, és millor utilitzar observers.
Observers#
Un observer és una classe dedicada a escoltar els events d'un model específic. Agrupa tots els listeners del model en una sola classe amb mètodes ben definits. Cada mètode de l'observer correspon a un event del cicle de vida del model:
php artisan make:observer ArticleObserver --model=Articlenamespace App\Observers;
use App\Models\Article;
use Illuminate\Support\Str;
class ArticleObserver
{
public function creating(Article $article): void
{
$article->slug = Str::slug($article->title);
if (auth()->check()) {
$article->user_id = auth()->id();
}
}
public function updating(Article $article): void
{
if ($article->isDirty('title')) {
$article->slug = Str::slug($article->title);
}
}
public function created(Article $article): void
{
// Notificar els seguidors de l'autor
$article->author->followers->each(function ($follower) use ($article) {
$follower->notify(new NewArticleNotification($article));
});
}
public function deleting(Article $article): void
{
// Eliminar les imatges associades
$article->images->each(function ($image) {
Storage::delete($image->path);
$image->delete();
});
}
public function forceDeleted(Article $article): void
{
// Netejar tots els registres relacionats
activity()->where('subject_type', Article::class)
->where('subject_id', $article->id)
->delete();
}
}La convenció de noms és clara: el mètode creating es crida quan el model s'està creant (abans de guardar), created quan ja s'ha creat (després de guardar), updating quan s'està actualitzant, updated quan ja s'ha actualitzat, etc. No cal registrar cada event individualment: Laravel detecta automàticament quins mètodes existeixen a l'observer.
Registrar observers#
Hi ha dues maneres de registrar un observer. La manera moderna (Laravel 10+) és amb l'atribut ObservedBy directament al model:
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
{
// ...
}L'atribut ObservedBy és la manera més neta perquè el registre viu amb el model. Quan obres el fitxer del model, veus immediatament que té un observer. No cal buscar en cap provider per saber qui escolta els events del model.
L'alternativa és registrar l'observer al EventServiceProvider o a l'AppServiceProvider:
// app/Providers/AppServiceProvider.php
use App\Models\Article;
use App\Observers\ArticleObserver;
public function boot(): void
{
Article::observe(ArticleObserver::class);
}Aturar la propagació d'events#
De vegades, vols que un listener aturi la propagació de l'event perquè els listeners restants no s'executin. Per exemple, si un listener de validació detecta un problema, pot voler aturar tot el procés. Per fer-ho, el mètode handle ha de retornar false:
class ValidateOrderStock
{
public function handle(OrderPlaced $event): bool|null
{
foreach ($event->order->items as $item) {
if ($item->product->stock < $item->quantity) {
Log::warning('Estoc insuficient per al producte: ' . $item->product->name);
// Aturar la propagació: els altres listeners no s'executaran
return false;
}
}
return null;
}
}Quan un listener retorna false, Laravel deixa de cridar els listeners restants per a aquell event. L'ordre en què s'executen els listeners depèn de l'ordre de registre (al $listen de l'EventServiceProvider) o de l'ordre en què Laravel els descobreix automàticament. Això significa que l'ordre importa si fas servir la propagació: el listener de validació ha d'executar-se abans que els altres.
Tingues en compte que aturar la propagació no reverteix les accions dels listeners que ja s'han executat. Si el primer listener ha enviat un correu i el segon atura la propagació, el correu ja s'ha enviat. Per a operacions que necessiten atomicitat, considera utilitzar transaccions de base de dades o un patró saga.
Listeners anònims amb closures#
Per a listeners senzills que no justifiquen una classe completa, pots registrar closures directament. Això és útil per a lògica lleugera que no necessita ser testejada independentment o reutilitzada:
// app/Providers/EventServiceProvider.php
use App\Events\OrderPlaced;
use Illuminate\Support\Facades\Event;
public function boot(): void
{
Event::listen(function (OrderPlaced $event) {
Log::info('Comanda creada: ' . $event->order->id);
});
// Amb classe d'event explícita
Event::listen(OrderPlaced::class, function (OrderPlaced $event) {
Cache::forget('orders-count');
});
}Les closures són convenients per a operacions petites com netejar cache, escriure logs o actualitzar comptadors. Però si la lògica creix, és millor moure-la a una classe listener dedicada. Les closures no es poden testejar individualment (no pots fer fake d'una closure) i no es poden encuar (s'executen sempre síncronament).
Per encuar una closure listener, utilitza el mètode queueable:
use Illuminate\Events\queueable;
Event::listen(queueable(function (OrderPlaced $event) {
// Això s'executarà en segon pla
Mail::to($event->user)->send(new OrderSummary($event->order));
})->onQueue('emails')->delay(30));Testejar events#
Laravel proporciona eines per testejar events de manera aïllada. La facade Event::fake() intercepta tots els events disparats durant el test i impedeix que els listeners s'executin. Això permet verificar que els events correctes s'han disparat sense executar efectes secundaris com enviar correus o fer crides a APIs:
namespace Tests\Feature;
use App\Events\OrderPlaced;
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class OrderTest extends TestCase
{
public function test_placing_order_dispatches_event(): void
{
Event::fake();
$user = User::factory()->create();
$this->actingAs($user);
$response = $this->postJson('/api/orders', [
'items' => [
['product_id' => 1, 'quantity' => 2],
],
]);
$response->assertStatus(201);
// Verificar que l'event s'ha disparat
Event::assertDispatched(OrderPlaced::class);
// Verificar amb condicions
Event::assertDispatched(OrderPlaced::class, function ($event) use ($user) {
return $event->user->id === $user->id;
});
// Verificar que un event NO s'ha disparat
Event::assertNotDispatched(OrderCancelled::class);
}
}Fake parcial#
De vegades vols fer fake només d'alguns events i deixar que els altres s'executin normalment. Això és útil quan necessites que els listeners d'un event s'executin (per exemple, per crear dades relacionades) però vols interceptar-ne un altre:
public function test_order_process(): void
{
// Només interceptar OrderShipped, els altres events s'executen normalment
Event::fake([OrderShipped::class]);
// Crear la comanda (els listeners d'OrderPlaced s'executen)
$order = Order::create([...]);
// Processar el pagament (normal)
$order->processPayment();
// Verificar que OrderShipped s'ha disparat
Event::assertDispatched(OrderShipped::class);
}Verificar el nombre de dispatches#
Pots verificar quantes vegades s'ha disparat un event, cosa que és útil per assegurar-te que no s'estan disparant events duplicats:
public function test_bulk_import(): void
{
Event::fake();
// Importar 10 productes
$this->importProducts(10);
// Verificar que s'ha disparat exactament 10 vegades
Event::assertDispatchedTimes(ProductImported::class, 10);
// Verificar que cap event no s'ha disparat
Event::assertNothingDispatched();
}Testejar listeners individualment#
També pots testejar listeners de manera aïllada, instanciant-los directament i cridant el mètode handle amb un event construït manualment. Això és útil per testejar la lògica interna del listener sense dependre de tot el flux de l'event:
public function test_send_order_confirmation_sends_notification(): void
{
Notification::fake();
$user = User::factory()->create();
$order = Order::factory()->for($user)->create();
$listener = new SendOrderConfirmation;
$listener->handle(new OrderPlaced($order, $user, $order->total));
Notification::assertSentTo($user, OrderConfirmed::class);
}Aquesta aproximació dóna control total sobre les dades de l'event i permet testejar escenaris específics sense haver de reproduir tot el flux de la petició HTTP. Combinar tests d'integració (que verifiquen que l'event es dispara) amb tests unitaris dels listeners (que verifiquen la lògica de cada listener) proporciona una cobertura completa.