Introduction
In older Laravel versions, every listener had to be registered explicitly in an EventServiceProvider::$listen array. Laravel 11 removed that provider in favor of auto-discovery, and Laravel 13 refined it further: just put a class in app/Listeners, type-hint the event in handle(), and Laravel wires it up.
Key Concepts
- Auto-discovery: The framework scans
app/Listenersat boot and binds each listener to the event type in itshandle()signature. - Explicit registration: Still supported via
Event::listen()in a service provider'sboot()method — needed for closures and third-party classes outsideapp/Listeners.
Real World Context
A feature flag listener that responds to UserSignedUp used to require three files: the event, the listener, and a line in EventServiceProvider. With auto-discovery, it's two files — one less place for registration to drift from reality.
Deep Dive
The auto-discovered form
php// app/Listeners/GrantTrialCredits.php namespace App\Listeners; use App\Events\UserSignedUp; class GrantTrialCredits { public function handle(UserSignedUp $event): void { $event->user->credits()->create(['amount' => 500]); } }
Nothing else. Boot the app and UserSignedUp::dispatch($user) now triggers GrantTrialCredits::handle().
Multiple events on one listener class
Laravel 13 discovers every method on a listener class whose name starts with handle (or is __invoke) and type-hints an event. So one class can respond to several events — just give each handler a distinct handle* name:
phpuse App\Events\{OrderPlaced, OrderRefunded}; class OrderMetrics { public function handleOrderPlaced(OrderPlaced $event): void { Metrics::increment('orders.placed', amount: $event->order->total); } public function handleOrderRefunded(OrderRefunded $event): void { Metrics::increment('orders.refunded', amount: $event->order->total); } }
No attribute, no service provider registration — the type hints on each method are the mapping. You can also use union types on a single handle() to react to multiple events with shared logic:
phppublic function handle(OrderPlaced|OrderRefunded $event): void { Metrics::increment('orders.activity'); }
When to register explicitly
Auto-discovery does not cover:
- Closure listeners — use
Event::listen(OrderPlaced::class, fn ($e) => ...). - Listeners outside
app/Listeners. - Wildcard listeners — use
Event::listen('order.*', ...).
Keep those in AppServiceProvider::boot().
Common Pitfalls
- Forgetting to type-hint the event — without a typed
$eventparameter, the scanner has nothing to bind to and the listener silently never fires. - Mixing auto-discovery with the legacy
$listenarray — if you still have a stale array mapping, you'll get duplicate invocations. Delete the array when you migrate.
Best Practices
- One listener, one event — keeps filenames descriptive and the auto-discovery mapping unambiguous.
- Use a single class with multiple
handle*methods only when the events share state or helpers — otherwise prefer one file per event-listener pair.
Summary
- Listeners in
app/Listenersare auto-discovered via method type-hints. - Methods starting with
handle(or__invoke) that type-hint an event are registered automatically. - Union types let one method react to multiple events.
- Closures, wildcards, and external classes still need explicit
Event::listen().