事件 - Event

簡介

Laravel 的 Event(事件) 提供了一種簡單的 Observer 設計模式實作,能讓你註冊(Subscribe)監聽(Listen)程式內發生的多種事件。Event 類別一般儲存在 app/Events 目錄下,而 Listener(監聽程式) 則一般儲存在 app/Listeners 目錄。若在專案內沒看到這些目錄的話請別擔心,在使用 Artisan 指令產生 Event 跟 Listener 的時候會自動建立。

Event 是以各種層面解耦(Decouple)程式的好方法,因為一個 Event 可以由多個不互相依賴的 Listener。舉例來說,我們可能會想在訂單出貨的時候傳送 Slack 通知給使用者。除了耦合訂單處理的程式碼跟 Slack 通知的程式碼外,我們可以產生一個 App\Events\OrderShipped 事件,然後使用一個 Listener 來接收並分派 Slack 通知。

註冊 Event 與 Listener

在你的 Laravel 專案中有個 App\Providers\EventServiceProvider,這個 Service Provider 是可以註冊所有 Event Listener 的好所在。listen 屬性是一個陣列,其中包含了所有的 Event (索引鍵) 即其 Listener (陣列值)。可以按照專案需求隨意增加 Event 到這個陣列。舉例來說,我們來新增一個 OrderShipped Event:

1use App\Events\OrderShipped;
2use App\Listeners\SendShipmentNotification;
3 
4/**
5 * The event listener mappings for the application.
6 *
7 * @var array
8 */
9protected $listen = [
10 OrderShipped::class => [
11 SendShipmentNotification::class,
12 ],
13];
1use App\Events\OrderShipped;
2use App\Listeners\SendShipmentNotification;
3 
4/**
5 * The event listener mappings for the application.
6 *
7 * @var array
8 */
9protected $listen = [
10 OrderShipped::class => [
11 SendShipmentNotification::class,
12 ],
13];
lightbulb

可以使用 event:list 指令來顯示程式中註冊的所有 Event 與 Listener 列表。

產生 Event 與 Listener

當然,手動為每個 Event 跟 Listener 建立檔案有點麻煩。我們不需要手動建立,只需要在 EventServiceProvider 中加上 Listener 與 Event,然後使用 event:generate Artisan 指令即可。這個指令會產生所有列在 EventServiceProvider 中不存在的 Event 與 Listener:

1php artisan event:generate
1php artisan event:generate

或者,也可以使用 make:eventmake:listener Artisan 指令來產生個別的 Event 與 Listener:

1php artisan make:event PodcastProcessed
2 
3php artisan make:listener SendPodcastNotification --event=PodcastProcessed
1php artisan make:event PodcastProcessed
2 
3php artisan make:listener SendPodcastNotification --event=PodcastProcessed

手動註冊 Event

一般來說,Event 應在 EventServiceProvider$listen 陣列中註冊。不過,也可以在 EventServiceProviderboot 方法中手動註冊基於類別或閉包的 Listener:

1use App\Events\PodcastProcessed;
2use App\Listeners\SendPodcastNotification;
3use Illuminate\Support\Facades\Event;
4 
5/**
6 * Register any other events for your application.
7 */
8public function boot(): void
9{
10 Event::listen(
11 PodcastProcessed::class,
12 [SendPodcastNotification::class, 'handle']
13 );
14 
15 Event::listen(function (PodcastProcessed $event) {
16 // ...
17 });
18}
1use App\Events\PodcastProcessed;
2use App\Listeners\SendPodcastNotification;
3use Illuminate\Support\Facades\Event;
4 
5/**
6 * Register any other events for your application.
7 */
8public function boot(): void
9{
10 Event::listen(
11 PodcastProcessed::class,
12 [SendPodcastNotification::class, 'handle']
13 );
14 
15 Event::listen(function (PodcastProcessed $event) {
16 // ...
17 });
18}

可放入佇列的匿名 Event Listener

在註冊基於閉包的 Event Listener 時,可以將該 Listener 閉包以 Illuminate\Events\queueable 函式包裝(Wrap)起來,以指示 Laravel 使用 Queue 來執行這個 Listener:

1use App\Events\PodcastProcessed;
2use function Illuminate\Events\queueable;
3use Illuminate\Support\Facades\Event;
4 
5/**
6 * Register any other events for your application.
7 */
8public function boot(): void
9{
10 Event::listen(queueable(function (PodcastProcessed $event) {
11 // ...
12 }));
13}
1use App\Events\PodcastProcessed;
2use function Illuminate\Events\queueable;
3use Illuminate\Support\Facades\Event;
4 
5/**
6 * Register any other events for your application.
7 */
8public function boot(): void
9{
10 Event::listen(queueable(function (PodcastProcessed $event) {
11 // ...
12 }));
13}

