事件 - Event
簡介
Laravel 的 Event 提供了一種簡單的 Observer 設計模式實作,能讓你註冊與監聽程式內發生的多種事件。Event 類別一般儲存在 app/Events
目錄下,而 Listener 則一般儲存在 app/Listeners
目錄。若在專案內沒看到這些目錄的話請別擔心,在使用 Artisan 指令產生 Event 跟 Listener 的時候會自動建立。
Event 是以各種層面解耦程式的好方法,因為一個 Event 可以由多個不互相依賴的 Listener。舉例來說,我們可能會想在訂單出貨的時候傳送 Slack 通知給使用者。除了耦合訂單處理的程式碼跟 Slack 通知的程式碼外,我們可以產生一個 App\Events\OrderShipped
事件,然後使用一個 Listener 來接收並分派 Slack 通知。
Generating Events and Listeners
To quickly generate events and listeners, you may use the make:event
and make:listener
Artisan commands:
1php artisan make:event PodcastProcessed23php artisan make:listener SendPodcastNotification --event=PodcastProcessed
1php artisan make:event PodcastProcessed23php artisan make:listener SendPodcastNotification --event=PodcastProcessed
For convenience, you may also invoke the make:event
and make:listener
Artisan commands without additional arguments. When you do so, Laravel will automatically prompt you for the class name and, when creating a listener, the event it should listen to:
1php artisan make:event23php artisan make:listener
1php artisan make:event23php artisan make:listener
Registering Events and Listeners
Event Discovery
By default, Laravel will automatically find and register your event listeners by scanning your application's Listeners
directory. When Laravel finds any listener class method that begins with handle
or __invoke
, Laravel will register those methods as event listeners for the event that is type-hinted in the method's signature:
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}
If you plan to store your listeners in a different directory or within multiple directories, you may instruct Laravel to scan those directories using the withEvents
method in your application's bootstrap/app.php
file:
1->withEvents(discover: [2 __DIR__.'/../app/Domain/Listeners',3])
1->withEvents(discover: [2 __DIR__.'/../app/Domain/Listeners',3])
The event:list
command may be used to list all of the listeners registered within your application:
1php artisan event:list
1php artisan event:list
Event Discovery in Production
To give your application a speed boost, you should cache a manifest of all of your application's listeners using the optimize
or event:cache
Artisan commands. Typically, this command should be run as part of your application's deployment process. This manifest will be used by the framework to speed up the event registration process. The event:clear
command may be used to destroy the event cache.
手動註冊 Event
Using the Event
facade, you may manually register events and their corresponding listeners within the boot
method of your application's AppServiceProvider
:
1use App\Domain\Orders\Events\PodcastProcessed;2use App\Domain\Orders\Listeners\SendPodcastNotification;3use Illuminate\Support\Facades\Event;45/**6 * Bootstrap any application services.7 */8public function boot(): void9{10 Event::listen(11 PodcastProcessed::class,12 SendPodcastNotification::class,13 );14}
1use App\Domain\Orders\Events\PodcastProcessed;2use App\Domain\Orders\Listeners\SendPodcastNotification;3use Illuminate\Support\Facades\Event;45/**6 * Bootstrap any application services.7 */8public function boot(): void9{10 Event::listen(11 PodcastProcessed::class,12 SendPodcastNotification::class,13 );14}
The event:list
command may be used to list all of the listeners registered within your application:
1php artisan event:list
1php artisan event:list
Closure Listeners
Typically, listeners are defined as classes; however, you may also manually register closure-based event listeners in the boot
method of your application's AppServiceProvider
:
1use App\Events\PodcastProcessed;2use Illuminate\Support\Facades\Event;34/**5 * Bootstrap any application services.6 */7public function boot(): void8{9 Event::listen(function (PodcastProcessed $event) {10 // ...11 });12}
1use App\Events\PodcastProcessed;2use Illuminate\Support\Facades\Event;34/**5 * Bootstrap any application services.6 */7public function boot(): void8{9 Event::listen(function (PodcastProcessed $event) {10 // ...11 });12}
可放入佇列的匿名 Event Listener
When registering closure based event listeners, you may wrap the listener closure within the Illuminate\Events\queueable
function to instruct Laravel to execute the listener using the queue:
1use App\Events\PodcastProcessed;2use function Illuminate\Events\queueable;3use Illuminate\Support\Facades\Event;45/**6 * Bootstrap any application services.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 * Bootstrap any application services.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 // The queued listener failed...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 // The queued listener failed...10}));
萬用字元 Event Listener
You may also register listeners using the *
character as a wildcard parameter, allowing you to catch multiple events on the same listener. Wildcard listeners receive the event name as their first argument and the entire event data array as their second argument:
1Event::listen('event.*', function (string $eventName, array $data) {2 // ...3});
1Event::listen('event.*', function (string $eventName, array $data) {2 // ...3});
定義 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
Next, let's take a look at the listener for our example event. Event listeners receive event instances in their handle
method. The make:listener
Artisan command, when invoked with the --event
option, will automatically import the proper event class and type-hint the event in the handle
method. Within the handle
method, you may perform any actions necessary to respond to the 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。
To specify that a listener should be queued, add the ShouldQueue
interface to the listener class. Listeners generated by the make:listener
Artisan commands already have this interface imported into the current namespace so you can use it immediately:
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}
Manually Interacting With the Queue
若有需要手動存取某個 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}
Queued Event Listeners and Database Transactions
當 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 // Order shipment logic...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 // Order shipment logic...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
After writing the subscriber, you are ready to register it with the event dispatcher. You may register subscribers using the subscribe
method of the Event
facade. Typically, this should be done within the boot
method of your application's AppServiceProvider
:
1<?php23namespace App\Providers;45use App\Listeners\UserEventSubscriber;6use Illuminate\Support\Facades\Event;7use Illuminate\Support\ServiceProvider;89class AppServiceProvider extends ServiceProvider10{11 /**12 * Bootstrap any application services.13 */14 public function boot(): void15 {16 Event::subscribe(UserEventSubscriber::class);17 }18}
1<?php23namespace App\Providers;45use App\Listeners\UserEventSubscriber;6use Illuminate\Support\Facades\Event;7use Illuminate\Support\ServiceProvider;89class AppServiceProvider extends ServiceProvider10{11 /**12 * Bootstrap any application services.13 */14 public function boot(): void15 {16 Event::subscribe(UserEventSubscriber::class);17 }18}
測試
在測試會分派 Event 的程式時,可以讓 Laravel 不要真的執行該 Event 的 Listener。我們可以直接測試 Event 的 Listener,將 Listener 的測試與分派該 Event 的程式碼測試分開。當然,若要測試 Listener,需要在測試中先初始化一個 Listener 實體,並直接呼叫 handle
方法。
使用 Event
Facade 的 fake
方法,就可避免執行真正的 Listener,在測試中執行程式碼,然後使用 assertDispatched
、assertNotDispatched
、assertNothingDispatched
等方法來判斷程式分派了哪些 Event:
1<?php23use App\Events\OrderFailedToShip;4use App\Events\OrderShipped;5use Illuminate\Support\Facades\Event;67test('orders can be shipped', function () {8 Event::fake();910 // Perform order shipping...1112 // Assert that an event was dispatched...13 Event::assertDispatched(OrderShipped::class);1415 // Assert an event was dispatched twice...16 Event::assertDispatched(OrderShipped::class, 2);1718 // Assert an event was not dispatched...19 Event::assertNotDispatched(OrderFailedToShip::class);2021 // Assert that no events were dispatched...22 Event::assertNothingDispatched();23});
1<?php23use App\Events\OrderFailedToShip;4use App\Events\OrderShipped;5use Illuminate\Support\Facades\Event;67test('orders can be shipped', function () {8 Event::fake();910 // Perform order shipping...1112 // Assert that an event was dispatched...13 Event::assertDispatched(OrderShipped::class);1415 // Assert an event was dispatched twice...16 Event::assertDispatched(OrderShipped::class, 2);1718 // Assert an event was not dispatched...19 Event::assertNotDispatched(OrderFailedToShip::class);2021 // Assert that no events were dispatched...22 Event::assertNothingDispatched();23});
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 // Perform order shipping...2021 // Assert that an event was dispatched...22 Event::assertDispatched(OrderShipped::class);2324 // Assert an event was dispatched twice...25 Event::assertDispatched(OrderShipped::class, 2);2627 // Assert an event was not dispatched...28 Event::assertNotDispatched(OrderFailedToShip::class);2930 // Assert that no events were dispatched...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 // Perform order shipping...2021 // Assert that an event was dispatched...22 Event::assertDispatched(OrderShipped::class);2324 // Assert an event was dispatched twice...25 Event::assertDispatched(OrderShipped::class, 2);2627 // Assert an event was not dispatched...28 Event::assertNotDispatched(OrderFailedToShip::class);2930 // Assert that no events were dispatched...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()
。
Faking a Subset of Events
若只想為一部分 Event 來 Fake Event Listener,則可將這些 Event 傳入fake
或 fakeFor
方法:
1test('orders can be processed', function () {2 Event::fake([3 OrderCreated::class,4 ]);56 $order = Order::factory()->create();78 Event::assertDispatched(OrderCreated::class);910 // Other events are dispatched as normal...11 $order->update([...]);12});
1test('orders can be processed', function () {2 Event::fake([3 OrderCreated::class,4 ]);56 $order = Order::factory()->create();78 Event::assertDispatched(OrderCreated::class);910 // Other events are dispatched as normal...11 $order->update([...]);12});
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 // Other events are dispatched as normal...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 // Other events are dispatched as normal...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<?php23use App\Events\OrderCreated;4use App\Models\Order;5use Illuminate\Support\Facades\Event;67test('orders can be processed', function () {8 $order = Event::fakeFor(function () {9 $order = Order::factory()->create();1011 Event::assertDispatched(OrderCreated::class);1213 return $order;14 });1516 // Events are dispatched as normal and observers will run ...17 $order->update([...]);18});
1<?php23use App\Events\OrderCreated;4use App\Models\Order;5use Illuminate\Support\Facades\Event;67test('orders can be processed', function () {8 $order = Event::fakeFor(function () {9 $order = Order::factory()->create();1011 Event::assertDispatched(OrderCreated::class);1213 return $order;14 });1516 // Events are dispatched as normal and observers will run ...17 $order->update([...]);18});
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 // Events are dispatched as normal and observers will run ...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 // Events are dispatched as normal and observers will run ...26 $order->update([...]);27 }28}