Event Listener Auto-Discovery

+15 Mana ✨

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/Listeners at boot and binds each listener to the event type in its handle() signature.
  • Explicit registration: Still supported via Event::listen() in a service provider's boot() method — needed for closures and third-party classes outside app/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:

php
use 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:

php
public 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

  1. Forgetting to type-hint the event — without a typed $event parameter, the scanner has nothing to bind to and the listener silently never fires.
  2. Mixing auto-discovery with the legacy $listen array — if you still have a stale array mapping, you'll get duplicate invocations. Delete the array when you migrate.

Best Practices

  1. One listener, one event — keeps filenames descriptive and the auto-discovery mapping unambiguous.
  2. 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/Listeners are 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().
✓ Completed