就像佇列任務一樣,可以使用 onConnectiononQueuedelay 等方法來自訂放入佇列之 Listener 的執行:

1Event::listen(queueable(function (PodcastProcessed $event) {
2 // ...
3})->onConnection('redis')->onQueue('podcasts')->delay(now()->addSeconds(10)));
1Event::listen(queueable(function (PodcastProcessed $event) {
2 // ...
3})->onConnection('redis')->onQueue('podcasts')->delay(now()->addSeconds(10)));

若想處理執行失敗的匿名佇列 Listener,可在定義 queueable Listener時提供一個閉包給catch方法。這個閉包會收到 Event 實體以及一個導致 Listener 失敗的Throwable` 實體:

1use App\Events\PodcastProcessed;
2use function Illuminate\Events\queueable;
3use Illuminate\Support\Facades\Event;
4use Throwable;
5 
6Event::listen(queueable(function (PodcastProcessed $event) {
7 // ...
8})->catch(function (PodcastProcessed $event, Throwable $e) {
9 // 佇列執行的 Listener 執行失敗...
10}));
1use App\Events\PodcastProcessed;
2use function Illuminate\Events\queueable;
3use Illuminate\Support\Facades\Event;
4use Throwable;
5 
6Event::listen(queueable(function (PodcastProcessed $event) {
7 // ...
8})->catch(function (PodcastProcessed $event, Throwable $e) {
9 // 佇列執行的 Listener 執行失敗...
10}));

萬用字元 Event Listener

可以使用 * 作為萬用字元(Wildcard)參數來註冊 Listener,這樣我們就可以在同一個 Listener 上處理多個 Event。萬用字元 Listener 會🉑️事件名稱作為其第一個引數,而整個 Event 資料陣列則為其第二個引數:

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

Event Discovery

除了在 EventServiceProvider$listen 陣列中手動指定 Listener 以外,還可以啟用 Event Discovery(Event 發現)。當啟用 Event Discovery 時,Laravel 會搜尋專案的 Listeners 目錄來自動找到並註冊你的 Event 與 Listener。此外,列在 EventServiceProvider 中顯式定義的 Event 還是會被註冊。

Laravel 會使用 PHP 的 Reflection 服務來搜尋 Listener 類別以尋找 Event Listener。當 Laravel 找到名稱以 handle__invoke 開頭的 Listener 類別方法時,Laravel 會從該方法簽章(Signature)上的型別提示(Type-Hint)中取得 Event,並將該方法註冊為該 Event 的 Listener:

1use App\Events\PodcastProcessed;
2 
3class SendPodcastNotification
4{
5 /**
6 * Handle the given event.
7 */
8 public function handle(PodcastProcessed $event): void
9 {
10 // ...
11 }
12}
1use App\Events\PodcastProcessed;
2 
3class SendPodcastNotification
4{
5 /**
6 * Handle the given event.
7 */
8 public function handle(PodcastProcessed $event): void
9 {
10 // ...
11 }
12}

Event Discovery 預設是關閉的,但可以在 EventServiceProvider 上複寫 shouldDiscoverEvents 方法來啟用:

1/**
2 * Determine if events and listeners should be automatically discovered.
3 */
4public function shouldDiscoverEvents(): bool
5{
6 return true;
7}
1/**
2 * Determine if events and listeners should be automatically discovered.
3 */
4public function shouldDiscoverEvents(): bool
5{
6 return true;
7}

預設情況下,會掃描專案 app/Listeners 目錄下的所有 Listener。若想定義其他要掃描的目錄,可在 EventServiceProvider 上複寫 discoverEventsWithin 方法:

1/**
2 * Get the listener directories that should be used to discover events.
3 *
4 * @return array<int, string>
5 */
6protected function discoverEventsWithin(): array
7{
8 return [
9 $this->app->path('Listeners'),
10 ];
11}
1/**
2 * Get the listener directories that should be used to discover events.
3 *
4 * @return array<int, string>
5 */
6protected function discoverEventsWithin(): array
7{
8 return [
9 $this->app->path('Listeners'),
10 ];
11}

在正式環境下使用 Event Discovery

正式環境(Production)中,讓 Laravel 在每個 Request 上都掃描所有 Listener 很沒效率。因此,在部署過程,請記得執行 event:cache Artisan 指令來為專案的所有 Event 與 Listener 建立一個快取資訊清單(Cache Manifest)。Laravel 會使用這個資訊清單來加快 Event 的註冊流程。可使用 event:clear 來清除該快取。

定義 Event

Event 類別基本上就是一個資料容器,用來保存與該 Event 有關的資訊。舉例來說,假設有個會接收 Eloquent ORM 物件的 App\Events\OrderShipped Event:

1<?php
2 
3namespace App\Events;
4 
5use App\Models\Order;
6use Illuminate\Broadcasting\InteractsWithSockets;
7use Illuminate\Foundation\Events\Dispatchable;
8use Illuminate\Queue\SerializesModels;
9 
10class OrderShipped
11{
12 use Dispatchable, InteractsWithSockets, SerializesModels;
13 
14 /**
15 * Create a new event instance.
16 */
17 public function __construct(
18 public Order $order,
19 ) {}
20}
1<?php
2 
3namespace App\Events;
4 
5use App\Models\Order;
6use Illuminate\Broadcasting\InteractsWithSockets;
7use Illuminate\Foundation\Events\Dispatchable;
8use Illuminate\Queue\SerializesModels;
9 
10class OrderShipped
11{
12 use Dispatchable, InteractsWithSockets, SerializesModels;
13 
14 /**
15 * Create a new event instance.
16 */
17 public function __construct(
18 public Order $order,
19 ) {}
20}

就像這樣,這個 Event 類別中並不包含邏輯。這個類別只是已付款訂單 App\Models\Order 實體的容器而已。若要使用 PHP 的 serialize 方法序列化這個 Event 物件時 (如:[佇列 Listener] 會序列化 Event),這個 Event 使用的 SerializesModels Trait 會妥善序列化所有的 Eloquent Model。

定義 Listener

接著,來看看要給我們的範例 Event 使用的 Listener。Event Listener 會在 handle 方法中接收 Event 實體。event:generatemake:listener Artisan 指令會自動載入適當的 Event 類別,並在 handle 方法上型別提示這個 Event。在 handle 方法中,我們就可以針對該 Event 回應適當的動作:

1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderShipped;
6 
7class SendShipmentNotification
8{
9 /**
10 * Create the event listener.
11 */
12 public function __construct()
13 {
14 // ...
15 }
16 
17 /**
18 * Handle the event.
19 */
20 public function handle(OrderShipped $event): void
21 {
22 // Access the order using $event->order...
23 }
24}
1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderShipped;
6 
7class SendShipmentNotification
8{
9 /**
10 * Create the event listener.
11 */
12 public function __construct()
13 {
14 // ...
15 }
16 
17 /**
18 * Handle the event.
19 */
20 public function handle(OrderShipped $event): void
21 {
22 // Access the order using $event->order...
23 }
24}
lightbulb

也可以在 Event Listener 的 Constructor(建構函式) 中型別提示任何的相依性。所有的 Event Listener 都會使用 Laravel Service Provider 解析,所以這些相依性也會自動被插入。

停止 Event 的傳播(Propagation)

有時候,我們可能會想停止將某個 Event 傳播(Propagation)到另一個 Listener 上。若要停止傳播,只要在 Listener 的 handle 方法上回傳 false 即可。

放入佇列的 Event Listener

若你的 Listener 要處理一些很慢的任務 (如寄送 E-Mail 或產生 HTTP Request),則 Listener 放入佇列可獲得許多好處。在使用佇列 Listener 前,請先確定已設定佇列,並在伺服器或本機開發環境上開啟一個 Queue Worker(佇列背景工作程式)

要將 Listener 指定為放在佇列裡執行,請在該 Listener 類別上加上 ShouldQueue 介面。由 event:generatemake:listener Artisan 指令產生的 Listener 都已先將這個介面匯入到目前的 Namespace(命名空間) 下了,因此我們可以直接使用該介面:

1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderShipped;
6use Illuminate\Contracts\Queue\ShouldQueue;
7 
8class SendShipmentNotification implements ShouldQueue
9{
10 // ...
11}
1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderShipped;
6use Illuminate\Contracts\Queue\ShouldQueue;
7 
8class SendShipmentNotification implements ShouldQueue
9{
10 // ...
11}

就這樣!之後,當這個 Listener 要處理的 Event 被分派(Dispatch)後,Event Dispatcher(分派程式) 就會自動使用 Laravel 的佇列系統來將這個 Listener 放入佇列。若佇列在執行該 Listener 時沒有擲回(Throw)任何 Exception,則該佇列任務會在執行完畢後自動刪除。

自訂佇列連線與佇列名稱

若想自訂 Event Listener 的佇列連線、佇列名稱、或是佇列延遲時間(Delay Time),可在 Listener 類別上定義 $connection$queue$delay 等屬性:

1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderShipped;
6use Illuminate\Contracts\Queue\ShouldQueue;
7 
8class SendShipmentNotification implements ShouldQueue
9{
10 /**
11 * The name of the connection the job should be sent to.
12 *
13 * @var string|null
14 */
15 public $connection = 'sqs';
16 
17 /**
18 * The name of the queue the job should be sent to.
19 *
20 * @var string|null
21 */
22 public $queue = 'listeners';
23 
24 /**
25 * The time (seconds) before the job should be processed.
26 *
27 * @var int
28 */
29 public $delay = 60;
30}
1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderShipped;
6use Illuminate\Contracts\Queue\ShouldQueue;
7 
8class SendShipmentNotification implements ShouldQueue
9{
10 /**
11 * The name of the connection the job should be sent to.
12 *
13 * @var string|null
14 */
15 public $connection = 'sqs';
16 
17 /**
18 * The name of the queue the job should be sent to.
19 *
20 * @var string|null
21 */
22 public $queue = 'listeners';
23 
24 /**
25 * The time (seconds) before the job should be processed.
26 *
27 * @var int
28 */
29 public $delay = 60;
30}

若想在執行階段定義 Listener 的佇列連線或佇列名稱,可以在 Listener 上定義 viaConnectionviaQueue 方法:

1/**
2 * Get the name of the listener's queue connection.
3 */
4public function viaConnection(): string
5{
6 return 'sqs';
7}
8 
9/**
10 * Get the name of the listener's queue.
11 */
12public function viaQueue(): string
13{
14 return 'listeners';
15}
1/**
2 * Get the name of the listener's queue connection.
3 */
4public function viaConnection(): string
5{
6 return 'sqs';
7}
8 
9/**
10 * Get the name of the listener's queue.
11 */
12public function viaQueue(): string
13{
14 return 'listeners';
15}

有條件地將 Listener 放入佇列

有時候,我們可能需要依據一些只有在執行階段才能取得的資料來判斷是否要將 Listener 放入佇列。若要在執行階段判斷是否將 Listner 放入佇列,可在 Listner 中新增一個 shouldQueue 方法來判斷。若 shouldQueue 方法回傳 false,則該 Listener 不會被執行:

1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderCreated;
6use Illuminate\Contracts\Queue\ShouldQueue;
7 
8class RewardGiftCard implements ShouldQueue
9{
10 /**
11 * Reward a gift card to the customer.
12 */
13 public function handle(OrderCreated $event): void
14 {
15 // ...
16 }
17 
18 /**
19 * Determine whether the listener should be queued.
20 */
21 public function shouldQueue(OrderCreated $event): bool
22 {
23 return $event->order->subtotal >= 5000;
24 }
25}
1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderCreated;
6use Illuminate\Contracts\Queue\ShouldQueue;
7 
8class RewardGiftCard implements ShouldQueue
9{
10 /**
11 * Reward a gift card to the customer.
12 */
13 public function handle(OrderCreated $event): void
14 {
15 // ...
16 }
17 
18 /**
19 * Determine whether the listener should be queued.
20 */
21 public function shouldQueue(OrderCreated $event): bool
22 {
23 return $event->order->subtotal >= 5000;
24 }
25}

手動使用佇列

若有需要手動存取某個 Listener 底層佇列任務的 deleterelease 方法,可使用 Illuminate\Queue\InteractsWithQueue Trait。在產生的 Listener 上已預設匯入了這個 Trait。有了 InteractsWithQueue 就可以存取這些方法:

1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderShipped;
6use Illuminate\Contracts\Queue\ShouldQueue;
7use Illuminate\Queue\InteractsWithQueue;
8 
9class SendShipmentNotification implements ShouldQueue
10{
11 use InteractsWithQueue;
12 
13 /**
14 * Handle the event.
15 */
16 public function handle(OrderShipped $event): void
17 {
18 if (true) {
19 $this->release(30);
20 }
21 }
22}
1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderShipped;
6use Illuminate\Contracts\Queue\ShouldQueue;
7use Illuminate\Queue\InteractsWithQueue;
8 
9class SendShipmentNotification implements ShouldQueue
10{
11 use InteractsWithQueue;
12 
13 /**
14 * Handle the event.
15 */
16 public function handle(OrderShipped $event): void
17 {
18 if (true) {
19 $this->release(30);
20 }
21 }
22}

佇列的 Event Listener 與資料庫 Transaction

當 Event Listener 是在資料庫 Transaction 內分派(Dispatch)的時候,這個 Listner 可能會在資料庫 Transaction 被 Commit 前就被佇列進行處理了。發生這種情況時,在資料庫 Transaction 期間對 Model 或資料庫記錄所做出的更新可能都還未反應到資料庫內。另外,所有在 Transaction 期間新增的 Model 或資料庫記錄也可能還未出現在資料庫內。若 Listner 有依賴這些 Model 的話,在處理分派該佇列 Listener 的任務時可能會出現未預期的錯誤。

若佇列連線的 after_commit 設定選項是 false,則我們還是可以通過在 Listner 類別上定義 $afterCommit 屬性來標示出這個佇列 Listener 應在資料庫 Transaction 被 Commit 後才可進行分派:

1<?php
2 
3namespace App\Listeners;
4 
5use Illuminate\Contracts\Queue\ShouldQueue;
6use Illuminate\Queue\InteractsWithQueue;
7 
8class SendShipmentNotification implements ShouldQueue
9{
10 use InteractsWithQueue;
11 
12 public $afterCommit = true;
13}
1<?php
2 
3namespace App\Listeners;
4 
5use Illuminate\Contracts\Queue\ShouldQueue;
6use Illuminate\Queue\InteractsWithQueue;
7 
8class SendShipmentNotification implements ShouldQueue
9{
10 use InteractsWithQueue;
11 
12 public $afterCommit = true;
13}
lightbulb

要瞭解更多有關這類問題的解決方法,請參考有關佇列任務與資料庫 Transaction 有關的說明文件。

處理失敗的任務

有時候,放入佇列的 Listener 可能會執行失敗。若該佇列的 Listener 達到最大 Queue Worker 所定義的最大嘗試次數,就會呼叫 Listener 上的 failed 方法。failed 方法會接收一個 Event 實體,以及導致失敗的 Throwable

1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderShipped;
6use Illuminate\Contracts\Queue\ShouldQueue;
7use Illuminate\Queue\InteractsWithQueue;
8use Throwable;
9 
10class SendShipmentNotification implements ShouldQueue
11{
12 use InteractsWithQueue;
13 
14 /**
15 * Handle the event.
16 */
17 public function handle(OrderShipped $event): void
18 {
19 // ...
20 }
21 
22 /**
23 * Handle a job failure.
24 */
25 public function failed(OrderShipped $event, Throwable $exception): void
26 {
27 // ...
28 }
29}
1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderShipped;
6use Illuminate\Contracts\Queue\ShouldQueue;
7use Illuminate\Queue\InteractsWithQueue;
8use Throwable;
9 
10class SendShipmentNotification implements ShouldQueue
11{
12 use InteractsWithQueue;
13 
14 /**
15 * Handle the event.
16 */
17 public function handle(OrderShipped $event): void
18 {
19 // ...
20 }
21 
22 /**
23 * Handle a job failure.
24 */
25 public function failed(OrderShipped $event, Throwable $exception): void
26 {
27 // ...
28 }
29}

指定佇列 Listener 的最大嘗試次數

若有某個佇列 Listener 遇到錯誤,我們通常不會想讓這個 Listener 一直重試。因此,Laravel 提供了多種定義 Listener 重試次數的方法。

可以在 Listener 類別中定義 $tries 屬性來指定要嘗試多少次後才將其視為執行失敗:

1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderShipped;
6use Illuminate\Contracts\Queue\ShouldQueue;
7use Illuminate\Queue\InteractsWithQueue;
8 
9class SendShipmentNotification implements ShouldQueue
10{
11 use InteractsWithQueue;
12 
13 /**
14 * The number of times the queued listener may be attempted.
15 *
16 * @var int
17 */
18 public $tries = 5;
19}
1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderShipped;
6use Illuminate\Contracts\Queue\ShouldQueue;
7use Illuminate\Queue\InteractsWithQueue;
8 
9class SendShipmentNotification implements ShouldQueue
10{
11 use InteractsWithQueue;
12 
13 /**
14 * The number of times the queued listener may be attempted.
15 *
16 * @var int
17 */
18 public $tries = 5;
19}

除了定義 Listener 重試多少次要視為失敗以外,也可以限制 Listener 嘗試執行的時間長度。這樣一來,在指定的時間範圍內,Listener 就可以不斷重試。若要定義最長可重試時間,請在 Listener 類別中定義一個 retryUntil 方法。該方法應回傳 DateTime 實體:

1use DateTime;
2 
3/**
4 * Determine the time at which the listener should timeout.
5 */
6public function retryUntil(): DateTime
7{
8 return now()->addMinutes(5);
9}
1use DateTime;
2 
3/**
4 * Determine the time at which the listener should timeout.
5 */
6public function retryUntil(): DateTime
7{
8 return now()->addMinutes(5);
9}

分派 Event

若要分派 Event,可呼叫該 Event 上的靜態 dispatch 方法。這個方法由 Illuminate\Foundation\Events\Dispatchable Trait 提供。任何傳入 dispatch 方法的引數會被傳給 Event 的 Constructor:

1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Events\OrderShipped;
6use App\Http\Controllers\Controller;
7use App\Models\Order;
8use Illuminate\Http\Request;
9use Illuminate\Http\Response;
10 
11class OrderShipmentController extends Controller
12{
13 /**
14 * Ship the given order.
15 */
16 public function store(Request $request): Response
17 {
18 $order = Order::findOrFail($request->order_id);
19 
20 // 訂單出貨邏輯...
21 
22 OrderShipped::dispatch($order);
23 
24 return response()->noContent();
25 }
26}
1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Events\OrderShipped;
6use App\Http\Controllers\Controller;
7use App\Models\Order;
8use Illuminate\Http\Request;
9use Illuminate\Http\Response;
10 
11class OrderShipmentController extends Controller
12{
13 /**
14 * Ship the given order.
15 */
16 public function store(Request $request): Response
17 {
18 $order = Order::findOrFail($request->order_id);
19 
20 // 訂單出貨邏輯...
21 
22 OrderShipped::dispatch($order);
23 
24 return response()->noContent();
25 }
26}

若想要有條件地分派 Event,可使用 dispatchIf dispatchUnless` 方法:

