#はじめに
Laravelのイベントはシンプルなオブザーバーパターンを提供し、アプリケーション内で発生するさまざまなイベントに対して購読・リスニングできます。イベントクラスは通常 app/Events ディレクトリに保存され、リスナーは app/Listeners に保存されます。これらのディレクトリがまだ存在しなくても心配いりません。Artisanのコンソールコマンドでイベントやリスナーを生成すると自動的に作成されます。
イベントはアプリケーションのさまざまな部分を疎結合にするのに最適です。単一のイベントに複数のリスナーを設定でき、それらは互いに依存しません。例えば、注文が発送されるたびにユーザーにSlack通知を送りたい場合、注文処理コードとSlack通知コードを結びつける代わりに、App\Events\OrderShipped イベントを発生させ、そのイベントを受け取ったリスナーがSlack通知を送信できます。
#イベントとリスナーの登録
Laravelアプリケーションに含まれる App\Providers\EventServiceProvider は、アプリケーションのすべてのイベントリスナーを登録する便利な場所を提供します。listen プロパティは、すべてのイベント(キー)とそれに対応するリスナー(値)の配列を含みます。必要に応じてこの配列にイベントを追加できます。例えば、OrderShipped イベントを追加してみましょう:
use App\Events\OrderShipped;
use App\Listeners\SendShipmentNotification;
/**
* アプリケーションのイベントリスナーのマッピング。
*
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
OrderShipped::class => [
SendShipmentNotification::class,
],
];
event:list コマンドを使うと、アプリケーションに登録されているすべてのイベントとリスナーの一覧を表示できます。
#イベントとリスナーの生成
もちろん、イベントやリスナーのファイルを手動で作成するのは面倒です。代わりに、EventServiceProvider にリスナーとイベントを追加し、event:generate Artisanコマンドを使いましょう。このコマンドは、EventServiceProvider にリストされているがまだ存在しないイベントやリスナーを生成します:
php artisan event:generate
または、make:event と make:listener Artisanコマンドを使って個別にイベントやリスナーを生成することもできます:
php artisan make:event PodcastProcessed
php artisan make:listener SendPodcastNotification --event=PodcastProcessed
#イベントの手動登録
通常、イベントは EventServiceProvider の $listen 配列で登録しますが、EventServiceProvider の boot メソッド内でクラスまたはクロージャベースのイベントリスナーを手動で登録することも可能です:
use App\Events\PodcastProcessed;
use App\Listeners\SendPodcastNotification;
use Illuminate\Support\Facades\Event;
/**
* その他のイベントを登録します。
*/
public function boot(): void
{
Event::listen(
PodcastProcessed::class,
SendPodcastNotification::class,
);
Event::listen(function (PodcastProcessed $event) {
// ...
});
}
#キューイング可能な匿名イベントリスナー
クロージャベースのイベントリスナーを手動で登録する場合、Illuminate\Events\queueable 関数でリスナーのクロージャをラップすると、Laravelがリスナーをキューで実行するよう指示できます:
use App\Events\PodcastProcessed;
use function Illuminate\Events\queueable;
use Illuminate\Support\Facades\Event;
/**
* その他のイベントを登録します。
*/
public function boot(): void
{
Event::listen(queueable(function (PodcastProcessed $event) {
// ...
}));
}
キューイングされたジョブと同様に、onConnection、onQueue、delay メソッドを使ってキューイングされたリスナーの実行をカスタマイズできます:
Event::listen(queueable(function (PodcastProcessed $event) {
// ...
})->onConnection('redis')->onQueue('podcasts')->delay(now()->addSeconds(10)));
匿名のキューイングされたリスナーの失敗を処理したい場合、queueable リスナーを定義するときに catch メソッドにクロージャを渡せます。このクロージャはイベントインスタンスとリスナーの失敗原因となった Throwable インスタンスを受け取ります:
use App\Events\PodcastProcessed;
use function Illuminate\Events\queueable;
use Illuminate\Support\Facades\Event;
use Throwable;
Event::listen(queueable(function (PodcastProcessed $event) {
// ...
})->catch(function (PodcastProcessed $event, Throwable $e) {
// キューイングされたリスナーが失敗しました...
}));
#ワイルドカードイベントリスナー
* をワイルドカードパラメータとして使い、複数のイベントを同じリスナーでキャッチすることもできます。ワイルドカードリスナーは最初の引数にイベント名、2番目の引数にイベントデータの配列を受け取ります:
Event::listen('event.*', function (string $eventName, array $data) {
// ...
});
#イベントディスカバリー
EventServiceProvider の $listen 配列に手動でイベントやリスナーを登録する代わりに、自動イベントディスカバリーを有効にできます。イベントディスカバリーを有効にすると、Laravelはアプリケーションの Listeners ディレクトリをスキャンしてイベントとリスナーを自動的に検出・登録します。さらに、EventServiceProvider に明示的に定義されたイベントも引き続き登録されます。
LaravelはPHPのリフレクション機能を使ってリスナークラスをスキャンします。handle または __invoke で始まるメソッドが見つかると、そのメソッドのシグネチャで型指定されたイベントに対するリスナーとして登録します:
use App\Events\PodcastProcessed;
class SendPodcastNotification
{
/**
* 指定されたイベントを処理します。
*/
public function handle(PodcastProcessed $event): void
{
// ...
}
}
イベントディスカバリーはデフォルトで無効ですが、アプリケーションの EventServiceProvider の shouldDiscoverEvents メソッドをオーバーライドして有効にできます:
/**
* イベントとリスナーを自動検出するかどうかを判定します。
*/
public function shouldDiscoverEvents(): bool
{
return true;
}
デフォルトでは、アプリケーションの app/Listeners ディレクトリ内のすべてのリスナーがスキャンされます。追加でスキャンしたいディレクトリがあれば、EventServiceProvider の discoverEventsWithin メソッドをオーバーライドして指定できます:
/**
* イベント検出に使用するリスナーディレクトリを取得します。
*
* @return array<int, string>
*/
protected function discoverEventsWithin(): array
{
return [
$this->app->path('Listeners'),
];
}
#本番環境でのイベントディスカバリー
本番環境では、リクエストごとにすべてのリスナーをスキャンするのは効率的ではありません。そこで、デプロイ時に event:cache Artisanコマンドを実行して、アプリケーションのすべてのイベントとリスナーのマニフェストをキャッシュしてください。このマニフェストをフレームワークが利用してイベント登録を高速化します。キャッシュを削除したい場合は event:clear コマンドを使います。
#イベントの定義
イベントクラスは基本的にイベントに関連する情報を保持するデータコンテナです。例えば、App\Events\OrderShipped イベントが Eloquent ORM のオブジェクトを受け取るとします:
<?php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderShipped
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* 新しいイベントインスタンスを作成します。
*/
public function __construct(
public Order $order,
) {}
}
ご覧の通り、このイベントクラスにはロジックは含まれていません。購入された App\Models\Order インスタンスのコンテナです。イベントで使われる SerializesModels トレイトは、イベントオブジェクトがPHPの serialize 関数でシリアライズされる場合(例えばキューイングされたリスナーを使うとき)に、Eloquentモデルを適切にシリアライズします。
#リスナーの定義
次に、例のイベントに対するリスナーを見てみましょう。イベントリスナーは handle メソッドでイベントインスタンスを受け取ります。event:generate と make:listener Artisanコマンドは適切なイベントクラスを自動でインポートし、handle メソッドに型指定します。handle メソッド内でイベントに応じた処理を行えます:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
class SendShipmentNotification
{
/**
* イベントリスナーを作成します。
*/
public function __construct()
{
// ...
}
/**
* イベントを処理します。
*/
public function handle(OrderShipped $event): void
{
// $event->order を使って注文にアクセスします...
}
}
イベントリスナーはコンストラクタで必要な依存関係を型指定できます。すべてのイベントリスナーはLaravelのサービスコンテナ経由で解決されるため、依存関係は自動的に注入されます。
#イベントの伝播を停止する
場合によっては、イベントの伝播を他のリスナーに対して停止したいことがあります。その場合はリスナーの handle メソッドから false を返してください。
#キューイングされたイベントリスナー
リスナーがメール送信やHTTPリクエストなど遅い処理を行う場合、キューイングが有効です。キューイングされたリスナーを使う前に、キューの設定を行い、サーバーやローカル環境でキューワーカーを起動してください。
リスナーをキューイングするには、リスナークラスに ShouldQueue インターフェイスを追加します。event:generate と make:listener Artisanコマンドで生成されたリスナーはすでにこのインターフェイスをインポートしているので、すぐに使えます:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendShipmentNotification implements ShouldQueue
{
// ...
}
以上です!このリスナーが処理するイベントがディスパッチされると、Laravelのキューシステムを使って自動的にキューに登録されます。キューでリスナーが実行されて例外が発生しなければ、処理完了後にキュージョブは自動的に削除されます。
#キュー接続、名前、遅延のカスタマイズ
イベントリスナーのキュー接続、キュー名、遅延時間をカスタマイズしたい場合は、リスナークラスに $connection、$queue、$delay プロパティを定義してください:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendShipmentNotification implements ShouldQueue
{
/**
* ジョブを送信する接続名。
*
* @var string|null
*/
public $connection = 'sqs';
/**
* ジョブを送信するキュー名。
*
* @var string|null
*/
public $queue = 'listeners';
/**
* ジョブが処理されるまでの遅延時間(秒)。
*
* @var int
*/
public $delay = 60;
}
リスナーのキュー接続、キュー名、遅延時間を実行時に指定したい場合は、リスナーに viaConnection、viaQueue、withDelay メソッドを定義できます。
/**
* リスナーのキュー接続名を取得します。
*/
public function viaConnection(): string
{
return 'sqs';
}
/**
* リスナーのキュー名を取得します。
*/
public function viaQueue(): string
{
return 'listeners';
}
/**
* ジョブが処理されるまでの秒数を取得します。
*/
public function withDelay(OrderShipped $event): int
{
return $event->highPriority ? 0 : 60;
}
#条件付きでリスナーをキューに追加する
実行時にのみ利用可能なデータに基づいて、リスナーをキューに追加するかどうかを判断したい場合があります。そのために、リスナーに shouldQueue メソッドを追加して、キューに追加すべきかどうかを判定できます。shouldQueue メソッドが false を返すと、リスナーは実行されません。
<?php
namespace App\Listeners;
use App\Events\OrderCreated;
use Illuminate\Contracts\Queue\ShouldQueue;
class RewardGiftCard implements ShouldQueue
{
/**
* 顧客にギフトカードを付与します。
*/
public function handle(OrderCreated $event): void
{
// ...
}
/**
* リスナーをキューに追加すべきか判定します。
*/
public function shouldQueue(OrderCreated $event): bool
{
return $event->order->subtotal >= 5000;
}
}
#キューとの手動操作
リスナーの基盤となるキュージョブの delete や release メソッドに手動でアクセスしたい場合は、Illuminate\Queue\InteractsWithQueue トレイトを使えます。このトレイトは生成されたリスナーにデフォルトでインポートされており、これらのメソッドにアクセスできます。
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
/**
* イベントを処理します。
*/
public function handle(OrderShipped $event): void
{
if (true) {
$this->release(30);
}
}
}
#キューイングされたイベントリスナーとデータベーストランザクション
キューイングされたリスナーがデータベーストランザクション内でディスパッチされると、トランザクションがコミットされる前にキューで処理されることがあります。この場合、トランザクション中にモデルやデータベースレコードに加えた更新がまだデータベースに反映されていない可能性があります。また、トランザクション内で作成されたモデルやレコードがまだ存在しない場合もあります。リスナーがこれらのモデルに依存していると、キューイングされたリスナーをディスパッチするジョブの処理時に予期しないエラーが発生することがあります。
キュー接続の after_commit 設定が false の場合でも、特定のキューイングされたリスナーをすべてのオープントランザクションがコミットされた後にディスパッチするように、リスナークラスで ShouldHandleEventsAfterCommit インターフェイスを実装できます。
<?php
namespace App\Listeners;
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue, ShouldHandleEventsAfterCommit
{
use InteractsWithQueue;
}
これらの問題の回避方法については、キュージョブとデータベーストランザクション のドキュメントをご覧ください。
#失敗したジョブの処理
キューイングされたイベントリスナーが失敗することがあります。キューのワーカーで定義された最大試行回数を超えると、リスナーの failed メソッドが呼ばれます。failed メソッドはイベントインスタンスと失敗の原因となった Throwable を受け取ります。
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Throwable;
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
/**
* イベントを処理します。
*/
public function handle(OrderShipped $event): void
{
// ...
}
/**
* ジョブの失敗を処理します。
*/
public function failed(OrderShipped $event, Throwable $exception): void
{
// ...
}
}
#キューイングされたリスナーの最大試行回数の指定
キューイングされたリスナーがエラーを起こしている場合、無限にリトライし続けるのは望ましくありません。Laravel はリスナーの試行回数や試行期間を指定する方法を提供しています。
リスナークラスに $tries プロパティを定義すると、リスナーが失敗とみなされるまでの最大試行回数を指定できます。
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
/**
* キューイングされたリスナーの最大試行回数。
*
* @var int
*/
public $tries = 5;
}
リスナーの最大試行回数の代わりに、リスナーが試行されなくなる時間を指定することもできます。これにより、指定した期間内は何度でもリトライできます。リスナーが試行されなくなる時間を指定するには、retryUntil メソッドをリスナーに追加し、DateTime インスタンスを返します。
use DateTime;
/**
* リスナーのタイムアウト時間を判定します。
*/
public function retryUntil(): DateTime
{
return now()->addMinutes(5);
}
#イベントのディスパッチ
イベントをディスパッチするには、イベントの静的な dispatch メソッドを呼び出します。このメソッドは Illuminate\Foundation\Events\Dispatchable トレイトによってイベントに提供されます。dispatch メソッドに渡した引数はイベントのコンストラクタに渡されます。
<?php
namespace App\Http\Controllers;
use App\Events\OrderShipped;
use App\Http\Controllers\Controller;
use App\Models\Order;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class OrderShipmentController extends Controller
{
/**
* 指定された注文を発送します。
*/
public function store(Request $request): RedirectResponse
{
$order = Order::findOrFail($request->order_id);
// 注文発送のロジック...
OrderShipped::dispatch($order);
return redirect('/orders');
}
}
条件付きでイベントをディスパッチしたい場合は、dispatchIf と dispatchUnless メソッドを使えます。
OrderShipped::dispatchIf($condition, $order);
OrderShipped::dispatchUnless($condition, $order);
テスト時に、特定のイベントがディスパッチされたことをリスナーを実行せずに検証したい場合があります。Laravel の 組み込みテストヘルパー が便利です。
#データベーストランザクション後のイベントディスパッチ
場合によっては、アクティブなデータベーストランザクションがコミットされた後にのみイベントをディスパッチしたいことがあります。その場合は、イベントクラスで ShouldDispatchAfterCommit インターフェイスを実装します。
このインターフェイスを実装すると、現在のデータベーストランザクションがコミットされるまでイベントはディスパッチされません。トランザクションが失敗した場合、イベントは破棄されます。トランザクションが進行中でない場合は、イベントは即座にディスパッチされます。
<?php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderShipped implements ShouldDispatchAfterCommit
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* 新しいイベントインスタンスを作成します。
*/
public function __construct(
public Order $order,
) {}
}
#イベントサブスクライバー
#イベントサブスクライバーの作成
イベントサブスクライバーは、サブスクライバークラス内で複数のイベントに登録できるクラスで、1つのクラスに複数のイベントハンドラーを定義できます。サブスクライバーは subscribe メソッドを定義し、イベントディスパッチャのインスタンスを受け取ります。渡されたディスパッチャの listen メソッドを呼び出してイベントリスナーを登録します。
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use Illuminate\Events\Dispatcher;
class UserEventSubscriber
{
/**
* ユーザーログインイベントを処理します。
*/
public function handleUserLogin(Login $event): void {}
/**
* ユーザーログアウトイベントを処理します。
*/
public function handleUserLogout(Logout $event): void {}
/**
* サブスクライバーのリスナーを登録します。
*/
public function subscribe(Dispatcher $events): void
{
$events->listen(
Login::class,
[UserEventSubscriber::class, 'handleUserLogin']
);
$events->listen(
Logout::class,
[UserEventSubscriber::class, 'handleUserLogout']
);
}
}
イベントリスナーのメソッドがサブスクライバー自身に定義されている場合、サブスクライバーの subscribe メソッドからイベントとメソッド名の配列を返すほうが便利です。Laravel はイベントリスナー登録時に自動的にサブスクライバーのクラス名を判別します。
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use Illuminate\Events\Dispatcher;
class UserEventSubscriber
{
/**
* ユーザーログインイベントを処理します。
*/
public function handleUserLogin(Login $event): void {}
/**
* ユーザーログアウトイベントを処理します。
*/
public function handleUserLogout(Logout $event): void {}
/**
* サブスクライバーのリスナーを登録します。
*
* @return array<string, string>
*/
public function subscribe(Dispatcher $events): array
{
return [
Login::class => 'handleUserLogin',
Logout::class => 'handleUserLogout',
];
}
}
#イベントサブスクライバーの登録
サブスクライバーを作成したら、イベントディスパッチャに登録します。EventServiceProvider の $subscribe プロパティを使ってサブスクライバーを登録できます。例えば、UserEventSubscriber をリストに追加します。
<?php
namespace App\Providers;
use App\Listeners\UserEventSubscriber;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* アプリケーションのイベントリスナーのマッピング。
*
* @var array
*/
protected $listen = [
// ...
];
/**
* 登録するサブスクライバークラス。
*
* @var array
*/
protected $subscribe = [
UserEventSubscriber::class,
];
}
#テスト
イベントをディスパッチするコードをテストする際、リスナーのコードは直接かつ別々にテストできるため、Laravel に実際にリスナーを実行させないように指示したい場合があります。もちろん、リスナー自体をテストする場合は、リスナーのインスタンスを生成し、テスト内で直接 handle メソッドを呼び出せます。
Event ファサードの fake メソッドを使うと、リスナーの実行を防ぎ、テスト対象のコードを実行し、その後 assertDispatched、assertNotDispatched、assertNothingDispatched メソッドでアプリケーションがどのイベントをディスパッチしたかを検証できます。
<?php
namespace Tests\Feature;
use App\Events\OrderFailedToShip;
use App\Events\OrderShipped;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* 注文の配送をテストする。
*/
public function test_orders_can_be_shipped(): void
{
Event::fake();
// 注文の配送処理を実行...
// イベントがディスパッチされたことを検証...
Event::assertDispatched(OrderShipped::class);
// イベントが2回ディスパッチされたことを検証...
Event::assertDispatched(OrderShipped::class, 2);
// イベントがディスパッチされなかったことを検証...
Event::assertNotDispatched(OrderFailedToShip::class);
// イベントが一切ディスパッチされなかったことを検証...
Event::assertNothingDispatched();
}
}
assertDispatched または assertNotDispatched メソッドにクロージャを渡すことで、特定の「真偽テスト」を通過するイベントがディスパッチされたかを検証できます。少なくとも1つのイベントがそのテストを通過すれば、アサーションは成功します。
Event::assertDispatched(function (OrderShipped $event) use ($order) {
return $event->order->id === $order->id;
});
イベントリスナーが特定のイベントをリッスンしているかを検証したい場合は、assertListening メソッドを使えます。
Event::assertListening(
OrderShipped::class,
SendShipmentNotification::class
);
Event::fake() を呼び出すと、イベントリスナーは一切実行されなくなります。そのため、モデルの creating イベントで UUID を生成するなど、イベントに依存するモデルファクトリーを使う場合は、ファクトリーの使用後に Event::fake() を呼び出すべきです。
#イベントの一部だけをフェイクする
特定のイベントだけリスナーの実行をフェイクしたい場合は、それらのイベントを fake または fakeFor メソッドに渡せます。
/**
* 注文処理をテストする。
*/
public function test_orders_can_be_processed(): void
{
Event::fake([
OrderCreated::class,
]);
$order = Order::factory()->create();
Event::assertDispatched(OrderCreated::class);
// 他のイベントは通常通りディスパッチされる...
$order->update([...]);
}
指定したイベント以外はすべてフェイクしたい場合は、except メソッドを使えます。
Event::fake()->except([
OrderCreated::class,
]);
#スコープ付きイベントフェイク
テストの一部だけでイベントリスナーの実行をフェイクしたい場合は、fakeFor メソッドを使えます。
<?php
namespace Tests\Feature;
use App\Events\OrderCreated;
use App\Models\Order;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* Test order process.
*/
public function test_orders_can_be_processed(): void
{
$order = Event::fakeFor(function () {
$order = Order::factory()->create();
Event::assertDispatched(OrderCreated::class);
return $order;
});
// イベントは通常通りディスパッチされ、オブザーバーも実行される...
$order->update([...]);
}
}