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

# パッケージのイベントリスナー登録とキャッシュ

> Laravel 13のイベント登録とevent:cacheの実装を読み解き、パッケージの明示登録、自動検出との二重登録、依存解決、更新時の検証方法を解説します。

パッケージにイベント駆動の拡張点を用意したいときは、**イベントクラスの提供**と**リスナーの登録**を分けて設計します。利用者が `vendor` 内のディレクトリを自動検出に追加しなくても動く構成にすると、インストール・更新・キャッシュ作成時の前提を減らせます。

このページではLaravel Framework `v13.36.0` の実装を確認し、[イベント](/jp/events)の基本操作をパッケージの長期保守に適用します。プロバイダーの読み込みは[パッケージの自動検出](/jp/advanced/package-discovery)、テスト環境は[パッケージのテスト](/jp/advanced/package-testing)を参照してください。

## パッケージのリスナーを明示登録する

パッケージが標準で実行する処理は、通常の `Illuminate\Support\ServiceProvider` の `boot()` で `Event::listen()` に登録できます。Laravelの既定のリスナー自動検出先はアプリケーションの `app/Listeners` であり、パッケージ内の `src/Listeners` まで自動で走査するわけではありません。

例として、配送完了イベントをログに記録するパッケージを作ります。イベントは通知に必要な値を持つだけのクラスです。

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

namespace Acme\Courier\Events;

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

リスナーは、コンストラクターでロガーを受け取り、`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,
        ]);
    }
}
```

プロバイダーにはイベントとリスナーのクラス名を登録します。

```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,
        );
    }
}
```

この構成はプロバイダー自身が読み込まれることを前提にします。Composerの `extra.laravel.providers` または利用者による明示登録を用意してください。イベントがHTTP処理でもキューワーカーでも必要なら、`runningInConsole()` で登録を制限しません。

<Warning>
  リスナー登録だけを理由に、このプロバイダーへ `DeferrableProvider` を実装しないでください。遅延対象のサービスが解決されなければ、リスナーも登録されません。重いサービスのバインディングは、必要に応じて別の[遅延プロバイダー](/jp/advanced/deferred-provider)へ分離します。
</Warning>

## 登録とインスタンス生成は別のタイミング

`Dispatcher::listen()` は、通常のイベント名に対するリスナーを内部配列へ追加します。上のようにクラス名を渡す場合、ここではリスナーのインスタンスを生成しません。

同期リスナーでは、イベント発火後に `createClassCallable()` がコンテナの `make()` を使ってクラスを解決します。そのため、コンストラクターの依存注入を利用できます。

```mermaid theme={null}
flowchart TD
    A["プロバイダーのboot()"] --> B["Event::listen()でクラス名を保存"]
    B --> C["ShipmentDeliveredを発火"]
    C --> D["Dispatcherがリスナーを選択"]
    D --> E["コンテナで同期リスナーを解決"]
    E --> F["handle()へイベントを渡す"]
```

この経路は `handle()` に対してコンテナの `call()` を行うものではありません。**任意のサービスはコンストラクターに注入し、`handle()` ではイベントを受け取る**設計にします。[Artisanコマンドの `handle()` への依存注入](/jp/advanced/package-commands)と同じ仕組みだと考えないでください。

ここで説明しているのは同期リスナーです。`ShouldQueue` を実装する場合はキュー用の別経路になるため、トランザクション、再試行、依存の解決について[イベントのキュー処理](/jp/events)と[キュー](/jp/queues)も確認します。

## event:cacheは明示登録を止めない

`event:cache` は、イベントDispatcherに登録されたすべてのリスナーを丸ごとシリアライズするコマンドではありません。

`EventCacheCommand::getEvents()` は、`Illuminate\Foundation\Support\Providers\EventServiceProvider` とその派生プロバイダーを列挙し、各プロバイダーの自動検出結果と `listens()` のマッピングを保存します。通常の `ServiceProvider::boot()` に書いた `Event::listen()` は、この収集対象には入りません。

一方、キャッシュがある起動でも通常のプロバイダーの `boot()` は実行されます。**キャッシュに含まれないことと、キャッシュがあると登録されないことは別です。**

| 登録方法 | `event:cache` に保存される内容 | キャッシュがある起動時 |
| - | - | - |
| 通常のプロバイダーの `boot()` で `Event::listen()` | この登録自体は収集対象外 | `boot()` で再び登録する |
| Foundationの `EventServiceProvider` の `$listen` | プロバイダーのクラス名ごとにマッピングを保存 | `getEvents()` がキャッシュを読む |
| 自動検出の対象になったリスナー | 検出結果をマッピングへ保存 | ディレクトリの再走査ではなくキャッシュを使う |