1OrderShipped::dispatchIf($condition, $order);
2 
3OrderShipped::dispatchUnless($condition, $order);
1OrderShipped::dispatchIf($condition, $order);
2 
3OrderShipped::dispatchUnless($condition, $order);
lightbulb

在測試時,若能在不實際觸發 Listener 的情況下判斷是否有分派特定 Event 會很實用。Laravel 的內建測試輔助函式就能讓我們在不實際觸發 Listener 的情況下分派 Event。

Event Subscriber

撰寫 Event Subscriber

Event Subscriber 是一種類別,在 Subscriber 類別內可以訂閱(Subscribe)多個 Event,讓我們能在單一類別中定義多個 Event 的處理程式(Handler)。Subscriber 應定義 subscribe 方法,會傳入一個 Event Dispatcher 實體給該方法。我們可以在給定的 Dispatcher 上呼叫 listen 方法來註冊 Event Listener:

1<?php
2 
3namespace App\Listeners;
4 
5use Illuminate\Auth\Events\Login;
6use Illuminate\Auth\Events\Logout;
7use Illuminate\Events\Dispatcher;
8 
9class UserEventSubscriber
10{
11 /**
12 * Handle user login events.
13 */
14 public function handleUserLogin(string $event): void {}
15 
16 /**
17 * Handle user logout events.
18 */
19 public function handleUserLogout(string $event): void {}
20 
21 /**
22 * Register the listeners for the subscriber.
23 */
24 public function subscribe(Dispatcher $events): void
25 {
26 $events->listen(
27 Login::class,
28 [UserEventSubscriber::class, 'handleUserLogin']
29 );
30 
31 $events->listen(
32 Logout::class,
33 [UserEventSubscriber::class, 'handleUserLogout']
34 );
35 }
36}
1<?php
2 
3namespace App\Listeners;
4 
5use Illuminate\Auth\Events\Login;
6use Illuminate\Auth\Events\Logout;
7use Illuminate\Events\Dispatcher;
8 
9class UserEventSubscriber
10{
11 /**
12 * Handle user login events.
13 */
14 public function handleUserLogin(string $event): void {}
15 
16 /**
17 * Handle user logout events.
18 */
19 public function handleUserLogout(string $event): void {}
20 
21 /**
22 * Register the listeners for the subscriber.
23 */
24 public function subscribe(Dispatcher $events): void
25 {
26 $events->listen(
27 Login::class,
28 [UserEventSubscriber::class, 'handleUserLogin']
29 );
30 
31 $events->listen(
32 Logout::class,
33 [UserEventSubscriber::class, 'handleUserLogout']
34 );
35 }
36}

