Skip to content

イベント

イントロダクション

Laravelのイベントは、シンプルなオブザーバーパターンの実装を提供し、アプリケーション内で発生するさまざまなイベントを購読してリッスンできるようにします。イベントクラスは通常、app/Eventsディレクトリに保存され、それらのリスナーはapp/Listenersに保存されます。これらのディレクトリがアプリケーションに表示されない場合でも心配しないでください。Artisanコンソールコマンドを使用してイベントとリスナーを生成すると、自動的に作成されます。

イベントは、アプリケーションのさまざまな側面を切り離すための優れた方法です。単一のイベントは、互いに依存しない複数のリスナーを持つことができるためです。たとえば、注文が発送されるたびにSlack通知をユーザーに送信したい場合があります。注文処理コードをSlack通知コードに結合する代わりに、App\Events\OrderShippedイベントを発生させ、リスナーが受信してSlack通知を送信するようにできます。

イベントとリスナーの生成

イベントとリスナーを素早く生成するには、make:eventmake:listenerのArtisanコマンドを使用できます:

php artisan make:event PodcastProcessed

php artisan make:listener SendPodcastNotification --event=PodcastProcessed

便宜上、追加の引数なしでmake:eventmake:listenerのArtisanコマンドを呼び出すこともできます。そうすると、Laravelは自動的にクラス名を尋ね、リスナーを作成する際にリッスンすべきイベントを尋ねます:

php artisan make:event

php artisan make:listener

イベントとリスナーの登録

イベントの自動検出

デフォルトでは、LaravelはアプリケーションのListenersディレクトリをスキャンすることで、イベントリスナーを自動的に見つけて登録します。Laravelは、handleまたは__invokeで始まるリスナークラスメソッドを見つけると、それらのメソッドをメソッドのシグネチャで型ヒントされたイベントのイベントリスナーとして登録します:

use App\Events\PodcastProcessed;

class SendPodcastNotification
{
    /**
     * イベントを処理します。
     */
    public function handle(PodcastProcessed $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ファサードを使用して、アプリケーションのAppServiceProviderbootメソッド内でイベントとそれに対応するリスナーを手動で登録できます:

use App\Domain\Orders\Events\PodcastProcessed;
use App\Domain\Orders\Listeners\SendPodcastNotification;
use Illuminate\Support\Facades\Event;

/**
 * 任意のアプリケーションサービスをブートストラップします。
 */
public function boot(): void
{
    Event::listen(
        PodcastProcessed::class,
        SendPodcastNotification::class,
    );
}

event:listコマンドを使用して、アプリケーション内に登録されているすべてのリスナーをリストアップできます:

php artisan event:list

クロージャリスナー

通常、リスナーはクラスとして定義されますが、アプリケーションのAppServiceProviderbootメソッド内でクロージャベースのイベントリスナーを手動で登録することもできます:

use App\Events\PodcastProcessed;
use Illuminate\Support\Facades\Event;

/**
 * 任意のアプリケーションサービスをブートストラップします。
 */
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;

/**
 * 任意のアプリケーションサービスをブートストラップします。
 */
public function boot(): void
{
    Event::listen(queueable(function (PodcastProcessed $event) {
        // ...
    }));
}

キューされたジョブと同様に、onConnectiononQueuedelayメソッドを使用して、キューされたリスナーの実行をカスタマイズできます:

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) {
    // キューされたリスナーが失敗しました...
}));

ワイルドカードイベントリスナー

*文字をワイルドカードパラメータとして使用してリスナーを登録することもできます。これにより、同じリスナーで複数のイベントをキャッチできます。ワイルドカードリスナーは、イベント名を最初の引数として受け取り、イベントデータの配列全体を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;

    /**
     * 新しいイベントインスタンスを作成します。
     */
    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
{
    /**
     * イベントリスナーを作成します。
     */
    public function __construct() {}

    /**
     * イベントを処理します。
     */
    public function handle(OrderShipped $event): void
    {
        // $event->orderを使用して注文にアクセスします...
    }
}

Note

イベントリスナーは、コンストラクタで必要な依存関係を型ヒントすることもできます。すべてのイベントリスナーは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
{
    /**
     * ジョブを送信する接続名。
     *
     * @var string|null
     */
    public $connection = 'sqs';

    /**
     * ジョブを送信するキュー名。
     *
     * @var string|null
     */
    public $queue = 'listeners';

    /**
     * ジョブが処理されるまでの時間(秒)。
     *
     * @var int
     */
    public $delay = 60;
}

リスナーのキュー接続、キュー名、または遅延時間を実行時に定義したい場合は、リスナーに viaConnectionviaQueue、または withDelay メソッドを定義できます:

/**
 * リスナーのキュー接続名を取得します。
 */
public function viaConnection(): string
{
    return 'sqs';
}

/**
 * リスナーのキュー名を取得します。
 */
public function viaQueue(): string
{
    return 'listeners';
}

/**
 * ジョブが処理されるまでの秒数を取得します。
 */
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
{
    /**
     * 顧客にギフトカードを贈る。
     */
    public function handle(OrderCreated $event): void
    {
        // ...
    }

    /**
     * リスナーをキューに入れるかどうかを決定します。
     */
    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;

    /**
     * イベントを処理します。
     */
    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;
}

Note

これらの問題を回避する方法について詳しくは、キュージョブとデータベーストランザクションに関するドキュメントをご覧ください。

失敗したジョブの処理

キューイベントリスナーが失敗することもあります。キューリスナーがキューワーカーによって定義された最大試行回数を超えると、リスナーの 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;

    /**
     * イベントを処理します。
     */
    public function handle(OrderShipped $event): void
    {
        // ...
    }

    /**
     * ジョブの失敗を処理します。
     */
    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;

    /**
     * キューリスナーが試行できる回数。
     *
     * @var int
     */
    public $tries = 5;
}

