本文へ移動
Laravel Tips

イベントとリスナー

「注文が発送された」のような出来事を知らせるイベントと、それを受けて動くリスナーの作り方・登録のしかた・キューとの組み合わせ・テストの方法を説明します。

イベントは、アプリの中で起きた出来事を知らせる「お知らせ」です。リスナーは、そのお知らせを受け取って動く係です。学校の放送のように、1つのお知らせを、何人もの係が別々に聞いて、それぞれの仕事をします。たとえば、注文を発送したら利用者に Slack(チャットのサービス)で知らせたいとします。発送の処理の中に Slack の処理を直接書くと、2つの処理がからまってしまいます。そこで、発送の処理は OrderShipped というイベントを出すだけにし、Slack に知らせる処理はリスナーに任せます。

イベントのクラスは、ふつう app/Events に、リスナーは app/Listeners に置きます。フォルダが見つからなくても大丈夫です。Artisan コマンド(php artisan で動かす Laravel のコマンド)でクラスを作ると、フォルダも自動で作られます。

イベントとリスナーを作る#

make:event と make:listener のコマンドで、すばやく作れます。

bash
php artisan make:event PodcastProcessed

php artisan make:listener SendPodcastNotification --event=PodcastProcessed

引数を付けずに実行すると、クラスの名前(リスナーなら、聞くイベントも)を聞かれます。

bash
php artisan make:event

php artisan make:listener

イベントとリスナーを登録する#

イベントの自動検出#

既定では、Laravel がアプリの Listeners フォルダを調べて、リスナーを自動で見つけて登録します。リスナーのクラスに、handle か __invoke で始まるメソッドがあれば、そのメソッドの引数に型で書いてあるイベントのリスナーとして登録されます。

php
use App\Events\PodcastProcessed;

class SendPodcastNotification
{
    /**
     * Handle the event.
     */
    public function handle(PodcastProcessed $event): void
    {
        // ...
    }
}

PHP のユニオン型(| でつなぐ書き方)を使えば、1つのメソッドで複数のイベントを聞けます。

php
/**
 * Handle the event.
 */
public function handle(PodcastProcessed|PodcastPublished $event): void
{
    // ...
}

リスナーを別のフォルダや複数のフォルダに置くなら、bootstrap/app.php の withEvents で、調べるフォルダを教えます。

php
->withEvents(discover: [
    __DIR__.'/../app/Domain/Orders/Listeners',
])

*(ワイルドカード。何にでも当てはまる記号)を使えば、似たフォルダをまとめて指せます。

php
->withEvents(discover: [
    __DIR__.'/../app/Domain/*/Listeners',
])

登録されているリスナーの一覧は、event:list コマンドで見られます。

bash
php artisan event:list

本番での自動検出#

アプリを速くするために、optimize か event:cache のコマンドで、リスナーの一覧(マニフェスト)をキャッシュしておきます。ふつうは、デプロイの手順の中で動かします。フレームワークが、この一覧を使って、登録を速く済ませます。キャッシュを消すには event:clear を使います。

検出するかを条件で決める#

リスナーを検出するかどうかを、条件で決めたいときは、リスナーに ShouldBeDiscovered を付けて、真偽を返す shouldBeDiscovered メソッドを書きます。false を返すと、そのリスナーは登録されません。

php
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 メソッドで、イベントとリスナーを自分で結びつけられます。

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,
    );
}

登録したリスナーの一覧は、同じく event:list で見られます。

bash
php artisan event:list

クロージャのリスナー#

リスナーはふつうクラスで作りますが、クロージャ(名前のない関数)でも、AppServiceProvider の boot メソッドで登録できます。

php
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 の関数で包むと、キューで動かせます。

php
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 で動かし方を決められます。

php
Event::listen(queueable(function (PodcastProcessed $event) {
    // ...
})->onConnection('redis')->onQueue('podcasts')->delay(now()->plus(seconds: 10)));

失敗したときの処理は、catch にクロージャを渡して決めます。イベントと、失敗の原因の Throwable が渡されます。

php
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引数にイベントのデータの配列が渡されます。

php
Event::listen('event.*', function (string $eventName, array $data) {
    // ...
});

イベントを作る#

イベントのクラスは、そのイベントに関するデータを入れておく入れ物にすぎません。たとえば、App\Events\OrderShipped が Eloquent のモデル(データベースの表を、PHP から扱いやすくしたクラス。Eloquent)を受け取る例です。