在 Subscriber 內可以定義 Event Listener 方法,但比起這麼做,在 Subscriber 的 subscribe 方法內回傳一組包含 Event 與方法名稱的陣列應該會更方便。在註冊 Event Listener 時,Laravel 會自動判斷該 Subscriber 的類別名稱:

1<?php
2 
3namespace App\Listeners;
4 
5use Illuminate\Auth\Events\Login;
6use Illuminate\Auth\Events\Logout;
7use Illuminate\Events\Dispatcher;
8 
9class UserEventSubscriber
10{
11 /**
12 * Handle user login events.
13 */
14 public function handleUserLogin(string $event): void {}
15 
16 /**
17 * Handle user logout events.
18 */
19 public function handleUserLogout(string $event): void {}
20 
21 /**
22 * Register the listeners for the subscriber.
23 *
24 * @return array<string, string>
25 */
26 public function subscribe(Dispatcher $events): array
27 {
28 return [
29 Login::class => 'handleUserLogin',
30 Logout::class => 'handleUserLogout',
31 ];
32 }
33}
1<?php
2 
3namespace App\Listeners;
4 
5use Illuminate\Auth\Events\Login;
6use Illuminate\Auth\Events\Logout;
7use Illuminate\Events\Dispatcher;
8 
9class UserEventSubscriber
10{
11 /**
12 * Handle user login events.
13 */
14 public function handleUserLogin(string $event): void {}
15 
16 /**
17 * Handle user logout events.
18 */
19 public function handleUserLogout(string $event): void {}
20 
21 /**
22 * Register the listeners for the subscriber.
23 *
24 * @return array<string, string>
25 */
26 public function subscribe(Dispatcher $events): array
27 {
28 return [
29 Login::class => 'handleUserLogin',
30 Logout::class => 'handleUserLogout',
31 ];
32 }
33}