リスナーが試行されなくなる時間を定義する代わりに、リスナーが試行される時間枠内で任意の回数試行できるようにすることもできます。これを実現するには、リスナークラスに retryUntil メソッドを追加します。このメソッドは DateTime インスタンスを返す必要があります:

use DateTime;

/**
 * リスナーがタイムアウトする時間を決定します。
 */
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
{
    /**
     * 指定された注文を出荷します。
     */
    public function store(Request $request): RedirectResponse
    {
        $order = Order::findOrFail($request->order_id);

        // 注文出荷のロジック...

        OrderShipped::dispatch($order);

        return redirect('/orders');
    }
}

条件付きでイベントをディスパッチしたい場合は、dispatchIf および dispatchUnless メソッドを使用できます:

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

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

Note

テスト時に、特定のイベントがディスパッチされたことをアサートするだけで、実際にはそのリスナーをトリガーしないことが役立つ場合があります。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;

    /**
     * 新しいイベントインスタンスの作成
     */
    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
{
    /**
     * ユーザーログインイベントの処理
     */
    public function handleUserLogin(Login $event): void {}

    /**
     * ユーザーログアウトイベントの処理
     */
    public function handleUserLogout(Logout $event): void {}

    /**
     * サブスクライバのリスナーを登録
     */
    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
{
    /**
     * ユーザーログインイベントの処理
     */
    public function handleUserLogin(Login $event): void {}

    /**
     * ユーザーログアウトイベントの処理
     */
    public function handleUserLogout(Logout $event): void {}

    /**
     * サブスクライバのリスナーを登録
     *
     * @return array<string, string>
     */
    public function subscribe(Dispatcher $events): array
    {
        return [
            Login::class => 'handleUserLogin',
            Logout::class => 'handleUserLogout',
        ];
    }
}

イベントサブスクライバの登録

サブスクライバを作成した後、イベントディスパッチャに登録する準備が整いました。Event ファサードの subscribe メソッドを使用してサブスクライバを登録できます。通常、これはアプリケーションの AppServiceProviderboot メソッド内で行うべきです。

<?php

namespace App\Providers;

use App\Listeners\UserEventSubscriber;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * 任意のアプリケーションサービスのブートストラップ
     */
    public function boot(): void
    {
        Event::subscribe(UserEventSubscriber::class);
    }
}

テスト

イベントをディスパッチするコードをテストする際、イベントのリスナーを実際に実行せずに、イベントがディスパッチされたことをアサートしたい場合があります。リスナーのコードは直接テストでき、対応するイベントをディスパッチするコードとは別にテストできるためです。もちろん、リスナー自体をテストするには、リスナーインスタンスを作成し、テスト内で直接 handle メソッドを呼び出すことができます。

Event ファサードの fake メソッドを使用すると、リスナーの実行を防ぎ、テスト対象のコードを実行し、assertDispatchedassertNotDispatched、および assertNothingDispatched メソッドを使用してアプリケーションによってディスパッチされたイベントをアサートできます。

<?php

use App\Events\OrderFailedToShip;
use App\Events\OrderShipped;
use Illuminate\Support\Facades\Event;

test('orders can be shipped', function () {
    Event::fake();

    // 注文の発送を実行...

    // イベントがディスパッチされたことをアサート...
    Event::assertDispatched(OrderShipped::class);

    // イベントが2回ディスパッチされたことをアサート...
    Event::assertDispatched(OrderShipped::class, 2);

    // イベントがディスパッチされなかったことをアサート...
    Event::assertNotDispatched(OrderFailedToShip::class);

    // イベントがディスパッチされなかったことをアサート...
    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
{
    /**
     * 注文の発送をテスト
     */
    public function test_orders_can_be_shipped(): void
    {
        Event::fake();

        // 注文の発送を実行...

        // イベントがディスパッチされたことをアサート...
        Event::assertDispatched(OrderShipped::class);

        // イベントが2回ディスパッチされたことをアサート...
        Event::assertDispatched(OrderShipped::class, 2);

        // イベントがディスパッチされなかったことをアサート...
        Event::assertNotDispatched(OrderFailedToShip::class);

        // イベントがディスパッチされなかったことをアサート...
        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
);

Warning

Event::fake() を呼び出した後、イベントリスナーは実行されません。したがって、UUIDをモデルの creating イベント中に作成するなど、イベントに依存するモデルファクトリをテストで使用する場合、ファクトリを使用した後に Event::fake() を呼び出す必要があります。

イベントのサブセットのフェイク

特定のイベントセットに対してのみイベントリスナーをフェイクしたい場合は、fake または fakeFor メソッドにそれらを渡すことができます。

test('orders can be processed', function () {
    Event::fake([
        OrderCreated::class,
    ]);

    $order = Order::factory()->create();

    Event::assertDispatched(OrderCreated::class);

    // 他のイベントは通常通りディスパッチされます...
    $order->update([...]);
});
/**
 * 注文処理をテスト
 */
public function test_orders_can_be_processed(): void
{
    Event::fake([
        OrderCreated::class,
    ]);

    $order = Order::factory()->create();

    Event::assertDispatched(OrderCreated::class);

    // 他のイベントは通常通りディスパッチされます...
    $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;
    });

    // イベントは通常通りディスパッチされ、オブザーバーが実行されます...
    $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
{
    /**
     * 注文処理をテスト
     */
    public function test_orders_can_be_processed(): void
    {
        $order = Event::fakeFor(function () {
            $order = Order::factory()->create();

            Event::assertDispatched(OrderCreated::class);

            return $order;
        });

        // イベントは通常通りディスパッチされ、オブザーバーが実行されます...
        $order->update([...]);
    }
}

ユーザーノート