【Laravel】イベント(Event)とリスナー(Listener)の使い方|疎結合な設計パターン

Laravel アプリケーションが複雑になるにつれ、「ある処理が終わったら別の処理を動かす」という連鎖をコントローラや Service クラス内に直接書いてしまいがちです。Event / Listener の仕組みを使うと、この依存関係を逆転させ、呼び出し側と処理側を切り離せます。本記事では実務でよく使うパターンを中心に整理します。

なお、挙動の細部はバージョンによって異なる場合があります。コード例は執筆時点で Laravel 10 を主な参照先とし、Laravel 11 以降での変更点にはその都度補足します。手元の composer.jsonlaravel/framework に合わせ、公式ドキュメントの Events 章を並行して参照することをおすすめします。

はじめに

「ユーザー登録が完了したら、ウェルカムメールを送信し、さらに分析イベントを記録する」という要件を追加する場面を考えます。コントローラで $mailer->send(...)$analytics->record(...) を直接呼ぶと問題が起きます。コントローラがメール送信・分析記録の両方に依存するため、テストや変更の際に双方を意識しなければなりません。

Laravel の Event / Listener は、「何かが起きた(Event)」という事実を発火し、「それに反応する処理(Listener)」を別のクラスに分離する仕組みです。発火元はリスナーの存在を知らなくてよいため、クラス間の依存が薄くなります。

この記事では次の範囲を扱います。

  • Event・Listener の作成と登録
  • Event のディスパッチ
  • Queueable Listener によるバックグラウンド実行
  • よくある失敗とその対処

背景:Event と Listener の関係

Event は「何が起きたか」を表すデータクラスです。Listener は「その Event に反応して何をするか」を定義するクラスです。

役割クラス例主なメソッド
EventUserRegisteredなし(コンストラクタでデータを保持)
ListenerSendWelcomeEmailhandle(UserRegistered $event)

1 つの Event に複数の Listener を登録でき、それぞれが独立して動作します。発火元のコードは event(new UserRegistered($user)) と書くだけで、どの Listener が動くかを意識しません。

実装と検証

Event と Listener の生成

Artisan コマンドでクラスを生成します。

php artisan make:event UserRegistered
php artisan make:listener SendWelcomeEmail --event=UserRegistered

生成された Event は、コンストラクタでデータを受け取る形になっています。

<?php

namespace App\\Events;

use App\\Models\\User;
use Illuminate\\Foundation\\Events\\Dispatchable;
use Illuminate\\Queue\\SerializesModels;

class UserRegistered
{
    use Dispatchable, SerializesModels;

    public function __construct(
        public readonly User $user,
    ) {}
}

Dispatchable トレイトを使うと UserRegistered::dispatch($user) のような静的メソッドで Event を発火できます。SerializesModels トレイトは Eloquent モデルをキューに乗せる際のシリアライズ・デシリアライズを担当します。Queueable Listener と組み合わせる場合は必要です。

Listener の実装

Listener の handle メソッドが実際の処理を担います。引数の型宣言で受け取りたい Event クラスを指定します。

<?php

namespace App\\Listeners;

use App\\Events\\UserRegistered;
use App\\Mail\\WelcomeMail;
use Illuminate\\Support\\Facades\\Mail;

class SendWelcomeEmail
{
    public function handle(UserRegistered $event): void
    {
        Mail::to($event->user)->send(new WelcomeMail($event->user));
    }
}

Event と Listener の登録(Laravel 10 以前)

Laravel 10 以前では app/Providers/EventServiceProvider.php$listen プロパティに対応を記述します。

protected $listen = [
    UserRegistered::class => [
        SendWelcomeEmail::class,
        RecordAnalyticsEvent::class,
    ],
];

php artisan event:list で登録済みの Event と Listener の一覧を確認できます。

Event と Listener の登録(Laravel 11 以降)

Laravel 11 のデフォルト構成では EventServiceProvider がアプリケーションスケルトンから削除されました。代わりに app/Providers/AppServiceProvider.phpboot メソッドで登録するか、自動検出(Event Discovery)を利用します。

AppServiceProvider 内で登録する場合は次のように書きます。自動検出を使う場合は EventServiceProvider を新しく作成して shouldDiscoverEvents() を有効にする方法もあります。

