事件 - Event
簡介
Laravel 的 Event 提供了一種簡單的 Observer 設計模式實作,能讓你註冊與監聽程式內發生的多種事件。Event 類別一般儲存在 app/Events
目錄下,而 Listener 則一般儲存在 app/Listeners
目錄。若在專案內沒看到這些目錄的話請別擔心,在使用 Artisan 指令產生 Event 跟 Listener 的時候會自動建立。
Event 是以各種層面解耦程式的好方法,因為一個 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;34/**5 * The event listener mappings for the application.6 *7 * @var array<class-string, array<int, class-string>>8 */9protected $listen = [10 OrderShipped::class => [11 SendShipmentNotification::class,12 ],13];
1use App\Events\OrderShipped;2use App\Listeners\SendShipmentNotification;34/**5 * The event listener mappings for the application.6 *7 * @var array<class-string, array<int, class-string>>8 */9protected $listen = [10 OrderShipped::class => [11 SendShipmentNotification::class,12 ],13];
可以使用 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:event
與 make:listener
Artisan 指令來產生個別的 Event 與 Listener:
1php artisan make:event PodcastProcessed23php artisan make:listener SendPodcastNotification --event=PodcastProcessed
1php artisan make:event PodcastProcessed23php artisan make:listener SendPodcastNotification --event=PodcastProcessed
手動註冊 Event
一般來說,Event 應在 EventServiceProvider
的 $listen
陣列中註冊。不過,也可以在 EventServiceProvider
的 boot
方法中手動註冊基於類別或閉包的 Listener:
1use App\Events\PodcastProcessed;2use App\Listeners\SendPodcastNotification;3use Illuminate\Support\Facades\Event;45/**6 * Register any other events for your application.7 */8public function boot(): void9{10 Event::listen(11 PodcastProcessed::class,12 [SendPodcastNotification::class, 'handle']13 );1415 Event::listen(function (PodcastProcessed $event) {16 // ...17 });18}
1use App\Events\PodcastProcessed;2use App\Listeners\SendPodcastNotification;3use Illuminate\Support\Facades\Event;45/**6 * Register any other events for your application.7 */8public function boot(): void9{10 Event::listen(11 PodcastProcessed::class,12 [SendPodcastNotification::class, 'handle']13 );1415 Event::listen(function (PodcastProcessed $event) {16 // ...17 });18}
可放入佇列的匿名 Event Listener
在註冊基於閉包的 Event Listener 時,可以將該 Listener 閉包以 Illuminate\Events\queueable
函式包裝起來,以指示 Laravel 使用 Queue 來執行這個 Listener:
1use App\Events\PodcastProcessed;2use function Illuminate\Events\queueable;3use Illuminate\Support\Facades\Event;45/**6 * Register any other events for your application.7 */8public function boot(): void9{10 Event::listen(queueable(function (PodcastProcessed $event) {11 // ...12 }));13}
1use App\Events\PodcastProcessed;2use function Illuminate\Events\queueable;3use Illuminate\Support\Facades\Event;45/**6 * Register any other events for your application.7 */8public function boot(): void9{10 Event::listen(queueable(function (PodcastProcessed $event) {11 // ...12 }));13}
就像佇列任務一樣,可以使用 onConnection
、onQueue
、delay
等方法來自訂放入佇列之 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;56Event::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;56Event::listen(queueable(function (PodcastProcessed $event) {7 // ...8})->catch(function (PodcastProcessed $event, Throwable $e) {9 // 佇列執行的 Listener 執行失敗...10}));
萬用字元 Event Listener
可以使用 *
作為萬用字元參數來註冊 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 Discovery 時,Laravel 會搜尋專案的 Listeners
目錄來自動找到並註冊你的 Event 與 Listener。此外,列在 EventServiceProvider
中顯式定義的 Event 還是會被註冊。
Laravel 會使用 PHP 的 Reflection 服務來搜尋 Listener 類別以尋找 Event Listener。當 Laravel 找到名稱以 handle
或 __invoke
開頭的 Listener 類別方法時,Laravel 會從該方法簽章上的型別提示中取得 Event,並將該方法註冊為該 Event 的 Listener:
1use App\Events\PodcastProcessed;23class SendPodcastNotification4{5 /**6 * Handle the given event.7 */8 public function handle(PodcastProcessed $event): void9 {10 // ...11 }12}
1use App\Events\PodcastProcessed;23class SendPodcastNotification4{5 /**6 * Handle the given event.7 */8 public function handle(PodcastProcessed $event): void9 {10 // ...11 }12}
Event Discovery 預設是關閉的,但可以在 EventServiceProvider
上複寫 shouldDiscoverEvents
方法來啟用:
1/**2 * Determine if events and listeners should be automatically discovered.3 */4public function shouldDiscoverEvents(): bool5{6 return true;7}
1/**2 * Determine if events and listeners should be automatically discovered.3 */4public function shouldDiscoverEvents(): bool5{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(): array7{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(): array7{8 return [9 $this->app->path('Listeners'),10 ];11}
在正式環境下使用 Event Discovery
在正式環境中,讓 Laravel 在每個 Request 上都掃描所有 Listener 很沒效率。因此,在部署過程,請記得執行 event:cache
Artisan 指令來為專案的所有 Event 與 Listener 建立一個快取資訊清單。Laravel 會使用這個資訊清單來加快 Event 的註冊流程。可使用 event:clear
來清除該快取。
定義 Event
Event 類別基本上就是一個資料容器,用來保存與該 Event 有關的資訊。舉例來說,假設有個會接收 Eloquent ORM 物件的 App\Events\OrderShipped
Event:
1<?php23namespace App\Events;45use App\Models\Order;6use Illuminate\Broadcasting\InteractsWithSockets;7use Illuminate\Foundation\Events\Dispatchable;8use Illuminate\Queue\SerializesModels;910class OrderShipped11{12 use Dispatchable, InteractsWithSockets, SerializesModels;1314 /**15 * Create a new event instance.16 */17 public function __construct(18 public Order $order,19 ) {}20}
1<?php23namespace App\Events;45use App\Models\Order;6use Illuminate\Broadcasting\InteractsWithSockets;7use Illuminate\Foundation\Events\Dispatchable;8use Illuminate\Queue\SerializesModels;910class OrderShipped11{12 use Dispatchable, InteractsWithSockets, SerializesModels;1314 /**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:generate
與 make:listener
Artisan 指令會自動載入適當的 Event 類別,並在 handle
方法上型別提示這個 Event。在 handle
方法中,我們就可以針對該 Event 回應適當的動作:
1<?php23namespace App\Listeners;45use App\Events\OrderShipped;67class SendShipmentNotification8{9 /**10 * Create the event listener.11 */12 public function __construct()13 {14 // ...15 }1617 /**18 * Handle the event.19 */20 public function handle(OrderShipped $event): void21 {22 // Access the order using $event->order...23 }24}
1<?php23namespace App\Listeners;45use App\Events\OrderShipped;67class SendShipmentNotification8{9 /**10 * Create the event listener.11 */12 public function __construct()13 {14 // ...15 }1617 /**18 * Handle the event.19 */20 public function handle(OrderShipped $event): void21 {22 // Access the order using $event->order...23 }24}
也可以在 Event Listener 的 Constructor 中型別提示任何的相依性。所有的 Event Listener 都會使用 Laravel Service Provider 解析,所以這些相依性也會自動被插入。
停止 Event 的傳播
有時候,我們可能會想停止將某個 Event 傳播到另一個 Listener 上。若要停止傳播,只要在 Listener 的 handle
方法上回傳 false
即可。
放入佇列的 Event Listener
若你的 Listener 要處理一些很慢的任務 (如寄送 E-Mail 或產生 HTTP Request),則 Listener 放入佇列可獲得許多好處。在使用佇列 Listener 前,請先確定已設定佇列,並在伺服器或本機開發環境上開啟一個 Queue Worker。
要將 Listener 指定為放在佇列裡執行,請在該 Listener 類別上加上 ShouldQueue
介面。由 event:generate
與 make:listener
Artisan 指令產生的 Listener 都已先將這個介面匯入到目前的 Namespace 下了,因此我們可以直接使用該介面:
1<?php23namespace App\Listeners;45use App\Events\OrderShipped;6use Illuminate\Contracts\Queue\ShouldQueue;78class SendShipmentNotification implements ShouldQueue9{10 // ...11}
1<?php23namespace App\Listeners;45use App\Events\OrderShipped;6use Illuminate\Contracts\Queue\ShouldQueue;78class SendShipmentNotification implements ShouldQueue9{10 // ...11}
就這樣!之後,當這個 Listener 要處理的 Event 被分派後,Event Dispatcher 就會自動使用 Laravel 的佇列系統來將這個 Listener 放入佇列。若佇列在執行該 Listener 時沒有擲回任何 Exception,則該佇列任務會在執行完畢後自動刪除。
自定佇列連線、名稱、與延遲
若想自訂 Event Listener 的佇列連線、佇列名稱、或是佇列延遲時間,可在 Listener 類別上定義 $connection
、$queue
、$delay
等屬性:
1<?php23namespace App\Listeners;45use App\Events\OrderShipped;6use Illuminate\Contracts\Queue\ShouldQueue;78class SendShipmentNotification implements ShouldQueue9{10 /**11 * The name of the connection the job should be sent to.12 *13 * @var string|null14 */15 public $connection = 'sqs';1617 /**18 * The name of the queue the job should be sent to.19 *20 * @var string|null21 */22 public $queue = 'listeners';2324 /**25 * The time (seconds) before the job should be processed.26 *27 * @var int28 */29 public $delay = 60;30}
1<?php23namespace App\Listeners;45use App\Events\OrderShipped;6use Illuminate\Contracts\Queue\ShouldQueue;78class SendShipmentNotification implements ShouldQueue9{10 /**11 * The name of the connection the job should be sent to.12 *13 * @var string|null14 */15 public $connection = 'sqs';1617 /**18 * The name of the queue the job should be sent to.19 *20 * @var string|null21 */22 public $queue = 'listeners';2324 /**25 * The time (seconds) before the job should be processed.26 *27 * @var int28 */29 public $delay = 60;30}
若想在執行階段定義 Listener 的佇列連線、佇列名稱、或是延遲,可以在 Listener 上定義 viaConnection
、viaQueue
、或 withDelay
方法:
1/**2 * Get the name of the listener's queue connection.3 */4public function viaConnection(): string5{6 return 'sqs';7}89/**10 * Get the name of the listener's queue.11 */12public function viaQueue(): string13{14 return 'listeners';15}1617/**18 * Get the number of seconds before the job should be processed.19 */20public function withDelay(OrderShipped $event): int21{22 return $event->highPriority ? 0 : 60;23}
1/**2 * Get the name of the listener's queue connection.3 */4public function viaConnection(): string5{6 return 'sqs';7}89/**10 * Get the name of the listener's queue.11 */12public function viaQueue(): string13{14 return 'listeners';15}1617/**18 * Get the number of seconds before the job should be processed.19 */20public function withDelay(OrderShipped $event): int21{22 return $event->highPriority ? 0 : 60;23}
有條件地將 Listener 放入佇列
有時候,我們可能需要依據一些只有在執行階段才能取得的資料來判斷是否要將 Listener 放入佇列。若要在執行階段判斷是否將 Listner 放入佇列,可在 Listner 中新增一個 shouldQueue
方法來判斷。若 shouldQueue
方法回傳 false
,則該 Listener 不會被執行:
1<?php23namespace App\Listeners;45use App\Events\OrderCreated;6use Illuminate\Contracts\Queue\ShouldQueue;78class RewardGiftCard implements ShouldQueue9{10 /**11 * Reward a gift card to the customer.12 */13 public function handle(OrderCreated $event): void14 {15 // ...16 }1718 /**19 * Determine whether the listener should be queued.20 */21 public function shouldQueue(OrderCreated $event): bool22 {23 return $event->order->subtotal >= 5000;24 }25}
1<?php23namespace App\Listeners;45use App\Events\OrderCreated;6use Illuminate\Contracts\Queue\ShouldQueue;78class RewardGiftCard implements ShouldQueue9{10 /**11 * Reward a gift card to the customer.12 */13 public function handle(OrderCreated $event): void14 {15 // ...16 }1718 /**19 * Determine whether the listener should be queued.20 */21 public function shouldQueue(OrderCreated $event): bool22 {23 return $event->order->subtotal >= 5000;24 }25}
手動使用佇列
若有需要手動存取某個 Listener 底層佇列任務的 delete
與 release
方法,可使用 Illuminate\Queue\InteractsWithQueue
Trait。在產生的 Listener 上已預設匯入了這個 Trait。有了 InteractsWithQueue
就可以存取這些方法:
1<?php23namespace App\Listeners;45use App\Events\OrderShipped;6use Illuminate\Contracts\Queue\ShouldQueue;7use Illuminate\Queue\InteractsWithQueue;89class SendShipmentNotification implements ShouldQueue10{11 use InteractsWithQueue;1213 /**14 * Handle the event.15 */16 public function handle(OrderShipped $event): void17 {18 if (true) {19 $this->release(30);20 }21 }22}
1<?php23namespace App\Listeners;45use App\Events\OrderShipped;6use Illuminate\Contracts\Queue\ShouldQueue;7use Illuminate\Queue\InteractsWithQueue;89class SendShipmentNotification implements ShouldQueue10{11 use InteractsWithQueue;1213 /**14 * Handle the event.15 */16 public function handle(OrderShipped $event): void17 {18 if (true) {19 $this->release(30);20 }21 }22}
佇列的 Event Listener 與資料庫 Transaction
當 Event Listener 是在資料庫 Transaction 內分派的時候,這個 Listner 可能會在資料庫 Transaction 被 Commit 前就被佇列進行處理了。發生這種情況時,在資料庫 Transaction 期間對 Model 或資料庫記錄所做出的更新可能都還未反應到資料庫內。另外,所有在 Transaction 期間新增的 Model 或資料庫記錄也可能還未出現在資料庫內。若 Listner 有依賴這些 Model 的話,在處理分派該佇列 Listener 的任務時可能會出現未預期的錯誤。
即使佇列連線的 after_commit
設定選項被設為 false
,還是可以通過在 Listener 類別上實作 ShouldHandleEventsAfterCommit
介面來讓 Laravel 知道要在所有開啟的資料庫 Transaction 被 Commit 後分派被放入佇列的 Listener:
1<?php23namespace App\Listeners;45use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;6use Illuminate\Contracts\Queue\ShouldQueue;7use Illuminate\Queue\InteractsWithQueue;89class SendShipmentNotification implements ShouldQueue, ShouldHandleEventsAfterCommit10{11 use InteractsWithQueue;12}
1<?php23namespace App\Listeners;45use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;6use Illuminate\Contracts\Queue\ShouldQueue;7use Illuminate\Queue\InteractsWithQueue;89class SendShipmentNotification implements ShouldQueue, ShouldHandleEventsAfterCommit10{11 use InteractsWithQueue;12}
要瞭解更多有關這類問題的解決方法,請參考有關佇列任務與資料庫 Transaction 有關的說明文件。
處理失敗的任務
有時候,放入佇列的 Listener 可能會執行失敗。若該佇列的 Listener 達到最大 Queue Worker 所定義的最大嘗試次數,就會呼叫 Listener 上的 failed
方法。failed
方法會接收一個 Event 實體,以及導致失敗的 Throwable
:
1<?php23namespace App\Listeners;45use App\Events\OrderShipped;6use Illuminate\Contracts\Queue\ShouldQueue;7use Illuminate\Queue\InteractsWithQueue;8use Throwable;910class SendShipmentNotification implements ShouldQueue11{12 use InteractsWithQueue;1314 /**15 * Handle the event.16 */17 public function handle(OrderShipped $event): void18 {19 // ...20 }2122 /**23 * Handle a job failure.24 */25 public function failed(OrderShipped $event, Throwable $exception): void26 {27 // ...28 }29}
1<?php23namespace App\Listeners;45use App\Events\OrderShipped;6use Illuminate\Contracts\Queue\ShouldQueue;7use Illuminate\Queue\InteractsWithQueue;8use Throwable;910class SendShipmentNotification implements ShouldQueue11{12 use InteractsWithQueue;1314 /**15 * Handle the event.16 */17 public function handle(OrderShipped $event): void18 {19 // ...20 }2122 /**23 * Handle a job failure.24 */25 public function failed(OrderShipped $event, Throwable $exception): void26 {27 // ...28 }29}
指定佇列 Listener 的最大嘗試次數
若有某個佇列 Listener 遇到錯誤,我們通常不會想讓這個 Listener 一直重試。因此,Laravel 提供了多種定義 Listener 重試次數的方法。
可以在 Listener 類別中定義 $tries
屬性來指定要嘗試多少次後才將其視為執行失敗:
1<?php23namespace App\Listeners;45use App\Events\OrderShipped;6use Illuminate\Contracts\Queue\ShouldQueue;7use Illuminate\Queue\InteractsWithQueue;89class SendShipmentNotification implements ShouldQueue10{11 use InteractsWithQueue;1213 /**14 * The number of times the queued listener may be attempted.15 *16 * @var int17 */18 public $tries = 5;19}
1<?php23namespace App\Listeners;45use App\Events\OrderShipped;6use Illuminate\Contracts\Queue\ShouldQueue;7use Illuminate\Queue\InteractsWithQueue;89class SendShipmentNotification implements ShouldQueue10{11 use InteractsWithQueue;1213 /**14 * The number of times the queued listener may be attempted.15 *16 * @var int17 */18 public $tries = 5;19}
除了定義 Listener 重試多少次要視為失敗以外,也可以限制 Listener 嘗試執行的時間長度。這樣一來,在指定的時間範圍內,Listener 就可以不斷重試。若要定義最長可重試時間,請在 Listener 類別中定義一個 retryUntil
方法。該方法應回傳 DateTime
實體:
1use DateTime;23/**4 * Determine the time at which the listener should timeout.5 */6public function retryUntil(): DateTime7{8 return now()->addMinutes(5);9}
1use DateTime;23/**4 * Determine the time at which the listener should timeout.5 */6public function retryUntil(): DateTime7{8 return now()->addMinutes(5);9}
分派 Event
若要分派 Event,可呼叫該 Event 上的靜態 dispatch
方法。這個方法由 Illuminate\Foundation\Events\Dispatchable
Trait 提供。任何傳入 dispatch
方法的引數會被傳給 Event 的 Constructor:
1<?php23namespace App\Http\Controllers;45use App\Events\OrderShipped;6use App\Http\Controllers\Controller;7use App\Models\Order;8use Illuminate\Http\RedirectResponse;9use Illuminate\Http\Request;1011class OrderShipmentController extends Controller12{13 /**14 * Ship the given order.15 */16 public function store(Request $request): RedirectResponse17 {18 $order = Order::findOrFail($request->order_id);1920 // 訂單出貨邏輯...2122 OrderShipped::dispatch($order);2324 return redirect('/orders');25 }26}
1<?php23namespace App\Http\Controllers;45use App\Events\OrderShipped;6use App\Http\Controllers\Controller;7use App\Models\Order;8use Illuminate\Http\RedirectResponse;9use Illuminate\Http\Request;1011class OrderShipmentController extends Controller12{13 /**14 * Ship the given order.15 */16 public function store(Request $request): RedirectResponse17 {18 $order = Order::findOrFail($request->order_id);1920 // 訂單出貨邏輯...2122 OrderShipped::dispatch($order);2324 return redirect('/orders');25 }26}
若想要有條件地分派 Event,可使用 dispatchIf
與
dispatchUnless` 方法:
1OrderShipped::dispatchIf($condition, $order);23OrderShipped::dispatchUnless($condition, $order);
1OrderShipped::dispatchIf($condition, $order);23OrderShipped::dispatchUnless($condition, $order);
在測試時,若能在不實際觸發 Listener 的情況下判斷是否有分派特定 Event 會很實用。Laravel 的內建測試輔助函式就能讓我們在不實際觸發 Listener 的情況下分派 Event。
在資料庫 Transaction 後分派 Event
有時候,你可能會想讓 Laravel 只在有效資料庫 Transaction 被 Commit 後才分派 Event。這時候,可以在 Event 類別上實作 ShouldDispatchAfterCommit
介面。
該介面會使 Laravel 等到資料庫 Transaction 被 Commit 後才分派 Event。若 Transaction 執行失敗,該 Event 則會被取消。若在分派 Event 時沒有正在進行的 Transaction,則該 Event 會立刻被分派:
1<?php23namespace App\Events;45use App\Models\Order;6use Illuminate\Broadcasting\InteractsWithSockets;7use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;8use Illuminate\Foundation\Events\Dispatchable;9use Illuminate\Queue\SerializesModels;1011class OrderShipped implements ShouldDispatchAfterCommit12{13 use Dispatchable, InteractsWithSockets, SerializesModels;1415 /**16 * Create a new event instance.17 */18 public function __construct(19 public Order $order,20 ) {}21}
1<?php23namespace App\Events;45use App\Models\Order;6use Illuminate\Broadcasting\InteractsWithSockets;7use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;8use Illuminate\Foundation\Events\Dispatchable;9use Illuminate\Queue\SerializesModels;1011class OrderShipped implements ShouldDispatchAfterCommit12{13 use Dispatchable, InteractsWithSockets, SerializesModels;1415 /**16 * Create a new event instance.17 */18 public function __construct(19 public Order $order,20 ) {}21}
Event Subscriber
撰寫 Event Subscriber
Event Subscriber 是一種類別,在 Subscriber 類別內可以訂閱多個 Event,讓我們能在單一類別中定義多個 Event 的處理程式。Subscriber 應定義 subscribe
方法,會傳入一個 Event Dispatcher 實體給該方法。我們可以在給定的 Dispatcher 上呼叫 listen
方法來註冊 Event Listener:
1<?php23namespace App\Listeners;45use Illuminate\Auth\Events\Login;6use Illuminate\Auth\Events\Logout;7use Illuminate\Events\Dispatcher;89class UserEventSubscriber10{11 /**12 * Handle user login events.13 */14 public function handleUserLogin(Login $event): void {}1516 /**17 * Handle user logout events.18 */19 public function handleUserLogout(Logout $event): void {}2021 /**22 * Register the listeners for the subscriber.23 */24 public function subscribe(Dispatcher $events): void25 {26 $events->listen(27 Login::class,28 [UserEventSubscriber::class, 'handleUserLogin']29 );3031 $events->listen(32 Logout::class,33 [UserEventSubscriber::class, 'handleUserLogout']34 );35 }36}
1<?php23namespace App\Listeners;45use Illuminate\Auth\Events\Login;6use Illuminate\Auth\Events\Logout;7use Illuminate\Events\Dispatcher;89class UserEventSubscriber10{11 /**12 * Handle user login events.13 */14 public function handleUserLogin(Login $event): void {}1516 /**17 * Handle user logout events.18 */19 public function handleUserLogout(Logout $event): void {}2021 /**22 * Register the listeners for the subscriber.23 */24 public function subscribe(Dispatcher $events): void25 {26 $events->listen(27 Login::class,28 [UserEventSubscriber::class, 'handleUserLogin']29 );3031 $events->listen(32 Logout::class,33 [UserEventSubscriber::class, 'handleUserLogout']34 );35 }36}
在 Subscriber 內可以定義 Event Listener 方法,但比起這麼做,在 Subscriber 的 subscribe
方法內回傳一組包含 Event 與方法名稱的陣列應該會更方便。在註冊 Event Listener 時,Laravel 會自動判斷該 Subscriber 的類別名稱:
1<?php23namespace App\Listeners;45use Illuminate\Auth\Events\Login;6use Illuminate\Auth\Events\Logout;7use Illuminate\Events\Dispatcher;89class UserEventSubscriber10{11 /**12 * Handle user login events.13 */14 public function handleUserLogin(Login $event): void {}1516 /**17 * Handle user logout events.18 */19 public function handleUserLogout(Logout $event): void {}2021 /**22 * Register the listeners for the subscriber.23 *24 * @return array<string, string>25 */26 public function subscribe(Dispatcher $events): array27 {28 return [29 Login::class => 'handleUserLogin',30 Logout::class => 'handleUserLogout',31 ];32 }33}
1<?php23namespace App\Listeners;45use Illuminate\Auth\Events\Login;6use Illuminate\Auth\Events\Logout;7use Illuminate\Events\Dispatcher;89class UserEventSubscriber10{11 /**12 * Handle user login events.13 */14 public function handleUserLogin(Login $event): void {}1516 /**17 * Handle user logout events.18 */19 public function handleUserLogout(Logout $event): void {}2021 /**22 * Register the listeners for the subscriber.23 *24 * @return array<string, string>25 */26 public function subscribe(Dispatcher $events): array27 {28 return [29 Login::class => 'handleUserLogin',30 Logout::class => 'handleUserLogout',31 ];32 }33}
註冊 Event Subscriber
寫好 Subscriber 後,就可以將 Subscriber 註冊到 Dispatcher 上了。可以使用 EventServiceProvider
的 $subscribe
屬性來註冊 Subscriber。舉例來說,我們來將 UserEventSubscriber
加到這個列表上:
1<?php23namespace App\Providers;45use App\Listeners\UserEventSubscriber;6use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;78class EventServiceProvider extends ServiceProvider9{10 /**11 * The event listener mappings for the application.12 *13 * @var array14 */15 protected $listen = [16 // ...17 ];1819 /**20 * The subscriber classes to register.21 *22 * @var array23 */24 protected $subscribe = [25 UserEventSubscriber::class,26 ];27}
1<?php23namespace App\Providers;45use App\Listeners\UserEventSubscriber;6use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;78class EventServiceProvider extends ServiceProvider9{10 /**11 * The event listener mappings for the application.12 *13 * @var array14 */15 protected $listen = [16 // ...17 ];1819 /**20 * The subscriber classes to register.21 *22 * @var array23 */24 protected $subscribe = [25 UserEventSubscriber::class,26 ];27}
測試
在測試會分派 Event 的程式時,可以讓 Laravel 不要真的執行該 Event 的 Listener。我們可以直接測試 Event 的 Listener,將 Listener 的測試與分派該 Event 的程式碼測試分開。當然,若要測試 Listener,需要在測試中先初始化一個 Listener 實體,並直接呼叫 handle
方法。
使用 Event
Facade 的 fake
方法,就可避免執行真正的 Listener,在測試中執行程式碼,然後使用 assertDispatched
、assertNotDispatched
、assertNothingDispatched
等方法來判斷程式分派了哪些 Event:
1<?php23namespace Tests\Feature;45use App\Events\OrderFailedToShip;6use App\Events\OrderShipped;7use Illuminate\Support\Facades\Event;8use Tests\TestCase;910class ExampleTest extends TestCase11{12 /**13 * Test order shipping.14 */15 public function test_orders_can_be_shipped(): void16 {17 Event::fake();1819 // 進行訂單出貨...2021 // 判斷是否已分派 Event...22 Event::assertDispatched(OrderShipped::class);2324 // 判斷是否已分派兩次 Event...25 Event::assertDispatched(OrderShipped::class, 2);2627 // 判斷 Event 是否未被分派...28 Event::assertNotDispatched(OrderFailedToShip::class);2930 // 判斷是否無 Event 被分派...31 Event::assertNothingDispatched();32 }33}
1<?php23namespace Tests\Feature;45use App\Events\OrderFailedToShip;6use App\Events\OrderShipped;7use Illuminate\Support\Facades\Event;8use Tests\TestCase;910class ExampleTest extends TestCase11{12 /**13 * Test order shipping.14 */15 public function test_orders_can_be_shipped(): void16 {17 Event::fake();1819 // 進行訂單出貨...2021 // 判斷是否已分派 Event...22 Event::assertDispatched(OrderShipped::class);2324 // 判斷是否已分派兩次 Event...25 Event::assertDispatched(OrderShipped::class, 2);2627 // 判斷 Event 是否未被分派...28 Event::assertNotDispatched(OrderFailedToShip::class);2930 // 判斷是否無 Event 被分派...31 Event::assertNothingDispatched();32 }33}
可以傳入一個閉包給 assertDispatched
或 assertNotDispatched
方法,來判斷某個 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::class4);
1Event::assertListening(2 OrderShipped::class,3 SendShipmentNotification::class4);
呼叫 Event::fake()
後,就不會執行 Event Listener。因此,若有測試使用的 Model Factory 仰賴於 Event,如在 Model 的 creating
Event 上建立 UUID 等,請在使用完 Factory 之後 再呼叫 Event::fake()
。
模擬一部分的 Event
若只想為一部分 Event 來 Fake Event Listener,則可將這些 Event 傳入fake
或 fakeFor
方法:
1/**2 * Test order process.3 */4public function test_orders_can_be_processed(): void5{6 Event::fake([7 OrderCreated::class,8 ]);910 $order = Order::factory()->create();1112 Event::assertDispatched(OrderCreated::class);1314 // 其他 Event 會被正常分派...15 $order->update([...]);16}
1/**2 * Test order process.3 */4public function test_orders_can_be_processed(): void5{6 Event::fake([7 OrderCreated::class,8 ]);910 $order = Order::factory()->create();1112 Event::assertDispatched(OrderCreated::class);1314 // 其他 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<?php23namespace Tests\Feature;45use App\Events\OrderCreated;6use App\Models\Order;7use Illuminate\Support\Facades\Event;8use Tests\TestCase;910class ExampleTest extends TestCase11{12 /**13 * Test order process.14 */15 public function test_orders_can_be_processed(): void16 {17 $order = Event::fakeFor(function () {18 $order = Order::factory()->create();1920 Event::assertDispatched(OrderCreated::class);2122 return $order;23 });2425 // Event 會被正常分派,Observer 會執行...26 $order->update([...]);27 }28}
1<?php23namespace Tests\Feature;45use App\Events\OrderCreated;6use App\Models\Order;7use Illuminate\Support\Facades\Event;8use Tests\TestCase;910class ExampleTest extends TestCase11{12 /**13 * Test order process.14 */15 public function test_orders_can_be_processed(): void16 {17 $order = Event::fakeFor(function () {18 $order = Order::factory()->create();1920 Event::assertDispatched(OrderCreated::class);2122 return $order;23 });2425 // Event 會被正常分派,Observer 會執行...26 $order->update([...]);27 }28}