php
<?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
<?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
<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendShipmentNotification implements ShouldQueue
{
    // ...
}

これだけです。このリスナーが受けるイベントが発生すると、リスナーは自動でキューに入ります。キューから動かしたときに例外(処理の途中で起きたエラーの知らせ)が出なければ、終わったあとで、ジョブは自動で消えます。

接続・キューの名前・待ち時間を決める#

リスナーの接続・キューの名前・待ち時間を決めたいときは、リスナーに PHP の属性 Connection・Queue・Delay を付けます。

php
<?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 のメソッドを書きます。

php
/**
 * 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
<?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
<?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
<?php

namespace App\Listeners;

use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
use Illuminate\Queue\InteractsWithQueue;

class SendShipmentNotification implements ShouldQueueAfterCommit
{
    use InteractsWithQueue;
}

補足

この問題への対策は、キューのページの「データベースのトランザクションとジョブ」にくわしく書いてあります。

キューのリスナーとミドルウェア#

キューに入れるリスナーにも、ジョブミドルウェア(ジョブの前後に決まった処理をはさむしくみ。キュー)が使えます。作ったジョブミドルウェアは、リスナーの middleware メソッドで返して付けます。

php
<?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
<?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
<?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
<?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
<?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
<?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
<?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 で待つ時間の上限を決めます。

php
#[DebounceFor(30, maxWait: 120)]
class UpdateProductSearchIndex implements ShouldQueue
{
    // ...
}

デバウンスの記録に使うキャッシュは、debounceVia メソッドで選べます。メソッドはイベントを受け取り、キャッシュを返します。

php
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
<?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
<?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 を返します。

php
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
<?php

namespace App\Listeners;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Backoff;

#[Backoff(3)]
class SendShipmentNotification implements ShouldQueue
{
    // ...
}

待つ時間を複雑に決めたいときは、backoff メソッドを書きます。

php
/**
 * 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秒ずつ待ちます。

php
/**
 * 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
<?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
<?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
<?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
<?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 を使います。

php
OrderShipped::dispatchIf($condition, $order);

OrderShipped::dispatchUnless($condition, $order);

補足

テストでは、リスナーを動かさずに、イベントが出されたかだけを確かめたいことがあります。Laravel には、そのための道具があります。後の「テスト」を見てください。

トランザクションが確定してからイベントを出す#

開いているトランザクションが確定してから、イベントを出したいことがあります。そのときは、イベントのクラスに ShouldDispatchAfterCommit を付けます。

いまのトランザクションが確定するまで、イベントは出されません。トランザクションが失敗すれば、イベントは捨てられます。イベントを出したとき、トランザクションが開いていなければ、すぐ出されます。

php
<?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() にクロージャを渡します。

php
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引数に、イベントの配列を渡します。

php
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
<?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
<?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
<?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
<?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
<?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つでも出ていれば、成功です。

php
Event::assertDispatched(function (OrderShipped $event) use ($order) {
    return $event->order->id === $order->id;
});

あるリスナーが、あるイベントを聞いているかだけを確かめたいときは、assertListening を使います。

php
Event::assertListening(
    OrderShipped::class,
    SendShipmentNotification::class
);

注意

Event::fake() を呼んだあとは、どのリスナーも動きません。モデルのファクトリ(ためしのデータを自動で作るしくみ)が、モデルの creating イベントで UUID(ほかと重ならない ID)を作る、のようにイベントに頼っているなら、ファクトリを使ったあとで Event::fake() を呼んでください。

Event のアサーションは、次のとおりです。

メソッド 説明
assertDispatched イベントが出されたことを確かめる(回数も指定できる)
assertDispatchedOnce イベントがちょうど1回出されたことを確かめる
assertNotDispatched イベントが出されていないことを確かめる
assertNothingDispatched イベントが1つも出されていないことを確かめる
assertListening リスナーがそのイベントを聞いていることを確かめる

一部のイベントだけをにせものにする#

一部のイベントのリスナーだけを、にせものにしたいときは、そのイベントを fake か fakeFor に渡します。

Pest で書く場合です。

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([
        // ...
    ]);
});

PHPUnit で書く場合です。

php
/**
 * 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 を使います。

php
Event::fake()->except([
    OrderCreated::class,
]);

テストの一部分だけにせものを使う#

テストの一部分だけ、リスナーをにせものにしたいときは、fakeFor を使います。

Pest で書く場合です。

php
<?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
<?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日時点の内容をもとに、日本語でまとめています。

ページの一覧