Foundationの `EventServiceProvider` を継承したパッケージで `$listen` を使う方法もあります。ただし、確認した実装では `shouldDiscoverEvents()` の既定値はその基底クラス自身に対する判定です。**継承するだけでは、そのパッケージのリスナーディレクトリが自動検出されるわけではありません。**

また、同プロバイダーの `$subscribe` と `$observers` は、`getEvents()` の結果を登録するループとは別に処理されます。`$listen` のキャッシュだけを見て、これらの登録まで省略されると判断しないでください。

<Tip>
  `Event::subscribe(SomeSubscriber::class)` は、登録時にコンテナでサブスクライバーを生成して `subscribe()` を呼びます。クラス名による `Event::listen()` と同じ遅延性ではありません。サブスクライバーのコンストラクターや `subscribe()` では外部通信などの副作用を避けます。
</Tip>

## 自動検出との二重登録を避ける

利用者が `withEvents(discover: [...])` でパッケージのリスナーディレクトリを追加し、パッケージのプロバイダーも同じリスナーを明示登録すると、1つのイベントで処理が複数回動く可能性があります。

`Dispatcher::listen()` は通常のイベントに対して配列への追加を行い、Dispatcher全体で同じクラス名を自動的に重複排除しません。Foundationの `EventServiceProvider` が自身のマッピング内で行う `array_unique()` も、別のプロバイダーによる明示登録までまとめる仕組みではありません。

登録の責任を一方に決めます。

* **パッケージが標準リスナーを登録する**場合、利用者にそのディレクトリの自動検出追加を要求しません。
* **利用者が必要なリスナーを選ぶ**場合、パッケージはイベントとリスナークラスを提供し、同じリスナーを標準で登録しません。
* 自動検出から明示登録へ移行する場合、利用者側の検出設定や古いイベントキャッシュも更新対象として案内します。

業務処理の冪等性も重要ですが、二重登録を残したまま冪等性だけで隠すのではなく、登録経路を整理してください。

## 更新前後の動作を検証する

登録状態を調べるには、パッケージを導入した検証用アプリケーションで `event:list` を実行します。キャッシュなし・ありの両方で対象のリスナーが存在することを確認します。

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

これは検証用アプリでの手順です。`event:list` の表示だけでは、リスナーが処理を完了することや1回だけ実行されることまでは保証できません。実際のイベント発火を使うテストも追加します。

例として、[TestbenchのベースTestCase](/jp/advanced/package-testing)で `CourierServiceProvider` を登録したうえで、ロガーが1回だけ呼ばれることを検証します。

```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()` はイベントの発火確認には便利ですが、通常は本物のリスナーの実行を止めます。このテストでは登録と処理を検証するため使用しません。自動検出との組み合わせやキャッシュありの起動も、別の検証用アプリ・新しいアプリケーションインスタンスで確認します。

<Warning>
  起動後に `event:cache` を実行しても、そのプロセス内に登録済みのDispatcherがキャッシュから再構築されるわけではありません。キャッシュありの動作確認は、新しい起動で行ってください。更新したリスナーを長時間動くワーカーへ反映するには、通常のデプロイ手順に沿った再起動も必要です。
</Warning>

## 長期保守のチェックリスト

* イベントクラス名、公開プロパティ、リスナーの登録方針を利用者向けの契約として文書化する。
* 利用者が購読するイベントの名前やペイロードを変えるときは、[バージョン互換性管理](/jp/advanced/package-versioning)に沿って変更を案内する。
* 自動検出や `$listen` に変更がある更新では、`event:cache` の再作成をデプロイ手順へ含める。
* パッケージの標準リスナーと利用者の独自リスナーを区別し、同じ標準リスナーを二重に登録させない。
* PHP・Laravelの対応バージョンごとに、リスナーが1回だけ実行されるテストを継続する。

## 参照した一次情報

* [Laravel公式ドキュメント: イベント登録・自動検出・キャッシュ・サブスクライバー](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: キャッシュ対象の収集](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Foundation/Console/EventCacheCommand.php)


## Related topics

- [応用トピック](/jp/advanced/index.md)
- [パッケージのルート登録とキャッシュ](/jp/advanced/package-routes.md)
- [イベントとリスナー](/jp/events.md)
- [Laravelパッケージ開発](/jp/advanced/package-development.md)
- [パッケージのキャッシュとoptimizeへの統合](/jp/advanced/package-optimization.md)


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