use Illuminate\\Support\\Facades\\Event;

public function boot(): void
{
    Event::listen(
        UserRegistered::class,
        SendWelcomeEmail::class,
    );
}

バージョンごとの登録方法については 公式ドキュメントの Registering Events and Listeners 節 を参照してください。

Event のディスパッチ

コントローラやサービスから event ヘルパーまたは静的メソッドで発火します。

use App\\Events\\UserRegistered;

// ヘルパー関数を使う
event(new UserRegistered($user));

// Dispatchable トレイトが使えれば静的メソッドでも書ける
UserRegistered::dispatch($user);

Queueable Listener

処理に時間がかかる Listener はキューで非同期実行できます。ShouldQueue インターフェースを実装するだけで、ワーカーが処理を引き受けます。

<?php

namespace App\\Listeners;

use App\\Events\\UserRegistered;
use Illuminate\\Contracts\\Queue\\ShouldQueue;

class SendWelcomeEmail implements ShouldQueue
{
    public function handle(UserRegistered $event): void
    {
        // ワーカーが非同期で実行する
    }
}

ShouldQueue を実装した Listener は、キューに Job として載ります。キューの設定(ドライバ・接続)や queue:work の起動は Queues の章 に従ってください。

実装時の注意点

Event に大量のデータを持たせない

Event がキューに載る際はシリアライズされます。大きなコレクションやバイナリを直接持たせるとペイロードが肥大化します。識別子のみを渡してリスナー内で再取得する設計を検討してください。

Listener の失敗とリトライ

ShouldQueue を実装した Listener は Job と同様にリトライ設定($tries$backoff)を持てます。failed メソッドで失敗時の処理を書くこともできます。

同期実行と非同期実行の切り替え

ShouldQueue を実装していない Listener は同期実行されます。テストでキューの動作を検証したい場合は Queue::fake()Event::assertDispatched() を組み合わせます。

失敗例:EventServiceProvider への登録を忘れて Event が無反応になる

make:eventmake:listener でクラスを生成しただけでは、Event と Listener はまだ結びついていません。EventServiceProvider$listen 配列(または Laravel 11 以降の同等の手順)への追加を省略すると、リスナーは一切動きません。event(new UserRegistered($user)) を呼んでも無反応になります。

この状態はエラーにならず無音で失敗するため、気づきにくいです。php artisan event:list で目的のリスナーが表示されるか確認する習慣をつけると、この問題を早期に検出できます。

学び:疎結合がもたらす設計上のメリット

Event / Listener を導入すると、次の点で設計が整理しやすくなります。

呼び出し側の責務を絞れる

「ユーザー登録が完了した」という事実だけを発火し、メール送信・ログ記録・外部サービス連携を別の Listener に委ねられます。発火元のクラスが肥大化しにくくなります。

Listener の追加・削除が発火元に影響しない

新しい処理を追加したいときは Listener を作って登録するだけです。発火元のコードを変更する必要がありません。

テストしやすくなる

Event::fake() を使うとディスパッチの検証ができ、実際の Listener を動かさずに発火の有無を確認できます。

// テスト例
Event::fake();

// 登録処理を実行
$this->post('/register', [...]);

// Event が発火されたかを検証
Event::assertDispatched(UserRegistered::class, function ($event) use ($user) {
    return $event->user->id === $user->id;
});

まとめ

  • Event はデータを保持するクラス、Listener は処理を担うクラスである。
  • EventServiceProvider$listen 配列(または Laravel 11 以降の登録方法)で Event と Listener を結びつける。
  • ShouldQueue を実装すると Listener をキューで非同期実行できる。
  • 登録忘れはエラーなしで無反応になるため、php artisan event:list で確認する。

次に試せること

  1. UserRegistered のような具体的なドメインイベントを 1 つ作り、Listener を 2 つ以上登録して動作を確認する。
  2. ShouldQueue を実装した Listener で queue:work との連携を試す。
  3. Laravel 公式ドキュメントの Event Subscribers 節 を読み、複数の Event を 1 つのクラスでまとめて扱う Event Subscriber パターンを把握する。

Source

LaravelLaravel

Posted by 千原 耕司