はじめに
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
で始まるリスナークラスメソッドを見つけると、Laravelはそのメソッドを、メソッドのシグネチャで型ヒントされたイベントのイベントリスナーとして登録します:
use App\Events\PodcastProcessed;
class SendPodcastNotification
{
/**
* Handle the given event.
*/
public function handle(PodcastProcessed $event): void
{
// ...
}
}
リスナーを別のディレクトリまたは複数のディレクトリに保存する予定がある場合、アプリケーションのbootstrap/app.php
ファイル内のwithEvents
メソッドを使用して、Laravelにそれらのディレクトリをスキャンさせることができます:
->withEvents(discover: [
__DIR__.'/../app/Domain/Orders/Listeners',
])
``````shell
php artisan event:list
`
本番環境でのイベントの発見
アプリケーションの速度を向上させるために、optimize
またはevent:cache
Artisanコマンドを使用して、アプリケーションのすべてのリスナーのマニフェストをキャッシュする必要があります。通常、このコマンドはアプリケーションのデプロイメントプロセスの一部として実行されるべきです。このマニフェストは、フレームワークがイベント登録プロセスを高速化するために使用します。event:clear
コマンドを使用して、イベントキャッシュを破棄できます。
イベントの手動登録
``````php
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,
);
}
`
``````shell
php artisan event:list
`
クロージャリスナー
通常、リスナーはクラスとして定義されますが、アプリケーションの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...
}));
ワイルドカードイベントリスナー
``````php
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
メソッドでイベントインスタンスを受け取ります。make:listener
Artisanコマンドは、--event
オプションを指定して呼び出すと、適切なイベントクラスを自動的にインポートし、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
{
// ...
}
}
キューリスナーの最大試行回数の指定
キューリスナーの1つがエラーに遭遇している場合、それが無限に再試行されることは望ましくありません。したがって、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);
}
イベントの発行
イベントを発行するには、イベントの静的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のイベント発見規約に従っている場合、サブスクライバー内のハンドラーメソッドを自動的に登録します。そうでない場合、subscribe
メソッドを使用してサブスクライバーを手動で登録できます。通常、これはアプリケーションのAppServiceProvider
メソッド内で行うべきです:
<?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
メソッドを直接呼び出すことができます。
``````php
<?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
);
<a name="faking-a-subset-of-events"></a>
### イベントのサブセットをフェイクする
特定のイベントのセットに対してのみイベントリスナーをフェイクしたい場合、`````fake`````または`````fakeFor`````メソッドに渡すことができます:
``````php
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([...]);
}
}