註冊 Event Subscriber

寫好 Subscriber 後,就可以將 Subscriber 註冊到 Dispatcher 上了。可以使用 EventServiceProvider$subscribe 屬性來註冊 Subscriber。舉例來說,我們來將 UserEventSubscriber 加到這個列表上:

1<?php
2 
3namespace App\Providers;
4 
5use App\Listeners\UserEventSubscriber;
6use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
7 
8class EventServiceProvider extends ServiceProvider
9{
10 /**
11 * The event listener mappings for the application.
12 *
13 * @var array
14 */
15 protected $listen = [
16 // ...
17 ];
18 
19 /**
20 * The subscriber classes to register.
21 *
22 * @var array
23 */
24 protected $subscribe = [
25 UserEventSubscriber::class,
26 ];
27}
1<?php
2 
3namespace App\Providers;
4 
5use App\Listeners\UserEventSubscriber;
6use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
7 
8class EventServiceProvider extends ServiceProvider
9{
10 /**
11 * The event listener mappings for the application.
12 *
13 * @var array
14 */
15 protected $listen = [
16 // ...
17 ];
18 
19 /**
20 * The subscriber classes to register.
21 *
22 * @var array
23 */
24 protected $subscribe = [
25 UserEventSubscriber::class,
26 ];
27}

測試

在測試會分派 Event 的程式時,可以讓 Laravel 不要真的執行該 Event 的 Listener。我們可以直接測試 Event 的 Listener,將 Listener 的測試與分派該 Event 的程式碼測試分開。當然,若要測試 Listener,需要在測試中先初始化一個 Listener 實體,並直接呼叫 handle 方法。

使用 Event Facade 的 fake 方法,就可避免執行真正的 Listener,在測試中執行程式碼,然後使用 assertDispatchedassertNotDispatchedassertNothingDispatched 等方法來判斷程式分派了哪些 Event:

1<?php
2 
3namespace Tests\Feature;
4 
5use App\Events\OrderFailedToShip;
6use App\Events\OrderShipped;
7use Illuminate\Support\Facades\Event;
8use Tests\TestCase;
9 
10class ExampleTest extends TestCase
11{
12 /**
13 * Test order shipping.
14 */
15 public function test_orders_can_be_shipped(): void
16 {
17 Event::fake();
18 
19 // 進行訂單出貨...
20 
21 // 判斷是否已分派 Event...
22 Event::assertDispatched(OrderShipped::class);
23 
24 // 判斷是否已分派兩次 Event...
25 Event::assertDispatched(OrderShipped::class, 2);
26 
27 // 判斷 Event 是否未被分派...
28 Event::assertNotDispatched(OrderFailedToShip::class);
29 
30 // 判斷是否無 Event 被分派...
31 Event::assertNothingDispatched();
32 }
33}
1<?php
2 
3namespace Tests\Feature;
4 
5use App\Events\OrderFailedToShip;
6use App\Events\OrderShipped;
7use Illuminate\Support\Facades\Event;
8use Tests\TestCase;
9 
10class ExampleTest extends TestCase
11{
12 /**
13 * Test order shipping.
14 */
15 public function test_orders_can_be_shipped(): void
16 {
17 Event::fake();
18 
19 // 進行訂單出貨...
20 
21 // 判斷是否已分派 Event...
22 Event::assertDispatched(OrderShipped::class);
23 
24 // 判斷是否已分派兩次 Event...
25 Event::assertDispatched(OrderShipped::class, 2);
26 
27 // 判斷 Event 是否未被分派...
28 Event::assertNotDispatched(OrderFailedToShip::class);
29 
30 // 判斷是否無 Event 被分派...
31 Event::assertNothingDispatched();
32 }
33}

可以傳入一個閉包給 assertDispatchedassertNotDispatched 方法,來判斷某個 Event 是否通過給定的「真值測試 (Truth Test)」。若分派的 Event 中至少有一個 Event 通過給定的真值測試,則該 Assertion 會被視為成功:

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

若只想判斷某個 Event Listener 是否有在監聽給定的 Event,可使用 assertListening 方法:

1Event::assertListening(
2 OrderShipped::class,
3 SendShipmentNotification::class
4);
1Event::assertListening(
2 OrderShipped::class,
3 SendShipmentNotification::class
4);
exclamation

呼叫 Event::fake() 後,就不會執行 Event Listener。因此,若有測試使用的 Model Factory 仰賴於 Event,如在 Model 的 creating Event 上建立 UUID 等,請在使用完 Factory 之後 再呼叫 Event::fake()

模擬一部分的 Event

若只想為一部分 Event 來 Fake Event Listener,則可將這些 Event 傳入fakefakeFor 方法:

1/**
2 * Test order process.
3 */
4public function test_orders_can_be_processed(): void
5{
6 Event::fake([
7 OrderCreated::class,
8 ]);
9 
10 $order = Order::factory()->create();
11 
12 Event::assertDispatched(OrderCreated::class);
13 
14 // 其他 Event 會被正常分派...
15 $order->update([...]);
16}
1/**
2 * Test order process.
3 */
4public function test_orders_can_be_processed(): void
5{
6 Event::fake([
7 OrderCreated::class,
8 ]);
9 
10 $order = Order::factory()->create();
11 
12 Event::assertDispatched(OrderCreated::class);
13 
14 // 其他 Event 會被正常分派...
15 $order->update([...]);
16}

