イベントとリスナー
「注文が発送された」のような出来事を知らせるイベントと、それを受けて動くリスナーの作り方・登録のしかた・キューとの組み合わせ・テストの方法を説明します。
イベントは、アプリの中で起きた出来事を知らせる「お知らせ」です。リスナーは、そのお知らせを受け取って動く係です。学校の放送のように、1つのお知らせを、何人もの係が別々に聞いて、それぞれの仕事をします。たとえば、注文を発送したら利用者に Slack(チャットのサービス)で知らせたいとします。発送の処理の中に Slack の処理を直接書くと、2つの処理がからまってしまいます。そこで、発送の処理は OrderShipped というイベントを出すだけにし、Slack に知らせる処理はリスナーに任せます。
イベントのクラスは、ふつう app/Events に、リスナーは app/Listeners に置きます。フォルダが見つからなくても大丈夫です。Artisan コマンド(php artisan で動かす Laravel のコマンド)でクラスを作ると、フォルダも自動で作られます。
イベントとリスナーを作る#
make:event と make:listener のコマンドで、すばやく作れます。
php artisan make:event PodcastProcessed
php artisan make:listener SendPodcastNotification --event=PodcastProcessed
引数を付けずに実行すると、クラスの名前(リスナーなら、聞くイベントも)を聞かれます。
php artisan make:event
php artisan make:listener
イベントとリスナーを登録する#
イベントの自動検出#
既定では、Laravel がアプリの Listeners フォルダを調べて、リスナーを自動で見つけて登録します。リスナーのクラスに、handle か __invoke で始まるメソッドがあれば、そのメソッドの引数に型で書いてあるイベントのリスナーとして登録されます。
use App\Events\PodcastProcessed;
class SendPodcastNotification
{
/**
* Handle the event.
*/
public function handle(PodcastProcessed $event): void
{
// ...
}
}
PHP のユニオン型(| でつなぐ書き方)を使えば、1つのメソッドで複数のイベントを聞けます。
/**
* Handle the event.
*/
public function handle(PodcastProcessed|PodcastPublished $event): void
{
// ...
}
リスナーを別のフォルダや複数のフォルダに置くなら、bootstrap/app.php の withEvents で、調べるフォルダを教えます。
->withEvents(discover: [
__DIR__.'/../app/Domain/Orders/Listeners',
])
*(ワイルドカード。何にでも当てはまる記号)を使えば、似たフォルダをまとめて指せます。
->withEvents(discover: [
__DIR__.'/../app/Domain/*/Listeners',
])
登録されているリスナーの一覧は、event:list コマンドで見られます。
php artisan event:list
本番での自動検出#
アプリを速くするために、optimize か event:cache のコマンドで、リスナーの一覧(マニフェスト)をキャッシュしておきます。ふつうは、デプロイの手順の中で動かします。フレームワークが、この一覧を使って、登録を速く済ませます。キャッシュを消すには event:clear を使います。
検出するかを条件で決める#
リスナーを検出するかどうかを、条件で決めたいときは、リスナーに ShouldBeDiscovered を付けて、真偽を返す shouldBeDiscovered メソッドを書きます。false を返すと、そのリスナーは登録されません。
use Illuminate\Contracts\Events\ShouldBeDiscovered;
class SendPodcastNotification implements ShouldBeDiscovered
{
/**
* Handle the event.
*/
public function handle(PodcastProcessed $event): void
{
// ...
}
/**
* Determine if the listener should be discovered.
*/
public static function shouldBeDiscovered(): bool
{
return app()->environment('production');
}
}
手動で登録する#
Event ファサード(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 の関数で包むと、キューで動かせます。
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) {
// ...
}));
}
ジョブ(キューに並べる1つ1つの仕事)と同じように、onConnection・onQueue・delay で動かし方を決められます。
Event::listen(queueable(function (PodcastProcessed $event) {
// ...
})->onConnection('redis')->onQueue('podcasts')->delay(now()->plus(seconds: 10)));
失敗したときの処理は、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...
}));
ワイルドカードのリスナー#
* を使って登録すると、1つのリスナーで、複数のイベントをまとめて受けられます。ワイルドカードのリスナーには、第1引数にイベントの名前、第2引数にイベントのデータの配列が渡されます。
Event::listen('event.*', function (string $eventName, array $data) {
// ...
});
イベントを作る#
イベントのクラスは、そのイベントに関するデータを入れておく入れ物にすぎません。たとえば、App\Events\OrderShipped が Eloquent のモデル(データベースの表を、PHP から扱いやすくしたクラス。Eloquent)を受け取る例です。
<?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 のトレイト(いくつものクラスに同じ機能を足す部品)は、Eloquent のモデルをうまく変換してくれます。イベントが PHP の serialize 関数で文字に変換されるとき(たとえば、キューに入れるリスナーのとき)に働きます。
リスナーを作る#
次に、このイベントのリスナーを見てみましょう。リスナーは、handle メソッドでイベントを受け取ります。make:listener に --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 で作ったリスナーには、すでに読み込みが入っているので、すぐ使えます。
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendShipmentNotification implements ShouldQueue
{
// ...
}
これだけです。このリスナーが受けるイベントが発生すると、リスナーは自動でキューに入ります。キューから動かしたときに例外(処理の途中で起きたエラーの知らせ)が出なければ、終わったあとで、ジョブは自動で消えます。
接続・キューの名前・待ち時間を決める#
リスナーの接続・キューの名前・待ち時間を決めたいときは、リスナーに PHP の属性 Connection・Queue・Delay を付けます。
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Connection;
use Illuminate\Queue\Attributes\Delay;
use Illuminate\Queue\Attributes\Queue;
#[Connection('sqs')]
#[Queue('listeners')]
#[Delay(60)]
class SendShipmentNotification implements ShouldQueue
{
// ...
}
動かすときに決めたいなら、リスナーに 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;
}
| 属性 | メソッド | 決めること |
|---|---|---|
Connection |
viaConnection |
キューの接続の名前 |
Queue |
viaQueue |
キューの名前 |
Delay |
withDelay |
動かすまでの待ち時間(秒) |
キューに入れるリスナーを、全部同じキューに送りたいなら、リスナーごとに決めずに、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 ($condition) {
$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;
}
補足
この問題への対策は、キューのページの「データベースのトランザクションとジョブ」にくわしく書いてあります。
キューのリスナーとミドルウェア#
キューに入れるリスナーにも、ジョブミドルウェア(ジョブの前後に決まった処理をはさむしくみ。キュー)が使えます。作ったジョブミドルウェアは、リスナーの middleware メソッドで返して付けます。
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use App\Jobs\Middleware\RateLimited;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendShipmentNotification implements ShouldQueue
{
/**
* Handle the event.
*/
public function handle(OrderShipped $event): void
{
// Process the event...
}
/**
* Get the middleware the listener should pass through.
*
* @return array<int, object>
*/
public function middleware(OrderShipped $event): array
{
return [new RateLimited];
}
}
リスナーを暗号化する#
リスナーのデータを、他人に読まれたり書き換えられたりしないようにできます。リスナーに ShouldBeEncrypted を付けると、キューに入れる前に、自動で暗号化されます。
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendShipmentNotification implements ShouldQueue, ShouldBeEncrypted
{
// ...
}
同じリスナーを1つだけにする(ユニーク)#
注意
ユニークなリスナーには、ロック(同時に1つだけが使える鍵)に対応したキャッシュが要ります。対応しているのは memcached・redis・dynamodb・database・file・array です。
同じリスナーが、キューに同時に1つしか入らないようにできます。リスナーのクラスに ShouldBeUnique を付けます。
<?php
namespace App\Listeners;
use App\Events\LicenseSaved;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
class AcquireProductKey implements ShouldQueue, ShouldBeUnique
{
public function __invoke(LicenseSaved $event): void
{
// ...
}
}
この例では、同じリスナーがすでにキューにあって終わっていなければ、新しくは入りません。ライセンスが短い間に何度保存されても、プロダクトキーを取るのは1回だけになります。
「何をもって同じとするか」の目印(キー)や、ユニークでいる時間の上限も決められます。リスナーに、uniqueId と uniqueFor を、プロパティかメソッドで書きます。メソッドは、イベントを受け取るので、イベントのデータで値を作れます。
<?php
namespace App\Listeners;
use App\Events\LicenseSaved;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
class AcquireProductKey implements ShouldQueue, ShouldBeUnique
{
/**
* The number of seconds after which the listener's unique lock will be released.
*
* @var int
*/
public $uniqueFor = 3600;
public function __invoke(LicenseSaved $event): void
{
// ...
}
/**
* Get the unique ID for the listener.
*/
public function uniqueId(LicenseSaved $event): string
{
return 'listener:'.$event->license->id;
}
}
この例では、ライセンスの ID が同じリスナーは、先のリスナーが終わるまで無視されます。同じライセンスで、プロダクトキーを二重に取らないための書き方です。1時間たっても終わっていなければ、ロックが外れ、同じ目印の新しいリスナーを、キューに入れられるようになります。
注意
複数のサーバーやコンテナからイベントを出すときは、すべてが同じキャッシュのサーバーにつながるようにしてください。そうしないと、ユニークかを正しく判断できません。
処理が始まるまでユニークにする#
ふつう、ユニークのロックは、リスナーが終わったとき(または、再挑戦を使い切って失敗したとき)に外れます。「処理が始まる直前に外したい」ときは、ShouldBeUnique の代わりに ShouldBeUniqueUntilProcessing を使います。
<?php
namespace App\Listeners;
use App\Events\LicenseSaved;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Contracts\Queue\ShouldQueue;
class AcquireProductKey implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
// ...
}
ユニークのロックに使うキャッシュ#
ShouldBeUnique のリスナーが送られるとき、Laravel は uniqueId の値でロックを取ろうとします。すでに取られていれば、送りません。ロックには、ふつう既定のキャッシュを使います。別のキャッシュを使いたいときは、uniqueVia メソッドで返します。
<?php
namespace App\Listeners;
use App\Events\LicenseSaved;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;
class AcquireProductKey implements ShouldQueue, ShouldBeUnique
{
// ...
/**
* Get the cache driver for the unique listener lock.
*/
public function uniqueVia(LicenseSaved $event): Repository
{
return Cache::driver('redis');
}
}
補足
「同時に動くのを1つに制限したいだけ」なら、WithoutOverlapping というジョブミドルウェアを使ってください(キュー)。
連続したイベントは最後の1つだけ処理する(デバウンス)#
短い間に何度も出されたイベントのうち、いちばん新しい1つだけを処理したいことがあります。キューに入れるリスナーに、PHP の属性 DebounceFor を付けます。
<?php
namespace App\Listeners;
use App\Events\ProductUpdated;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\DebounceFor;
#[DebounceFor(30)]
class UpdateProductSearchIndex implements ShouldQueue
{
/**
* Handle the event.
*/
public function handle(ProductUpdated $event): void
{
// Update the product's search index...
}
/**
* Get the debounce ID for the listener.
*/
public function debounceId(ProductUpdated $event): string
{
return (string) $event->product->getKey();
}
}
この例では、同じ商品の ProductUpdated が 30 秒以内に何度出されても、最後のイベントだけが処理されます。デバウンスの ID がちがうものは、別々に扱われます。
何度も出されて、リスナーが延々と後回しになるのを防ぎたいときは、maxWait で待つ時間の上限を決めます。
#[DebounceFor(30, maxWait: 120)]
class UpdateProductSearchIndex implements ShouldQueue
{
// ...
}
デバウンスの記録に使うキャッシュは、debounceVia メソッドで選べます。メソッドはイベントを受け取り、キャッシュを返します。
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;
public function debounceVia(ProductUpdated $event): Repository
{
return Cache::driver('redis');
}
デバウンスとユニークは、同時に使えません。DebounceFor を付けたリスナーは、ShouldBeUnique を付けないでください。
注意
複数のサーバーやコンテナからイベントを出すときは、すべてが同じキャッシュのサーバーにつながるようにしてください。
失敗したときの扱い#
キューに入れたリスナーも、失敗することがあります。ワーカーで決めた最大の試す回数を超えると、リスナーの 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 には、回数や時間の上限を決める方法があります。
リスナーに PHP の属性 Tries を付けると、何回試したら失敗とするかを決められます。
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\InteractsWithQueue;
#[Tries(5)]
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
// ...
}
回数のかわりに、「この時刻を過ぎたら、もう試さない」と決めることもできます。その時間のあいだは、何回でも試せます。リスナーに retryUntil メソッドを足し、DateTimeInterface を返します。
use DateTimeInterface;
/**
* Determine the time at which the listener should timeout.
*/
public function retryUntil(): DateTimeInterface
{
return now()->plus(minutes: 5);
}
retryUntil と tries の両方があるときは、retryUntil が優先されます。
再挑戦までの待ち時間(バックオフ)#
例外が出たリスナーを、何秒待ってから再挑戦するかは、PHP の属性 Backoff で決められます。
<?php
namespace App\Listeners;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Backoff;
#[Backoff(3)]
class SendShipmentNotification implements ShouldQueue
{
// ...
}
待つ時間を複雑に決めたいときは、backoff メソッドを書きます。
/**
* Calculate the number of seconds to wait before retrying the queued listener.
*/
public function backoff(OrderShipped $event): int
{
return 3;
}
backoff メソッドから秒数の配列を返すと、だんだん待ち時間を伸ばす「指数的な」待ち方にできます。次の例では、1回目の再挑戦まで1秒、2回目まで5秒、3回目まで10秒です。それ以降は、試せる回数が残っているかぎり、10秒ずつ待ちます。
/**
* Calculate the number of seconds to wait before retrying the queued listener.
*
* @return list<int>
*/
public function backoff(OrderShipped $event): array
{
return [1, 5, 10];
}
例外の回数の上限#
「何度でも試してよいが、処理されなかった例外が決まった回数に達したら失敗にする」こともできます(release で戻しただけの再挑戦は、数えません)。リスナーに、PHP の属性 Tries と MaxExceptions を付けます。
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\MaxExceptions;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\InteractsWithQueue;
#[Tries(25)]
#[MaxExceptions(3)]
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
/**
* Handle the event.
*/
public function handle(OrderShipped $event): void
{
// Process the event...
}
}
この例では、リスナーは最大25回まで再挑戦されます。ただし、処理されなかった例外が3回出たら失敗になります。
制限時間(タイムアウト)#
リスナーにかかる時間は、だいたい分かっていることが多いはずです。Laravel では、制限時間(タイムアウト)を決められます。これより長く処理していると、そのリスナーを動かしているワーカーは、エラーで終了します。リスナーに PHP の属性 Timeout を付けて、最大の秒数を決めます。
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Timeout;
#[Timeout(120)]
class SendShipmentNotification implements ShouldQueue
{
// ...
}
時間切れになったリスナーを、失敗として扱いたいときは、PHP の属性 FailOnTimeout を付けます。
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\FailOnTimeout;
#[FailOnTimeout]
class SendShipmentNotification implements ShouldQueue
{
// ...
}
イベントを出す#
イベントを出すには、イベントの static な(インスタンスを作らずにクラス名から呼べる)dispatch メソッドを呼びます。このメソッドは、Illuminate\Foundation\Events\Dispatchable のトレイトが、イベントに足してくれます。dispatch に渡した引数は、イベントのコンストラクターに渡されます。
<?php
namespace App\Http\Controllers;
use App\Events\OrderShipped;
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 には、そのための道具があります。後の「テスト」を見てください。
トランザクションが確定してからイベントを出す#
開いているトランザクションが確定してから、イベントを出したいことがあります。そのときは、イベントのクラスに 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;
/**
* Create a new event instance.
*/
public function __construct(
public Order $order,
) {}
}
イベントを後回しにする(Event::defer)#
後回し(defer)にすると、モデルのイベントの発生とリスナーの実行を、決めた処理のかたまりが終わるまで遅らせられます。関連するデータを全部作ってから、リスナーを動かしたいときに便利です。
イベントを後回しにするには、Event::defer() にクロージャを渡します。
use App\Models\User;
use Illuminate\Support\Facades\Event;
Event::defer(function () {
$user = User::create(['name' => 'Victoria Otwell']);
$user->posts()->create(['title' => 'My first post!']);
});
クロージャの中で起きたイベントは、すべて、クロージャが終わってから出されます。リスナーは、後回しの処理の中で作られた関連データを、すべて使えます。クロージャの中で例外が出たら、後回しにしたイベントは出されません。
一部のイベントだけを後回しにするには、第2引数に、イベントの配列を渡します。
use App\Models\User;
use Illuminate\Support\Facades\Event;
Event::defer(function () {
$user = User::create(['name' => 'Victoria Otwell']);
$user->posts()->create(['title' => 'My first post!']);
}, ['eloquent.created: '.User::class]);
イベントのサブスクライバー#
サブスクライバーは、1つのクラスの中で、複数のイベントを受け持つクラスです。1つのクラスに、いくつものイベント用の処理を書けます。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 が自動で登録します。そうでなければ、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);
}
}
テスト#
イベントを出すコードをテストするときは、リスナーを実際には動かさないほうが、よいことが多いです。リスナーの中身は、イベントを出す側のコードとは別に、単独でテストできるからです。リスナーそのものをテストするには、リスナーのインスタンス(クラスから作った実物)を作って、テストの中で handle を直接呼びます。
Event ファサードの fake を呼ぶと、リスナーが動かなくなります。テストしたいコードを動かしたあと、assertDispatched・assertNotDispatched・assertNothingDispatched などのアサーション(「こうなっているはず」を確かめる命令)で、アプリが出したイベントを確かめられます。
Pest(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 dispatched once...
Event::assertDispatchedOnce(OrderShipped::class);
// Assert an event was not dispatched...
Event::assertNotDispatched(OrderFailedToShip::class);
// Assert that no events were dispatched...
Event::assertNothingDispatched();
});
PHPUnit(PHP のテストの道具)で書く場合です。
<?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 dispatched once...
Event::assertDispatchedOnce(OrderShipped::class);
// Assert an event was not dispatched...
Event::assertNotDispatched(OrderFailedToShip::class);
// Assert that no events were dispatched...
Event::assertNothingDispatched();
}
}
assertDispatched と assertNotDispatched には、条件を書いたクロージャ(true か false を返す関数)を渡せます。条件に合うイベントが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(ほかと重ならない ID)を作る、のようにイベントに頼っているなら、ファクトリを使ったあとで Event::fake() を呼んでください。
Event のアサーションは、次のとおりです。
| メソッド | 説明 |
|---|---|
assertDispatched |
イベントが出されたことを確かめる(回数も指定できる) |
assertDispatchedOnce |
イベントがちょうど1回出されたことを確かめる |
assertNotDispatched |
イベントが出されていないことを確かめる |
assertNothingDispatched |
イベントが1つも出されていないことを確かめる |
assertListening |
リスナーがそのイベントを聞いていることを確かめる |
一部のイベントだけをにせものにする#
一部のイベントのリスナーだけを、にせものにしたいときは、そのイベントを fake か fakeFor に渡します。
Pest で書く場合です。
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([
// ...
]);
});
PHPUnit で書く場合です。
/**
* 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 を使います。
Pest で書く場合です。
<?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([
// ...
]);
});
PHPUnit で書く場合です。
<?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([
// ...
]);
}
}
関連するページ#
公式ドキュメント(英語)
2026年10月5日時点の内容をもとに、日本語でまとめています。