Il sistema di eventi e listener di Laravel è una potente implementazione del Observer pattern, che permette di disaccoppiare diverse parti di un'applicazione. Quando un determinato evento si verifica nel tuo business (ad esempio, un utente si registra, un ordine viene piazzato, un file viene caricato), uno o più listener possono reagire a quell'evento per eseguire azioni specifiche, come inviare email, aggiornare statistiche, o accodare job. Questo approccio promuove un codice più pulito, modulare e manutenibile.
Tuttavia, nelle applicazioni Laravel 9 o Laravel 10 più datate o di grandi dimensioni, la registrazione manuale di tutti gli eventi e dei loro listener all'interno della proprietà $listen
dell'EventServiceProvider
può diventare un compito oneroso e portare a file di configurazione molto estesi. Fortunatamente, con l'evoluzione del framework e l'adozione delle funzionalità più recenti di PHP (come gli attributi di PHP 8), Laravel (specialmente a partire dalla versione 11 e in prospettiva Laravel 12) offre modi più eleganti e automatizzati per gestire questa registrazione: l'event discovery e l'attributo #[AsEventListener]
.
Questo articolo tecnico ti guiderà attraverso il processo di refactoring della gestione eventi della tua impresa, passando dall'approccio tradizionale basato sull'EventServiceProvider
a queste tecniche moderne, con l'obiettivo di migliorare la leggibilità del codice, la Developer Experience (DX) e la manutenibilità generale della tua applicazione Laravel.
Se vuoi approfondire, continua a leggere. Se hai una domanda specifica a riguardo di questo articolo, contattami per una consulenza dedicata. Dai anche un'occhiata al mio profilo per capire come posso aiutare concretamente la tua azienda o startup a crescere e a modernizzarsi.
Gestione eventi "classica" in Laravel 9/10: EventServiceProvider
e l'array $listen
Ricordiamo brevemente come eventi e listener venivano tipicamente definiti e registrati.
1. Definizione di un Evento
Un evento è una semplice classe PHP, spesso un Data Transfer Object (DTO), che contiene informazioni sull'accaduto. Di solito utilizza il trait Illuminate\Foundation\Events\Dispatchable
per facilitarne l'invio e Illuminate\Queue\SerializesModels
se l'evento (o i suoi dati) devono essere serializzati per i listener in coda.
// app/Events/OrderPlaced.php (tipico in L9/L10)
namespace App\Events;
use App\Models\Order; // Il tuo model Order
use Illuminate\Broadcasting\InteractsWithSockets; // Opzionale, per broadcasting
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderPlaced
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public Order $order;
/**
* Crea una nuova istanza dell'evento.
*/
public function __construct(Order $order)
{
$this->order = $order;
}
}
2. Creazione di un Listener
Un listener è una classe che contiene un metodo handle
(o un altro metodo specificato) che riceve l'istanza dell'evento e ne esegue la logica associata. Può opzionalmente implementare l'interfaccia Illuminate\Contracts\Queue\ShouldQueue
se la sua esecuzione deve essere asincrona.
// app/Listeners/SendOrderConfirmationEmail.php (tipico in L9/L10)
namespace App\Listeners;
use App\Events\OrderPlaced;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Mail; // Esempio
use App\Mail\OrderConfirmationMailable; // Ipotetico Mailable
class SendOrderConfirmationEmail implements ShouldQueue // In coda
{
use InteractsWithQueue;
/**
* Gestisce l'evento.
*/
public function handle(OrderPlaced $event): void
{
// Esempio: invia un'email di conferma
// Mail::to($event->order->customer_email)
// ->send(new OrderConfirmationMailable($event->order));
logger()->info("Listener SendOrderConfirmationEmail: Email di conferma inviata per ordine #{$event->order->id}");
}
}
// app/Listeners/UpdateInventoryOnOrderPlaced.php (tipico in L9/L10)
namespace App\Listeners;
use App\Events\OrderPlaced;
// Non implementa ShouldQueue, quindi verrà eseguito sincronamente
class UpdateInventoryOnOrderPlaced
{
public function handle(OrderPlaced $event): void
{
// Logica per aggiornare l'inventario
logger()->info("Listener UpdateInventoryOnOrderPlaced: Inventario aggiornato per ordine #{$event->order->id}");
// foreach ($event->order->items as $item) {
// $item->product->decrementStock($item->quantity);
// }
}
}
3. Registrazione manuale nell'EventServiceProvider
Il cuore della registrazione in Laravel 9/10 (quando l'event discovery non era abilitato o usato) era la proprietà protetta $listen
all'interno di app/Providers/EventServiceProvider.php
.
// app/Providers/EventServiceProvider.php (Laravel 9/L10 style)
namespace App\Providers;
use App\Events\OrderPlaced;
use App\Events\UserRegistered; // Altro evento d'esempio
use App\Listeners\SendOrderConfirmationEmail;
use App\Listeners\UpdateInventoryOnOrderPlaced;
use App\Listeners\SendWelcomeEmailToUser; // Altro listener d'esempio
use Illuminate\Auth\Events\Registered; // Evento di Laravel Fortify/Breeze
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider
{
/**
* Le mappature evento-listener per l'applicazione.
*
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
Registered::class => [ // Evento di Laravel per la registrazione utente
SendEmailVerificationNotification::class,
],
UserRegistered::class => [ // Il nostro evento custom
SendWelcomeEmailToUser::class,
],
OrderPlaced::class => [ // Il nostro evento custom per l'ordine
SendOrderConfirmationEmail::class, // Listener in coda
UpdateInventoryOnOrderPlaced::class, // Listener sincrono
],
// ... decine o centinaia di altre mappature in applicazioni grandi
];
public function boot(): void
{
parent::boot();
// Qui si potevano registrare anche subscriber o listener anonimi
}
/**
* Determina se gli eventi e i listener devono essere scoperti automaticamente.
* Nelle app L9/L10 che usano $listen, questo era spesso false.
*
* @return bool
*/
public function shouldDiscoverEvents(): bool
{
return false; // Tipico se si usa l'array $listen estensivamente
}
}
Limiti di questo approccio tradizionale:
- File
EventServiceProvider
enorme: in applicazioni complesse con molti eventi e listener, questo file può diventare molto lungo e difficile da navigare, violando il principio di singola responsabilità. - Rischio di errori/omissioni: è facile dimenticare di registrare un nuovo listener o commettere un errore di battitura nel namespace dell'evento o del listener.
- Accoppiamento configurazionale: la definizione del legame tra un evento e il suo listener è fisicamente separata dalla classe del listener stesso, rendendo meno immediato capire cosa ascolta un listener senza consultare l'
EventServiceProvider
. - Meno intuitivo per nuovi sviluppatori: trovare tutti i listener per un dato evento richiede di cercare in questo file centralizzato.
Verso Laravel 12: Event Discovery e Attributi #[AsEventListener]
Laravel 11 ha snellito la struttura applicativa di default e ha abilitato l'event discovery per impostazione predefinita. Questo, combinato con l'uso degli attributi PHP 8 (in particolare #[AsEventListener]
), offre un modo molto più pulito e moderno per gestire gli eventi, pratica che si consolida in Laravel 12.
1. Event Discovery Automatico
Quando l'event discovery è abilitato (il metodo shouldDiscoverEvents()
nell'EventServiceProvider
ritorna true
, che è il default in Laravel 11+, o se il metodo non esiste proprio), Laravel scansiona automaticamente una o più directory (di default app/Listeners
) alla ricerca di classi listener. Se un listener ha un metodo handle
che fa il type-hint di una classe evento (es. public function handle(OrderPlaced $event)
), Laravel registrerà automaticamente quel listener per quell'evento.
È possibile personalizzare le directory da scansionare sovrascrivendo il metodo discoverEventsWithin()
nell'EventServiceProvider
:
// app/Providers/EventServiceProvider.php (Laravel 11/12)
// protected function discoverEventsWithin(): array
// {
// return [
// $this->app->path('Listeners'),
// $this->app->path('Domain/Orders/Listeners'), // Esempio di directory custom
// ];
// }
2. L'Attributo #[AsEventListener]
Per un controllo ancora più esplicito e per rendere la registrazione indipendente dalle convenzioni di nomenclatura del metodo (cioè, non dover per forza chiamare il metodo handle
), Laravel ha introdotto l'attributo #[AsEventListener]
(che fa uso del componente symfony/event-dispatcher-contracts
).
Questo attributo PHP 8 può essere applicato direttamente sopra la classe del listener o sopra un metodo specifico all'interno della classe.
Guida pratica al refactoring:
Passo 1: Abilitare l'Event Discovery (se non già attivo) Nel tuo app/Providers/EventServiceProvider.php
(se ancora lo usi per altre ragioni, come la registrazione di subscriber o listener anonimi), assicurati che:
// app/Providers/EventServiceProvider.php (per Laravel 11/12)
public function shouldDiscoverEvents(): bool
{
return true; // Abilita l'event discovery
}
In una nuova applicazione Laravel 11+, questo è già il comportamento di default e potresti non avere nemmeno il metodo shouldDiscoverEvents()
definito, il che va bene.
Passo 2: Refactoring dei Listener per usare #[AsEventListener]
Ora puoi modificare le tue classi listener per utilizzare l'attributo.
Esempio 1: Listener singolo per un evento (come SendWelcomeEmailToUser
)
// app/Listeners/SendWelcomeEmailToUser.php (Laravel 11/12 style)
namespace App\Listeners;
use App\Events\UserRegistered; // L'evento che questo listener gestisce
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener; // Importa l'attributo
#[AsEventListener(event: UserRegistered::class)] // La magia è qui!
class SendWelcomeEmailToUser implements ShouldQueue
{
use InteractsWithQueue;
public function __construct()
{
// Puoi ancora avere dipendenze iniettate qui
}
public function handle(UserRegistered $event): void
{
logger()->info("Listener SendWelcomeEmailToUser (via attributo): Email di benvenuto inviata a utente #{$event->user->id}");
// ... logica invio email ...
}
}
Con l'attributo #[AsEventListener(event: UserRegistered::class)]
, non è più necessario registrare questo listener nell'array $listen
dell'EventServiceProvider
. Laravel lo scoprirà e lo registrerà automaticamente.
Esempio 2: Una singola classe Listener che gestisce più eventi o metodi specifici Puoi avere una classe che funge da subscriber per più eventi, o che usa metodi diversi da handle
.
// app/Listeners/OrderEventSubscriber.php (Laravel 11/12 style)
namespace App\Listeners;
use App\Events\OrderPlaced;
use App\Events\OrderShipped;
use App\Events\OrderCancelled;
use Illuminate\Contracts\Queue\ShouldQueue; // Per l'intera classe o per metodi specifici
use Illuminate\Queue\InteractsWithQueue;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
// Se l'intera classe deve essere in coda per tutti i suoi metodi listener:
// class OrderEventSubscriber implements ShouldQueue
class OrderEventSubscriber
{
// use InteractsWithQueue; // Necessario se la classe implementa ShouldQueue
#[AsEventListener(event: OrderPlaced::class, method: 'onOrderPlaced')]
public function onOrderPlaced(OrderPlaced $event): void
{
// Questo metodo gestirà l'evento OrderPlaced
logger()->info("OrderEventSubscriber: Ordine Piazzato #{$event->order->id}");
// Se vuoi che *questo specifico ascoltatore* sia in coda, e la classe no,
// dovresti creare un listener separato che implementa ShouldQueue.
// L'attributo di per sé non rende un metodo accodabile se la classe non è ShouldQueue.
}
// Se il nome del metodo è 'handle', e l'attributo è sulla classe,
// oppure se l'attributo sul metodo non specifica 'method',
// e il metodo fa il type-hint dell'evento, funziona con l'event discovery.
// Per chiarezza con più metodi, specificare `event` e `method` è una buona pratica.
#[AsEventListener(event: OrderShipped::class, method: 'onOrderShipped', priority: 10)]
public function onOrderShipped(OrderShipped $event): void
{
logger()->info("OrderEventSubscriber: Ordine Spedito #{$event->order->id} (priorità 10)");
}
// Questo metodo può essere scoperto automaticamente se la classe è nella directory dei listener
// e se OrderCancelled è un evento, e shouldDiscoverEvents() è true.
// L'attributo lo rende esplicito.
#[AsEventListener(OrderCancelled::class)] // Il metodo 'handle' è implicito per l'evento specificato
public function handle(OrderCancelled $event): void
{
logger()->info("OrderEventSubscriber: Ordine Cancellato #{$event->order->id} (metodo handle)");
}
}
Importante sui Listener in Coda con Attributi: Se vuoi che un listener (o uno specifico metodo listener) venga eseguito in coda, la classe del listener deve implementare l'interfaccia ShouldQueue
. L'attributo #[AsEventListener]
di per sé non determina se il listener è in coda; si occupa solo della registrazione. Laravel controllerà se la classe del listener (o il job che lo wrappa, se si usa ShouldQueueReturning
), implementa ShouldQueue
.
Passo 3: Snellire o Rimuovere le Registrazioni Manuali da $listen
Una volta che i tuoi listener sono stati refactorati per usare l'attributo #[AsEventListener]
e l'event discovery è attivo, puoi (e dovresti) rimuovere le voci corrispondenti dall'array $listen
nel tuo app/Providers/EventServiceProvider.php
. Questo renderà il tuo EventServiceProvider
significativamente più pulito. Potrebbe persino diventare quasi vuoto se tutte le tue mappature evento-listener passano a questo nuovo sistema. Questa modernizzazione della gestione eventi si sposa perfettamente con la nuova struttura applicativa snella di Laravel 11/12, che mira a ridurre il boilerplate nei Service Provider.
Considerazioni Aggiuntive
- Priorità dei Listener: L'attributo
#[AsEventListener]
accetta un parametropriority
(intero, più alto = prima esecuzione) per controllare l'ordine di esecuzione dei listener per lo stesso evento.#[AsEventListener(event: MyEvent::class, priority: 100)]
- Listener Anonimi (Closures): Per logiche molto semplici e non riutilizzabili, puoi ancora registrare listener basati su closure direttamente nel metodo
boot()
del tuoEventServiceProvider
(o inbootstrap/app.php
per Laravel 11+) usandoEvent::listen(MyEvent::class, function (MyEvent $event) { ... });
. - Subscriber di Eventi: Se una classe ascolta molti eventi, puoi ancora creare un Event Subscriber (una classe con metodi
subscribe($events)
) e registrarlo manualmente. Tuttavia, l'approccio con più attributi#[AsEventListener]
su metodi diversi all'interno di una singola classe listener offre una flessibilità simile con una registrazione più automatica.
Testing
Il testing degli eventi e dei listener non cambia drasticamente, ma diventa più focalizzato.
Event::fake()
: Continua a essere lo strumento principale per testare che gli eventi vengano inviati correttamente e con i dati attesi, senza eseguire i listener reali.// tests/Feature/OrderCreationTest.php use App\Events\OrderPlaced; use App\Models\Order; use Illuminate\Support\Facades\Event; // ... public function test_order_placed_event_is_dispatched_on_order_creation(): void { Event::fake(); // Previene l'esecuzione dei listener // Logica che crea un ordine e dovrebbe inviare l'evento OrderPlaced $order = $this->createOrder(); // Metodo helper Event::assertDispatched(OrderPlaced::class, function ($event) use ($order) { return $event->order->id === $order->id; }); Event::assertDispatchedTimes(OrderPlaced::class, 1); }
Test Unitari dei Listener: Poiché i listener sono ora classi PHP più standard (specialmente se la loro registrazione è gestita da attributi), puoi testarli unitariamente in modo più semplice. Istanzia il listener, crea un'istanza fittizia dell'evento con i dati necessari, e chiama il metodo
handle
(o il metodo specificato nell'attributo), asserendo sui risultati o sugli effetti collaterali (es. mocking delMail
facade).// tests/Unit/Listeners/SendOrderConfirmationEmailTest.php namespace Tests\Unit\Listeners; use App\Events\OrderPlaced; use App\Listeners\SendOrderConfirmationEmail; use App\Models\Order; use App\Models\User; // Assumendo che Order abbia una relazione con User use Illuminate\Support\Facades\Mail; use App\Mail\OrderConfirmationMailable; // Ipotetico Mailable use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class SendOrderConfirmationEmailTest extends TestCase { use RefreshDatabase; public function test_handle_sends_order_confirmation_email(): void { Mail::fake(); // Fai il fake del Mail facade $user = User::factory()->create(['email' => '[email protected]']); $order = Order::factory()->for($user)->create(); // Crea un ordine fittizio $event = new OrderPlaced($order); $listener = new SendOrderConfirmationEmail(); $listener->handle($event); // Esegui il listener // Verifica che il Mailable corretto sia stato inviato all'utente corretto Mail::assertSent(OrderConfirmationMailable::class, function ($mail) use ($user, $order) { return $mail->hasTo($user->email) && $mail->order->id === $order->id; }); } }
Benefici del refactoring per le applicazioni della tua impresa
Adottare l'event discovery e gli attributi #[AsEventListener]
per la gestione eventi nelle tue applicazioni Laravel (specialmente evolvendo da L9/L10 a L12) offre vantaggi significativi:
- Codice più Pulito e Organizzato: La logica di registrazione è co-locata con il listener stesso, migliorando la coesione e rendendo più facile capire quali eventi gestisce una classe.
- Riduzione del Rischio di Errori: Meno configurazione manuale significa minori possibilità di errori di battitura o di dimenticare di registrare un listener.
- Migliore Developer Experience (DX): Aggiungere, modificare o rimuovere listener diventa un'operazione più semplice e intuitiva, senza dover navigare e modificare un (potenzialmente enorme)
EventServiceProvider
. EventServiceProvider
Snello: Questo file può concentrarsi su registrazioni più complesse o listener anonimi, se ancora necessari, diventando più leggero e manutenibile.- Allineamento con le Pratiche Moderne di Laravel e PHP: Sfrutta appieno le funzionalità del framework e del linguaggio.
Il ruolo del programmatore Laravel esperto
Intraprendere un refactoring del sistema di eventi, specialmente in un'applicazione aziendale di una certa dimensione, può beneficiare dell'occhio esperto di un senior laravel developer. Posso aiutare la tua impresa a:
- Identificare i listener e le registrazioni che possono essere modernizzate.
- Implementare l'approccio con attributi in modo efficiente e corretto.
- Ristrutturare i listener per massimizzare la testabilità unitaria.
- Assicurare che la transizione avvenga senza regressioni, attraverso test adeguati.
La mia esperienza ventennale mi permette di guidare la tua attività attraverso queste evoluzioni tecniche, assicurando che il tuo codice non solo funzioni, ma sia anche un piacere da mantenere. Per saperne di più sul mio approccio, visita la pagina Chi Sono.
Modernizzare la gestione degli eventi in Laravel 12 è un investimento nella chiarezza, nella robustezza e nella futura evoluzione della tua applicazione. È un passo verso un codice più elegante che supporta meglio le esigenze del tuo business.
Se sei pronto a snellire il tuo EventServiceProvider
e a rendere la gestione degli eventi nella tua applicazione Laravel più intuitiva e moderna, contattami per discutere di come possiamo implementare queste best practice.
Ultima modifica: Mercoledì 26 Febbraio 2025, alle 10:37