> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Registering and caching package event listeners

> Examine the event registration and event:cache implementation in Laravel 13, and learn about explicit registration in packages, duplicate registration with auto-discovery, dependency resolution, and how to verify behavior during updates.

When you want to provide event-driven extension points in your package, design **providing event classes** and **registering listeners** as separate concerns. If your setup works without requiring users to add directories inside `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](/en/events) to long-term package maintenance. For loading providers, see [Package discovery](/en/advanced/package-discovery); for the test environment, see [Package testing](/en/advanced/package-testing).

## Register package listeners explicitly

Work that your package performs by default can be registered with `Event::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.

```php theme={null}
<?php

namespace Acme\Courier\Events;

final class ShipmentDelivered
{
    public function __construct(public readonly string $shipmentId)
    {
    }
}
```

The listener receives a logger in its constructor and handles the event in `handle()`.

```php theme={null}
<?php

namespace Acme\Courier\Listeners;

use Acme\Courier\Events\ShipmentDelivered;
use Psr\Log\LoggerInterface;

final class LogDeliveredShipment
{
    public function __construct(private readonly LoggerInterface $logger)
    {
    }

    public function handle(ShipmentDelivered $event): void
    {
        $this->logger->info('Shipment delivered', [
            'shipment_id' => $event->shipmentId,
        ]);
    }
}
```

In the provider, register the class names of the event and the listener.

```php theme={null}
<?php

namespace Acme\Courier;

use Acme\Courier\Events\ShipmentDelivered;
use Acme\Courier\Listeners\LogDeliveredShipment;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Event::listen(
            ShipmentDelivered::class,
            LogDeliveredShipment::class,
        );
    }
}
```

This setup assumes that the provider itself is loaded. Provide either Composer's `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()`.

<Warning>
  Do not implement `DeferrableProvider` on this provider solely because it registers listeners. If the deferred services are never resolved, the listeners are never registered either. If needed, move heavy service bindings into a separate [deferred provider](/en/advanced/deferred-provider).
</Warning>

## 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.

```mermaid theme={null}
flowchart TD
    A["Provider boot()"] --> B["Event::listen() stores the class name"]
    B --> C["ShipmentDelivered is dispatched"]
    C --> D["Dispatcher selects the listeners"]
    D --> E["Container resolves the synchronous listener"]
    E --> F["The event is passed to handle()"]
```

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()`](/en/advanced/package-commands).

What is described here applies to synchronous listeners. Implementing `ShouldQueue` takes a separate queue path, so also review [queued event listeners](/en/events) and [Queues](/en/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.**

| Registration method | What `event:cache` stores | On boot with a cache |
| - | - | - |
| `Event::listen()` in a regular provider's `boot()` | This registration itself is not collected | Registered again in `boot()` |
| `$listen` on Foundation's `EventServiceProvider` | Mapping stored per provider class name | `getEvents()` reads the cache |
| Listeners targeted by auto-discovery | Discovery results stored in the mapping | Uses the cache instead of rescanning directories |

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.

<Tip>
  `Event::subscribe(SomeSubscriber::class)` instantiates the subscriber through the container at registration time and calls `subscribe()`. It is not lazy in the same way as `Event::listen()` with a class name. Avoid side effects such as external communication in a subscriber's constructor or `subscribe()`.
</Tip>

## Avoid duplicate registration with auto-discovery

If a user adds your package's listener directory with `withEvents(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.

Idempotency in business logic matters too, but rather than masking duplicate registration with idempotency alone, clean up the registration paths.

## Verify behavior before and after updates

To inspect registration state, run `event:list` in a verification application with the package installed. Confirm that the target listener is present both without and with the cache.

```bash theme={null}
php artisan event:clear
php artisan event:list
php artisan event:cache
php artisan event:list
```

These steps are for a verification app. The `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](/en/advanced/package-testing), then verify that the logger is called exactly once.

```php theme={null}
<?php

namespace Acme\Courier\Tests\Feature;

use Acme\Courier\Events\ShipmentDelivered;
use Acme\Courier\Tests\TestCase;
use Illuminate\Support\Facades\Event;
use Psr\Log\LoggerInterface;

class PackageEventsTest extends TestCase
{
    public function test_delivery_is_logged_once(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $logger->expects($this->once())
            ->method('info')
            ->with('Shipment delivered', ['shipment_id' => 'shipment-123']);

        $this->app->instance(LoggerInterface::class, $logger);

        Event::dispatch(new ShipmentDelivered('shipment-123'));
    }
}
```

`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.

<Warning>
  Running `event:cache` after boot does not rebuild the dispatcher already registered in that process from the cache. Verify cached behavior on a fresh boot. To apply updated listeners to long-running workers, you also need to restart them following your normal deployment procedure.
</Warning>

## 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](/en/advanced/package-versioning).
* For updates that change auto-discovery or `$listen`, include regenerating `event:cache` in 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.

## Primary sources consulted

* [Official Laravel documentation: event registration, auto-discovery, caching, and subscribers](https://github.com/laravel/docs/blob/73e61270287e4aaa1cecd77cd66aa6ef4f499828/events.md)
* [Dispatcher: listen, subscribe, createClassCallable](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Events/Dispatcher.php)
* [Foundation EventServiceProvider: register, getEvents, shouldDiscoverEvents](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php)
* [EventCacheCommand: collecting cache targets](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Foundation/Console/EventCacheCommand.php)


## Related topics

- [Advanced Topics](/en/advanced/index.md)
- [Package route registration and caching](/en/advanced/package-routes.md)
- [Laravel Package Development](/en/advanced/package-development.md)
- [Deferred Service Providers](/en/advanced/deferred-provider.md)
- [Events & Listeners](/en/events.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.