Event discovery in Laravel 12: da EventServiceProvider a auto-discovery per listener disaccoppiati e testabili

Event discovery in Laravel 12: da EventServiceProvider a auto-discovery per listener disaccoppiati e testabili

In una piattaforma marketplace con migliaia di utenti attivi, l'EventServiceProvider conteneva 67 mapping evento→listener - un file da 180 righe dove ogni modifica richiedeva di scorrere l'intero array $listen per trovare la posizione corretta. Il Gang of Four definisce l'Observer pattern come "una dipendenza one-to-many tra oggetti, dove un cambiamento di stato notifica automaticamente tutti i dipendenti." Laravel implementa questo pattern con eventi e listener - ma fino a Laravel 10, la registrazione manuale nell'EventServiceProvider era il collo di bottiglia organizzativo.

Come funziona l'event discovery in Laravel 12 e cosa sostituisce?

L'event discovery, introdotto in Laravel 5.8.9 (aprile 2019) come opt-in e diventato il default da Laravel 11, elimina la registrazione manuale. Il framework scansiona la directory app/Listeners, riflette ogni classe pubblica e ispeziona i metodi handle() e __invoke(): il type-hint del parametro determina quale evento il listener gestisce. In produzione, php artisan event:cache genera un manifest cachato per evitare la scansione a ogni request.

Il cambiamento strutturale più significativo è avvenuto con Laravel 11 (marzo 2024): l'EventServiceProvider è stato rimosso dallo skeleton di default - insieme ad altri ~69 file nella nuova struttura applicativa snella. La registrazione avviene interamente per auto-discovery, con la possibilità di aggiungere listener manuali via Event::listen() in AppServiceProvider:

/* Listener auto-discovered - nessuna registrazione manuale necessaria */
namespace App\Listeners;

use App\Events\OrderPlaced;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Mail;
use App\Mail\OrderConfirmationMailable;

class SendOrderConfirmation implements ShouldQueue
{
    /* Il type-hint su OrderPlaced è sufficiente per la registrazione automatica.
     * ShouldQueue rende il listener asincrono - eseguito da un worker. */
    public function handle(OrderPlaced $event): void
    {
        Mail::to($event->order->customer_email)
            ->send(new OrderConfirmationMailable($event->order));
    }
}

/* In Laravel 12, per directory custom di listener: */
// bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
    ->withEvents(discover: [
        __DIR__.'/../app/Listeners',
        __DIR__.'/../app/Domain/Orders/Listeners',
    ])
    ->create();

/* Registrazione manuale (solo se necessaria) in AppServiceProvider */
public function boot(): void
{
    Event::listen(OrderPlaced::class, function (OrderPlaced $event) {
        Log::info("Ordine {$event->order->id} piazzato");
    });
}

Martin Fowler identifica quattro pattern distinti sotto il termine "event-driven": Event Notification, Event-Carried State Transfer, Event Sourcing e CQRS. L'event system di Laravel implementa il primo - Event Notification - dove il producer segnala un fatto accaduto e i consumer reagiscono indipendentemente. AWS documenta che questo pattern garantisce disaccoppiamento, scalabilità indipendente e fault tolerance - esattamente i vantaggi che l'auto-discovery amplifica eliminando l'accoppiamento configurazionale dell'EventServiceProvider.

Come testare che gli eventi vengano dispatchati e i listener reagiscano correttamente?

Event::fake() intercetta il dispatch degli eventi senza eseguire i listener - verifica il "cosa viene emesso". Per testare il listener stesso, si istanzia direttamente e si chiama handle(), come per i job in coda con withFakeQueueInteractions():

/* Test di integrazione: l'evento viene dispatchato? */
public function test_order_creation_dispatches_event(): void
{
    Event::fake([OrderPlaced::class]);

    $order = Order::factory()->create();
    app(OrderService::class)->confirm($order);

    Event::assertDispatched(OrderPlaced::class, fn ($e) =>
        $e->order->id === $order->id
    );
    Event::assertDispatchedTimes(OrderPlaced::class, 1);
}

/* Test unitario del listener: la logica interna funziona? */
public function test_send_order_confirmation_sends_email(): void
{
    Mail::fake();

    $order = Order::factory()->create();
    $listener = new SendOrderConfirmation();
    $listener->handle(new OrderPlaced($order));

    Mail::assertSent(OrderConfirmationMailable::class, fn ($mail) =>
        $mail->hasTo($order->customer_email)
    );
}

La combinazione dei due livelli - dispatch e logica - garantisce copertura completa senza dipendere dall'auto-discovery nei test. I test automatici dei listener girano in millisecondi perché non c'è overhead di discovery.

Errori comuni nella gestione degli eventi Laravel

Il primo errore è confondere l'auto-discovery di Laravel con l'attributo #[AsEventListener] di Symfony. L'attributo AsEventListener, introdotto in Symfony 5.3, è un meccanismo del Symfony EventDispatcher - non funziona in Laravel. Laravel usa esclusivamente il type-hint del parametro handle() per determinare quale evento un listener gestisce. Importare Symfony\Component\EventDispatcher\Attribute\AsEventListener in un listener Laravel non ha alcun effetto.

Il secondo è mantenere un EventServiceProvider custom durante l'upgrade da Laravel 10 a 11 senza disabilitare l'auto-discovery. Il framework registra l'auto-discovery di default - se il vecchio EventServiceProvider rimane attivo con $listen, i listener vengono registrati due volte (una via $listen, una via discovery), causando esecuzioni duplicate.

Il terzo è usare singleton() per listener che accedono a stato mutabile. Un listener registrato come singleton mantiene lo stato tra eventi diversi nella stessa request - se modifica proprietà interne durante handle(), l'evento successivo riceve stato inquinato. I listener devono essere stateless o registrati con bind() nel Service Container.

Il quarto è non cachare gli eventi in produzione. Senza event:cache, Laravel esegue la scansione delle directory e la reflection a ogni request - un overhead misurabile su applicazioni con decine di listener. Il comando php artisan event:cache genera il manifest; event:clear lo invalida durante il deploy.

L'event system di Laravel è il meccanismo fondamentale per il disaccoppiamento - ogni azione di business significativa (ordine piazzato, pagamento ricevuto, utente registrato) dovrebbe produrre un evento che i listener consumano indipendentemente. La struttura snella di Laravel 12 rende l'auto-discovery la scelta naturale, e i middleware avanzati completano il quadro per applicativi modulari. Per conoscere il mio approccio all'architettura event-driven in Laravel, visita la mia pagina professionale. Se il tuo EventServiceProvider è un file da centinaia di righe con mapping manuali che crescono a ogni feature, contattami per una consulenza dedicata - partiamo dall'inventario degli eventi e dalla migrazione all'auto-discovery.

Ultima modifica: