メインコンテンツまでスキップ

イベント

はじめに

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

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

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

イベントとリスナーを素早く生成するために、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 で始まるリスナークラスのメソッドを見つけると、そのメソッドをイベントリスナーとして登録します。メソッドのシグネチャで型ヒントされたイベントに対して:

    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/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;

/**
* Bootstrap any application services.
*/
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;

/**
* 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) {
// ...
}));
}

キューイングされたジョブと同様に、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) {
// The queued listener failed...
}));

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

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

/**
* Create a new event instance.
*/
public function __construct(
public Order $order,
) {}
}

このイベントクラスにはロジックが含まれていないことがわかります。これは、購入されたApp\Models\Orderインスタンスのコンテナです。イベントがPHPのserialize関数を使用してシリアル化される場合(たとえば、キューイングされたリスナーを利用する場合)、イベントで使用されるSerializesModelsトレイトは、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のキューシステムを使用して、イベントディスパッチャによってリスナーが自動的にキューに入れられます。キューによってリスナーが実行されたときに例外がスローされない場合、キューされたジョブは処理が完了した後に自動的に削除されます。

キュー接続、名前、および遅延のカスタマイズ

    <?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;
}

イベントリスナーのキュー接続、キュー名、または遅延時間をカスタマイズしたい場合は、リスナークラスで$connection$queue、または$delayプロパティを定義することができます。

    /**
* 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;
}

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

    <?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;
}
}

キューイングリスナーの条件付き

時々、ランタイムでのみ利用可能なデータに基づいてリスナーをキューに入れるかどうかを決定する必要があります。これを実現するために、リスナーにshouldQueueメソッドを追加して、リスナーをキューに入れるかどうかを決定することができます。shouldQueueメソッドがfalseを返すと、リスナーはキューに入れられません。

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

キューイングイベントリスナーとデータベーストランザクションの手動操作

リスナーの基礎となるキュージョブのdeleteおよびreleaseメソッドに手動でアクセスする必要がある場合は、Illuminate\Queue\InteractsWithQueueトレイトを使用できます。このトレイトは、生成されたリスナーでデフォルトでインポートされ、これらのメソッドにアクセスできます。

(((((b5a4b6c5e7f5e4b4)))))

キューイングリスナーがデータベーストランザクション内でディスパッチされる場合、データベーストランザクションがコミットされる前にキューによって処理される可能性があります。これが発生すると、データベーストランザクション中にモデルやデータベースレコードに行った更新がまだデータベースに反映されていない可能性があります。さらに、トランザクション内で作成されたモデルやデータベースレコードはデータベースに存在しないかもしれません。リスナーがこれらのモデルに依存している場合、キューによってディスパッチされるジョブが処理されると予期しないエラーが発生する可能性があります。

キュー接続のafter_commit構成オプションがfalseに設定されている場合、特定のキューイングリスナーがディスパッチされるタイミングを、リスナークラスで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;
}

注記

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

失敗したジョブの処理

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

If you would like to conditionally dispatch an event, you may use the `dispatchIf` and `dispatchUnless` methods:

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

イベントサブスクライバ

イベントサブスクライバの記述

イベントサブスクライバは、サブスクライバクラス自体から複数のイベントにサブスクライブできるクラスであり、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',
];
}
}

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

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

    <?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 メソッドを使用すると、リスナーの実行を防止し、テストコードを実行し、その後、アプリケーションがディスパッチしたイベントを assertDispatchedassertNotDispatchedassertNothingDispatched メソッドを使用してアサートできます:

<?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
);

警告

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

イベントの一部を偽装する

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

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