イベント
イントロダクション
Laravelのイベントは、シンプルなオブザーバーパターンの実装を提供し、アプリケーション内で発生するさまざまなイベントを購読およびリスニングできるようにします。イベントクラスは通常、app/Events
ディレクトリに格納され、リスナーはapp/Listeners
ディレクトリに格納されます。アプリケーションにこれらのディレクトリが表示されない場合でも、Artisanコンソールコマンドを使用してイベントとリスナーを生成するときに作成されるため、心配する必要はありません。
イベントは、アプリケーションのさまざまな側面を分離する優れた方法です。単一のイベントに、互いに依存しない複数のリスナーを設定できるためです。たとえば、注文が出荷されるたびに、Slack通知をユーザーに送信したい場合があります。注文処理コードをSlack通知コードに結合する代わりに、リスナーが受信してSlack通知のディスパッチに使用できるApp\Events\OrderShipped
イベントを発生させることができます。
イベントとリスナーの生成
イベントとリスナーを迅速に生成するには、make:event
およびmake:listener
Artisanコマンドを使用します
php artisan make:event PodcastProcessed php artisan make:listener SendPodcastNotification --event=PodcastProcessed
便宜上、追加の引数なしでmake:event
およびmake:listener
Artisanコマンドを呼び出すこともできます。その場合、Laravelはクラス名を自動的に要求し、リスナーを作成するときは、リスニングするイベントを要求します
php artisan make:event php artisan make:listener
イベントとリスナーの登録
イベント検出
デフォルトでは、LaravelはアプリケーションのListeners
ディレクトリをスキャンすることで、イベントリスナーを自動的に検出して登録します。Laravelは、handle
または__invoke
で始まるリスナーのクラスメソッドを見つけると、それらのメソッドを、メソッドのシグネチャでタイプヒントが指定されたイベントのイベントリスナーとして登録します
use App\Events\PodcastProcessed; class SendPodcastNotification{ /** * Handle the given event. */ public function handle(PodcastProcessed $event): void { // ... }}
PHPのユニオン型を使用して、複数のイベントをリスニングできます
/** * Handle the given event. */public function handle(PodcastProcessed|PodcastPublished $event): void{ // ...}
リスナーを別のディレクトリまたは複数のディレクトリに格納する場合は、アプリケーションのbootstrap/app.php
ファイルでwithEvents
メソッドを使用して、Laravelにそれらのディレクトリをスキャンするように指示できます
->withEvents(discover: [ __DIR__.'/../app/Domain/Orders/Listeners',])
event:list
コマンドを使用して、アプリケーション内に登録されているすべてのリスナーを一覧表示できます
php artisan event:list
本番環境でのイベント検出
アプリケーションの速度を向上させるには、optimize
またはevent:cache
Artisanコマンドを使用して、アプリケーションのすべてのリスナーのマニフェストをキャッシュする必要があります。通常、このコマンドは、アプリケーションのデプロイメントプロセスの一部として実行する必要があります。このマニフェストは、イベント登録プロセスを高速化するためにフレームワークによって使用されます。event:clear
コマンドを使用して、イベントキャッシュを破棄できます。
イベントの手動登録
Event
ファサードを使用すると、アプリケーションのAppServiceProvider
のboot
メソッド内で、イベントと対応するリスナーを手動で登録できます
use App\Domain\Orders\Events\PodcastProcessed;use App\Domain\Orders\Listeners\SendPodcastNotification;use Illuminate\Support\Facades\Event; /** * Bootstrap any application services. */public function boot(): void{ Event::listen( PodcastProcessed::class, SendPodcastNotification::class, );}
event:list
コマンドを使用して、アプリケーション内に登録されているすべてのリスナーを一覧表示できます
php artisan event:list
クロージャリスナー
通常、リスナーはクラスとして定義されます。ただし、アプリケーションのAppServiceProvider
のboot
メソッドに、クロージャベースのイベントリスナーを手動で登録することもできます
use App\Events\PodcastProcessed;use Illuminate\Support\Facades\Event; /** * Bootstrap any application services. */public function boot(): void{ Event::listen(function (PodcastProcessed $event) { // ... });}
キューイング可能な匿名イベントリスナー
クロージャベースのイベントリスナーを登録する場合、リスナーのクロージャをIlluminate\Events\queueable
関数でラップして、キューを使用してリスナーを実行するようにLaravelに指示できます
use App\Events\PodcastProcessed;use function Illuminate\Events\queueable;use Illuminate\Support\Facades\Event; /** * Bootstrap any application services. */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) { // The queued listener failed...}));
ワイルドカードイベントリスナー
また、ワイルドカードパラメータとして*
文字を使用してリスナーを登録することもできます。これにより、同じリスナーで複数のイベントをキャッチできます。ワイルドカードリスナーは、最初の引数としてイベント名、2番目の引数としてイベントデータ配列全体を受け取ります
Event::listen('event.*', function (string $eventName, array $data) { // ...});
イベントの定義
イベントクラスは、基本的に、イベントに関連する情報を保持するデータコンテナです。たとえば、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; /** * Create a new event instance. */ public function __construct( public Order $order, ) {}}
ご覧のとおり、このイベントクラスにはロジックが含まれていません。購入されたApp\Models\Order
インスタンスのコンテナです。イベントで使用されるSerializesModels
トレイトは、キューイングされたリスナーを利用する場合など、PHPのserialize
関数を使用してイベントオブジェクトがシリアル化される場合に、Eloquentモデルを正常にシリアル化します。
リスナーの定義
次に、サンプルイベントのリスナーを見てみましょう。イベントリスナーは、handle
メソッドでイベントインスタンスを受け取ります。--event
オプションを指定してmake:listener
Artisanコマンドを呼び出すと、適切なイベントクラスが自動的にインポートされ、handle
メソッドでイベントのタイプヒントが指定されます。handle
メソッド内では、イベントに応答するために必要なアクションを実行できます
<?php namespace App\Listeners; use App\Events\OrderShipped; class SendShipmentNotification{ /** * Create the event listener. */ public function __construct() {} /** * Handle the event. */ public function handle(OrderShipped $event): void { // Access the order using $event->order... }}
イベントリスナーは、コンストラクタで必要な依存関係のタイプヒントを指定することもできます。すべてのイベントリスナーは、Laravel サービスコンテナを介して解決されるため、依存関係は自動的に注入されます。
イベントの伝播を停止する
場合によっては、イベントの伝播を他のリスナーに停止したい場合があります。リスナーのhandle
メソッドからfalse
を返すことで、これを行うことができます。
キューイングされたイベントリスナー
リスナーがメール送信やHTTPリクエストなどの時間のかかるタスクを実行する場合、キューイングリスナーを使用すると便利です。キューイングリスナーを使用する前に、キューの設定を行い、サーバーまたはローカル開発環境でキューワーカーを起動してください。
リスナーをキューイングする必要があることを指定するには、リスナークラスにShouldQueue
インターフェースを追加します。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{ /** * The name of the connection the job should be sent to. * * @var string|null */ public $connection = 'sqs'; /** * The name of the queue the job should be sent to. * * @var string|null */ public $queue = 'listeners'; /** * The time (seconds) before the job should be processed. * * @var int */ public $delay = 60;}
実行時にリスナーのキュー接続、キュー名、または遅延を定義する場合は、リスナーでviaConnection
、viaQueue
、またはwithDelay
メソッドを定義できます。
/** * Get the name of the listener's queue connection. */public function viaConnection(): string{ return 'sqs';} /** * Get the name of the listener's queue. */public function viaQueue(): string{ return 'listeners';} /** * Get the number of seconds before the job should be processed. */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{ /** * Reward a gift card to the customer. */ public function handle(OrderCreated $event): void { // ... } /** * Determine whether the listener should be queued. */ 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; /** * Handle the event. */ public function handle(OrderShipped $event): void { if (true) { $this->release(30); } }}
キューイングされたイベントリスナーとデータベースのトランザクション
データベース・トランザクション内でキューイングされたリスナーがディスパッチされると、データベース・トランザクションがコミットされる前にキューによって処理される場合があります。この場合、データベース・トランザクション中にモデルまたはデータベース・レコードに加えた更新がデータベースに反映されていない可能性があります。また、トランザクション内で作成されたモデルまたはデータベース・レコードがデータベースに存在しない可能性があります。リスナーがこれらのモデルに依存している場合、キューイングされたリスナーをディスパッチするジョブが処理されるときに予期しないエラーが発生する可能性があります。
キュー接続のafter_commit
設定オプションがfalse
に設定されている場合でも、リスナークラスでShouldQueueAfterCommit
インターフェースを実装することにより、特定のキューイングされたリスナーがすべてのオープン・データベース・トランザクションがコミットされた後にディスパッチされる必要があることを示すことができます。
<?php namespace App\Listeners; use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;use Illuminate\Queue\InteractsWithQueue; class SendShipmentNotification implements ShouldQueueAfterCommit{ 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; /** * Handle the event. */ public function handle(OrderShipped $event): void { // ... } /** * Handle a job failure. */ 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; /** * The number of times the queued listener may be attempted. * * @var int */ public $tries = 5;}
リスナーが失敗するまでに試行できる回数を定義する代わりに、リスナーの試行を停止する時間を定義できます。これにより、リスナーは指定された時間枠内で何回でも試行できます。リスナーの試行を停止する時間を定義するには、リスナークラスにretryUntil
メソッドを追加します。このメソッドは、DateTime
インスタンスを返す必要があります。
use DateTime; /** * Determine the time at which the listener should timeout. */public function retryUntil(): DateTime{ return now()->addMinutes(5);}
キューイングされたリスナーのバッファ時間の指定
例外が発生したリスナーを再試行する前にLaravelが待機する秒数を設定する場合は、リスナークラスでbackoff
プロパティを定義することで設定できます。
/** * The number of seconds to wait before retrying the queued listener. * * @var int */public $backoff = 3;
リスナーのバッファ時間を決定するためのより複雑なロジックが必要な場合は、リスナークラスでbackoff
メソッドを定義できます。
/** * Calculate the number of seconds to wait before retrying the queued listener. */public function backoff(): int{ return 3;}
backoff
メソッドからバックオフ値の配列を返すことで、「指数関数的」バックオフを簡単に設定できます。この例では、再試行の遅延は、最初の再試行で1秒、2回目の再試行で5秒、3回目の再試行で10秒、残りの試行回数が多い場合はそれ以降の再試行ごとに10秒になります。
/** * Calculate the number of seconds to wait before retrying the queued listener. * * @return array<int, int> */public function backoff(): array{ return [1, 5, 10];}
イベントのディスパッチ
イベントをディスパッチするには、イベントで静的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{ /** * Ship the given order. */ public function store(Request $request): RedirectResponse { $order = Order::findOrFail($request->order_id); // Order shipment logic... OrderShipped::dispatch($order); return redirect('/orders'); }}
イベントを条件付きでディスパッチする場合は、dispatchIf
およびdispatchUnless
メソッドを使用できます。
OrderShipped::dispatchIf($condition, $order); OrderShipped::dispatchUnless($condition, $order);
テストを行う際には、実際にリスナーをトリガーすることなく、特定のイベントがディスパッチされたことをアサートすると便利です。Laravelの組み込みテストヘルパーを使用すると、簡単に実現できます。
データベースのトランザクション後にイベントをディスパッチする
アクティブなデータベース・トランザクションがコミットされた後にのみイベントをディスパッチするようにLaravelに指示したい場合があります。そのためには、イベント・クラスで`ShouldDispatchAfterCommit`インターフェースを実装します。
このインターフェースは、現在のデータベース・トランザクションがコミットされるまでイベントをディスパッチしないようにLaravelに指示します。トランザクションが失敗した場合、イベントは破棄されます。イベントのディスパッチ時にデータベース・トランザクションが進行中でない場合、イベントはすぐにディスパッチされます。
<?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; /** * Create a new event instance. */ public function __construct( public Order $order, ) {}}
イベント購読者
イベント購読者の記述
イベントサブスクライバーとは、サブスクライバークラス自体から複数のイベントをサブスクライブできるクラスであり、単一のクラス内で複数のイベントハンドラーを定義できます。サブスクライバーは、イベントディスパッチャーインスタンスに渡されるsubscribe
メソッドを定義する必要があります。指定されたディスパッチャーでlisten
メソッドを呼び出して、イベントリスナーを登録できます。
<?php namespace App\Listeners; use Illuminate\Auth\Events\Login;use Illuminate\Auth\Events\Logout;use Illuminate\Events\Dispatcher; class UserEventSubscriber{ /** * Handle user login events. */ public function handleUserLogin(Login $event): void {} /** * Handle user logout events. */ public function handleUserLogout(Logout $event): void {} /** * Register the listeners for the subscriber. */ 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{ /** * Handle user login events. */ public function handleUserLogin(Login $event): void {} /** * Handle user logout events. */ public function handleUserLogout(Logout $event): void {} /** * Register the listeners for the subscriber. * * @return array<string, string> */ public function subscribe(Dispatcher $events): array { return [ Login::class => 'handleUserLogin', Logout::class => 'handleUserLogout', ]; }}
イベント購読者の登録
サブスクライバーを作成した後、Laravelのイベント検出規則に従っている場合、Laravelはサブスクライバー内のハンドラーメソッドを自動的に登録します。そうでない場合は、`Event`ファサードの`subscribe`メソッドを使用して、サブスクライバーを手動で登録できます。通常、これはアプリケーションの`AppServiceProvider`の`boot`メソッド内で行う必要があります。
<?php namespace App\Providers; use App\Listeners\UserEventSubscriber;use Illuminate\Support\Facades\Event;use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider{ /** * Bootstrap any application services. */ public function boot(): void { Event::subscribe(UserEventSubscriber::class); }}
テスト
イベントをディスパッチするコードをテストする場合、リスナーのコードは対応するイベントをディスパッチするコードとは別に直接テストできるため、Laravelにイベントのリスナーを実際に実行しないように指示することがあります。もちろん、リスナー自体をテストするには、リスナーインスタンスをインスタンス化し、テストで`handle`メソッドを直接呼び出すことができます。
`Event`ファサードの`fake`メソッドを使用すると、リスナーの実行を防ぎ、テスト対象のコードを実行し、アプリケーションによってディスパッチされたイベントを`assertDispatched`、`assertNotDispatched`、および`assertNothingDispatched`メソッドを使用してアサートできます。
<?php use App\Events\OrderFailedToShip;use App\Events\OrderShipped;use Illuminate\Support\Facades\Event; test('orders can be shipped', function () { Event::fake(); // Perform order shipping... // Assert that an event was dispatched... Event::assertDispatched(OrderShipped::class); // Assert an event was dispatched twice... Event::assertDispatched(OrderShipped::class, 2); // Assert an event was not dispatched... Event::assertNotDispatched(OrderFailedToShip::class); // Assert that no events were dispatched... Event::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{ /** * Test order shipping. */ public function test_orders_can_be_shipped(): void { Event::fake(); // Perform order shipping... // Assert that an event was dispatched... Event::assertDispatched(OrderShipped::class); // Assert an event was dispatched twice... Event::assertDispatched(OrderShipped::class, 2); // Assert an event was not dispatched... Event::assertNotDispatched(OrderFailedToShip::class); // Assert that no events were dispatched... 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`メソッドに渡すことができます。
test('orders can be processed', function () { Event::fake([ OrderCreated::class, ]); $order = Order::factory()->create(); Event::assertDispatched(OrderCreated::class); // Other events are dispatched as normal... $order->update([...]);});
/** * Test order process. */public function test_orders_can_be_processed(): void{ Event::fake([ OrderCreated::class, ]); $order = Order::factory()->create(); Event::assertDispatched(OrderCreated::class); // Other events are dispatched as normal... $order->update([...]);}
`except`メソッドを使用して、指定されたイベントセットを除くすべてのイベントを偽造できます。
Event::fake()->except([ OrderCreated::class,]);
スコープ付きイベントフェイク
テストの一部に対してのみイベントリスナーを偽造する場合は、`fakeFor`メソッドを使用できます。
<?php use App\Events\OrderCreated;use App\Models\Order;use Illuminate\Support\Facades\Event; test('orders can be processed', function () { $order = Event::fakeFor(function () { $order = Order::factory()->create(); Event::assertDispatched(OrderCreated::class); return $order; }); // Events are dispatched as normal and observers will run ... $order->update([...]);});
<?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; }); // Events are dispatched as normal and observers will run ... $order->update([...]); }}