也可以使用 except 方法來 Fake 除了一組特定 Event 外的所有 Event:

1Event::fake()->except([
2 OrderCreated::class,
3]);
1Event::fake()->except([
2 OrderCreated::class,
3]);

限定範圍地模擬 Event

若只想未一部分的測試 Fake Event Listener,則可使用 fakeFor 方法:

1<?php
2 
3namespace Tests\Feature;
4 
5use App\Events\OrderCreated;
6use App\Models\Order;
7use Illuminate\Support\Facades\Event;
8use Tests\TestCase;
9 
10class ExampleTest extends TestCase
11{
12 /**
13 * Test order process.
14 */
15 public function test_orders_can_be_processed(): void
16 {
17 $order = Event::fakeFor(function () {
18 $order = Order::factory()->create();
19 
20 Event::assertDispatched(OrderCreated::class);
21 
22 return $order;
23 });
24 
25 // Event 會被正常分派,Observer 會執行...
26 $order->update([...]);
27 }
28}
1<?php
2 
3namespace Tests\Feature;
4 
5use App\Events\OrderCreated;
6use App\Models\Order;
7use Illuminate\Support\Facades\Event;
8use Tests\TestCase;
9 
10class ExampleTest extends TestCase
11{
12 /**
13 * Test order process.
14 */
15 public function test_orders_can_be_processed(): void
16 {
17 $order = Event::fakeFor(function () {
18 $order = Order::factory()->create();
19 
20 Event::assertDispatched(OrderCreated::class);
21 
22 return $order;
23 });
24 
25 // Event 會被正常分派,Observer 會執行...
26 $order->update([...]);
27 }
28}
翻譯進度
100% 已翻譯
更新時間:
2024年6月30日 上午8:26:00 [世界標準時間]
翻譯人員:
  • cornch
幫我們翻譯此頁

留言

尚無留言

“Laravel” is a Trademark of Taylor Otwell.
The source documentation is released under MIT license. See laravel/docs on GitHub for details.
The translated documentations are released under MIT license. See cornch/laravel-docs-l10n on GitHub for details.