Events
Table of Contents
Introduction
Laravel の events は、シンプルなオブザーバーパターンの実装を提供し、あなたの application 内で発生するさまざまな events を subscribe してリッスンすることができます。 Event クラスは通常、app/Events
ディレクトリに保存され、その listeners は app/Listeners
に保存されます。これらのディレクトリがあなたの application に見えなくても心配しないでください、なぜならあなたが events と listeners を Artisan console コマンドを使用して生成するときにそれらは作成されます。
Events は、application の様々な要素を分離する素晴らしい方法として役立ちます。なぜなら、シングル Events は、互いに依存しない複数の listeners を持つことができるからです。例えば、注文が発送されるたびに user に Slack 通知を送りたい場合があります。注文の処理コードを Slack 通知コードに結びつける代わりに、listeners が受け取って Slack 通知を dispatch するために使用できるApp\Events\OrderShipped
Events を発生させることができます。
Generating Events and Listeners
すばやく events と listeners を生成するには、make:event
およびmake:listener
という Artisan コマンドを使用できます:
php artisan make:event PodcastProcessed
php artisan make:listener SendPodcastNotification --event=PodcastProcessed
便宜上、追加の引数なしで make:event
と make:listener
の Artisan コマンドを呼び出すこともできます。そうすると、 Laravel は自動的にあなたに class 名を prompt し、リスナーを作成するときには、それがリッスンすべき event を指示します:
php artisan make:event
php artisan make:listener
Registering Events and Listeners
Event 発見
default として、Laravel は、あなたの application のListeners
ディレクトリをスキャンして、自動的にあなたの event listeners を登録します。Laravel が、handle
または__invoke
で始まる listener class methods を見つけたとき、その methods が signature で types 指定されている event の event listeners として、それらの methods を登録します。
use App\Events\PodcastProcessed;
class SendPodcastNotification
{
/**
* Handle the given event.
*/
public function handle(PodcastProcessed $event): void
{
// ...
}
}
あなたが listeners を異なるディレクトリや複数のディレクトリに保存する予定の場合、withEvents
method を使用してそれらのディレクトリを Laravel にスキャンさせるよう指示できます。この操作は、アプリケーションの bootstrap/app.php
ファイル内で行います:
->withEvents(discover: [
__DIR__.'/../app/Domain/Listeners',
])
event:list
の command は、あなたの application 内で登録されているすべての listeners をリストするために使用できます:
php artisan event:list
Production における Event の発見
あなたの application の速度を向上させるために、optimize
またはevent:cache
の Artisan コマンドを使用して、アプリケーションの listeners のマニフェスト全体を cache する必要があります。通常、この command は、あなたのアプリケーションのdeployment processの一部として実行するべきです。このマニフェストはフレームワークによって event の登録 process を高速化するために使用されます。event:clear
command は、 event cache を削除するために使用することができます。
手動で Events を登録する
Event
facade を使用すると、アプリケーションのAppServiceProvider
のboot
method 内で手動で register events とそれに対応する listeners を設定することができます:
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
の command は、あなたの application 内に登録されているすべての listeners を一覧表示するために使用することができます:
php artisan event:list
クロージャー Listeners
通常、 listeners はクラスとして定義されますが、アプリケーションのAppServiceProvider
のboot
method で手動で閉じ込めベースの event listeners を register することも可能です。
use App\Events\PodcastProcessed;
use Illuminate\Support\Facades\Event;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Event::listen(function (PodcastProcessed $event) {
// ...
});
}
キュー可能な匿名の Event Listeners
クロージャーベースの event listeners を登録する際には、リスナークロージャをIlluminate\Events\queueable
関数でラップして、 Laravel にリスナーをqueueを使用して実行するよう指示することができます:
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) {
// ...
}));
}
キューに入れられた jobs のように、キューに入れられたリスナーの実行をカスタマイズするためにonConnection
、onQueue
、およびdelay
メソッドを使用することができます:
Event::listen(queueable(function (PodcastProcessed $event) {
// ...
})->onConnection('redis')->onQueue('podcasts')->delay(now()->addSeconds(10)));
匿名のキューリスナーの失敗を handle したい場合は、queueable
リスナーを定義する際に、catch
method にクロージャを提供できます。このクロージャは、 event インスタンスと、リスナーの失敗を引き起こした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...
}));
ワイルドカード Event Listeners
また、ワイルドカードパラメータとして*
文字を使用して、 register listeners することも可能で、これにより同じリスナー上で複数の events を catch することができます。ワイルドカードの listeners は、最初の引数として event 名を、2 番目の引数として全体の event data array を受け取ります:
Event::listen('event.*', function (string $eventName, array $data) {
// ...
});
Defining Events
event class とは基本的に、 event に関連する情報を保持する data コンテナです。例えば、App\Events\OrderShipped
event がEloquent ORM object を受け取るとしましょう:
<?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,
) {}
}
ご覧の通り、この event class はロジックを含んでいません。これは購入されたApp\Models\Order
インスタンスのコンテナです。 event で使われているSerializesModels
トレイトは、PHP のserialize
関数を使用して event object がシリアライズされた場合、たとえば queued listenersを利用する際などに Eloquent models を優雅にシリアライズします。
Defining Listeners
次に、私たちの例の event に対するリスナーを見てみましょう。 Event listeners は、そのhandle
method 内で event インスタンスを受け取ります。make:listener
Artisan command は、--event
オプションで呼び出されると、適切な event class を自動的にインポートし、 event をhandle
method 内で型ヒントします。handle
method 内で、 event に対応するために必要なアクションを実行することができます:
<?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...
}
}
NOTE
あなたの event listeners は、コンストラクタで必要とする依存関係を型ヒントとしても指定できます。すべての event listeners は、 Laravel のservice containerを介して解決されるため、依存関係は自動的に注入されます。
Event の伝播を停止する
時には、 event の伝播を他の listeners に止めたいと思うこともあるかもしれません。その場合、リスナーの handle
method から false
を返すことでそれを実現できます。
Queued Event Listeners
listeners を queue イングすることは、mail の送信や HTTP request の作成など、時間のかかるタスクを実行する場合に有用です。queue イングされた listeners を使用する前に、queue を設定し、server またはローカル development 環境で queue ワーカーを起動してください。
リスナーがキューに入れられるように指定するには、リスナーの class にShouldQueue
インターフェースを追加します。make:listener
Artisan コマンドで生成された Listeners はすでにこのインターフェースを現在の名前空間にインポートしているため、すぐに使用することができます。
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendShipmentNotification implements ShouldQueue
{
// ...
}
それで完了です!これで、このリスナーが処理する event がディスパッチされると、リスナーは Laravel のqueue systemを使用して event ディスパッチャーによって自動的にキューに追加されます。リスナーが queue によって実行される時に exceptions がスローされなければ、キューに追加された job は自動的に削除されます。 processing が完了した後にです。
Queue Connection のカスタマイズ、名前、& Delay
もし event リスナーの queue connection 、 queue 名、または queue delay 時間をカスタマイズしたい場合、リスナーの class に $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;
}
リスナーの queue connection 、 queue 名、または delay を runtime で定義したい場合は、リスナーで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;
}
条件付きキューイング Listeners
時々、リスナーがキューに入るべきかどうかを決定する必要があり、その判断材料は data としてしか利用できない runtime の情報に基づいているかもしれません。このような場合に対処するために、リスナーに shouldQueue
method を追加してリスナーがキューに入るべきかどうかを判断できます。shouldQueue
method が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;
}
}
Queue と手動でやり取りする
リスナーの基礎となる queue ジョブの delete
と release
メソッドへ手動でアクセスする必要がある場合、Illuminate\Queue\InteractsWithQueue
trait を使用して行うことができます。この trait は、生成された listeners に対して default でインポートされ、これらのメソッドへのアクセスを提供します。
<?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);
}
}
}
キューイングされた Event Listeners と Database Transactions
キューに入れられた listeners が database transactions 内でディスパッチされると、それらは database transaction がコミットされる前に queue によって処理される可能性があります。これが起こると、 database transaction の間に models や database のレコードに行った更新は、まだ database に反映されていない可能性があります。また、 transaction の中で作成された models や database のレコードは、 database に存在しない可能性もあります。リスナーがこれらの models に依存している場合、キューに入れられたリスナーをディスパッチする job が処理されるときに、予期しない errors が発生する可能性があります。
あなたの queue 接続のafter_commit
設定オプションがfalse
に設定されている場合でも、特定のキューに入れられたリスナーがすべてのオープンな database transactions がコミットされた後にディスパッチされるべきであることを示すことはできます。それは、リスナーの class 上で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;
}
NOTE
これらの問題を回避する方法について詳しく知るために、キュー内の jobs と database transactionsに関するドキュメンテーションをご覧ください。
失敗した Jobs の処理
場合によっては、キューに追加された event listeners が失敗することがあります。キューに追加されたリスナーが queue ワーカーによって定義された最大試行回数を超えた場合、failed
method がリスナーで呼び出されます。failed
method は event インスタンスと、その失敗を引き起こした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
{
// ...
}
}
キューリスナーの最大試行回数の指定
あなたのキューに入れられた listeners の 1 つが error に遭遇している場合、おそらくそれが無限に再試行し続けることは望ましくないでしょう。そのため、 Laravel は、リスナーが何回またはどれくらいの期間試行されるかを指定するための様々な方法を提供しています。
リスナーの class に $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;
}
listener が失敗するまで何回試行するかを定義する代わりに、listener を試行しない時刻を定義することもできます。これにより、listener は指定された時間枠内で何回でも試行することが可能になります。listener の試行を停止する時刻を定義するには、retryUntil
method を listener の class に追加してください。この method はDateTime
インスタンスを返すべきです:
use DateTime;
/**
* Determine the time at which the listener should timeout.
*/
public function retryUntil(): DateTime
{
return now()->addMinutes(5);
}
Dispatching Events
event を dispatch するためには、event 上で静的なdispatch
method を呼び出すことができます。この method はIlluminate\Foundation\Events\Dispatchable
トレイトによって event に用意されています。dispatch
method に渡された引数はすべて、イベントのコンストラクターに渡されます:
<?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');
}
}
条件付きで dispatch あるいは event を行いたい場合は、dispatchIf
メソッドとdispatchUnless
メソッドを使用することができます:
OrderShipped::dispatchIf($condition, $order);
OrderShipped::dispatchUnless($condition, $order);
NOTE
testing helpers する際には、特定の events が dispatch されたことを assert することが役立つことがありますが、それらの listeners を実際にトリガーすることなく行います。Laravel の組み込み testing helperのおかげでとても簡単になります。
Database Transactions の後に Events を dispatch する
時々、アクティブな database transaction がコミットした後にのみ、 Laravel に event を dispatch するよう指示したいことがあるかもしれません。そのためには、ShouldDispatchAfterCommit
インターフェースを event class に実装することができます。
このインターフェースは、現在の database transaction がコミットされるまで、 Laravel に event を dispatch しないよう指示します。もし transaction が失敗した場合、 event は破棄されます。もし event が発行された時に進行中の database transaction がない場合、 event はすぐに発行されます。
<?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,
) {}
}
Event Subscribers
Event サブスクライバーの記述
event の購読者は、購読者の class 内から複数の event を購読することができるクラスであり、一つの class 内にいくつもの event ハンドラーを定義することが可能です。購読者は、 subscribe
method を定義すべきで、このメソッドは event dispatcher インスタンスが渡されます。その dispatch ャーに対してlisten
method を呼び出すことで、event listener の登録が可能です。
<?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']
);
}
}
もし、あなたの events listenermethod がサブスクライバ自体内で定義されているなら、サブスクライバの subscribe
method から events と method の名前の array を返す方が便利かもしれません。Laravel は event listeners を登録する時に自動的にサブスクライバの class 名を決定します。
<?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',
];
}
}
Event サブスクライバーの登録
サブスクライバを書いた後、それを register して event ディスパッチャーに登録する準備が整います。サブスクライバは、Event
facade のsubscribe
method を使用して register することができます。通常、これはアプリケーションのAppServiceProvider
のboot
method 内で行うべきです。
<?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);
}
}
Testing
events を発行する testing コードを使用する時、events の listeners を実際に実行しないように Laravel に指示することができます。その理由は、listeners のコードは発行するコードと対応する event とは別に直接そして独立して testing することができるからです。もちろん、listeners 自体を testing するには、listeners インスタンスを生成し、testing 中にhandle
method`を直接呼び出すことができます。
Event
facade のfake
method を使用すると、 listeners の実行を防ぎ、test 対象の code を実行し、その後、assertDispatched
、assertNotDispatched
、およびassertNothingDispatched
メソッドを使用して、あなたの application によって発行された events を assert することができます。
<?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
methods に渡すことで、特定の"真実 test"を通過する event が dispatch されたことを assert することができます。少なくとも一つの event が指定された真実 test を通過していれば、アサーションは成功します:
Event::assertDispatched(function (OrderShipped $event) use ($order) {
return $event->order->id === $order->id;
});
もし単純に event リスナーが特定の event を聴いていることを assert したい場合は、assertListening
method を使用することができます。
Event::assertListening(
OrderShipped::class,
SendShipmentNotification::class
);
WARNING
Event::fake()
を呼び出した後、 event listeners は実行されません。そのため、テストが events に依存する model のファクトリーを使用している場合(たとえば、モデルのcreating
event の間に UUID を生成するような場合)、ファクトリーを使用した後にEvent::fake()
を呼び出すべきです。
Events の一部を偽造する
特定の events のみに対して fake event listeners を設定したい場合、それらをfake
またはfakeFor
method に渡すことができます:
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
method を使用して、指定された一連の events を除いてすべての events を fake することができます。
Event::fake()->except([
OrderCreated::class,
]);
スコープした Event フェイク
テストの一部でのみ fake event listeners を使用したい場合は、fakeFor
method を使用することができます。
<?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([...]);
}
}