vendor to auto-discovery, you reduce the assumptions involved in installation, updates, and cache generation.
This page examines the implementation in Laravel Framework v13.36.0 and applies the basics of Events to long-term package maintenance. For loading providers, see Package discovery; for the test environment, see Package testing.
Register package listeners explicitly
Work that your package performs by default can be registered withEvent::listen() in the boot() method of a regular Illuminate\Support\ServiceProvider. Laravel’s default listener auto-discovery location is the application’s app/Listeners; it does not automatically scan src/Listeners inside your package.
As an example, let’s create a package that logs a shipment-delivered event. The event is simply a class that holds the values needed for notification.
handle().
extra.laravel.providers or explicit registration by the user. If the event is needed both in HTTP handling and in queue workers, do not restrict registration with runningInConsole().
Registration and instantiation happen at different times
Dispatcher::listen() adds listeners for regular event names to an internal array. When you pass a class name as shown above, the listener is not instantiated at this point.
For synchronous listeners, after the event is dispatched, createClassCallable() resolves the class using the container’s make(). This lets you use constructor dependency injection.
This path does not invoke the container’s call() on handle(). Design your listeners so that arbitrary services are injected into the constructor and handle() receives the event. Do not assume it works the same way as dependency injection into an Artisan command’s handle().
What is described here applies to synchronous listeners. Implementing ShouldQueue takes a separate queue path, so also review queued event listeners and Queues regarding transactions, retries, and dependency resolution.
event:cache does not stop explicit registration
event:cache is not a command that serializes every listener registered with the event dispatcher.
EventCacheCommand::getEvents() enumerates Illuminate\Foundation\Support\Providers\EventServiceProvider and its subclass providers, and stores each provider’s auto-discovery results and listens() mapping. Event::listen() calls written in a regular ServiceProvider::boot() are not included in this collection.
On the other hand, the boot() of regular providers still runs on boots where a cache exists. Not being included in the cache is different from not being registered when a cache exists.
Packages can also extend Foundation’s
EventServiceProvider and use $listen. However, in the implementation examined, the default value of shouldDiscoverEvents() is a check against that base class itself. Simply extending it does not cause your package’s listener directory to be auto-discovered.
In addition, that provider’s $subscribe and $observers are processed separately from the loop that registers the results of getEvents(). Do not conclude from the $listen cache alone that these registrations are skipped as well.
Avoid duplicate registration with auto-discovery
If a user adds your package’s listener directory withwithEvents(discover: [...]) while your package’s provider also explicitly registers the same listener, the work may run multiple times for a single event.
For regular events, Dispatcher::listen() appends to an array, and the dispatcher as a whole does not automatically deduplicate identical class names. The array_unique() that Foundation’s EventServiceProvider performs within its own mapping does not merge explicit registrations made by other providers either.
Assign responsibility for registration to one side.
- If the package registers default listeners, do not require users to add that directory to auto-discovery.
- If users choose which listeners they need, the package provides event and listener classes and does not register the same listeners by default.
- When migrating from auto-discovery to explicit registration, also instruct users to update their discovery settings and stale event caches.
Verify behavior before and after updates
To inspect registration state, runevent:list in a verification application with the package installed. Confirm that the target listener is present both without and with the cache.
event:list output alone cannot guarantee that a listener completes its work or runs only once. Also add tests that actually dispatch events.
As an example, register CourierServiceProvider in the Testbench base TestCase, then verify that the logger is called exactly once.
Event::fake() is convenient for asserting that events were dispatched, but it normally prevents real listeners from running. Because this test verifies registration and handling, it does not use it. Also verify combinations with auto-discovery and boots with a cache in a separate verification app and a fresh application instance.
Long-term maintenance checklist
- Document event class names, public properties, and your listener registration policy as a contract for users.
- When changing the names or payloads of events that users subscribe to, communicate the change following Version compatibility management.
- For updates that change auto-discovery or
$listen, include regeneratingevent:cachein the deployment procedure. - Distinguish the package’s default listeners from users’ own listeners, and do not have the same default listener registered twice.
- For each supported PHP and Laravel version, maintain tests that verify listeners run exactly once.