跳轉至內容

活動

簡介

Laravel 的事件提供了一個簡單的觀察者模式實現,允許你訂閱和監聽應用程式中發生的各種事件。事件類通常儲存在 app/Events 目錄中,而它們的監聽器則儲存在 app/Listeners 目錄中。如果你的應用程式中沒有這些目錄,也不必擔心,當你使用 Artisan 命令生成事件和監聽器時,它們會自動為你建立。

事件是解耦應用程式各個部分的絕佳方式,因為單個事件可以有多個互不依賴的監聽器。例如,你可能希望在每次訂單發貨時向用戶傳送 Slack 通知。與其將訂單處理程式碼與 Slack 通知程式碼耦合,不如觸發一個 App\Events\OrderShipped 事件,監聽器接收到該事件後即可執行傳送 Slack 通知的操作。

生成事件與監聽器

要快速生成事件和監聽器,可以使用 make:eventmake:listener Artisan 命令。

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

為方便起見,你也可以不帶引數呼叫 make:eventmake:listener Artisan 命令。此時,Laravel 會自動提示你輸入類名,並在建立監聽器時提示該監聽器應監聽的事件。

1php artisan make:event
2 
3php artisan make:listener

註冊事件與監聽器

事件發現

預設情況下,Laravel 會透過掃描應用程式的 Listeners 目錄自動查詢並註冊事件監聽器。當 Laravel 發現任何以 handle__invoke 開頭的監聽器類方法時,它會將這些方法註冊為方法簽名中型別提示的事件的監聽器。

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

你可以使用 PHP 的聯合型別來監聽多個事件。

1/**
2 * Handle the event.
3 */
4public function handle(PodcastProcessed|PodcastPublished $event): void
5{
6 // ...
7}

如果你計劃將監聽器儲存在不同的目錄或多個目錄中,可以在應用程式的 bootstrap/app.php 檔案中使用 withEvents 方法指示 Laravel 掃描這些目錄。

1->withEvents(discover: [
2 __DIR__.'/../app/Domain/Orders/Listeners',
3])

你可以使用 * 字元作為萬用字元來掃描多個相似的目錄。

1->withEvents(discover: [
2 __DIR__.'/../app/Domain/*/Listeners',
3])

可以使用 event:list 命令列出應用程式中註冊的所有監聽器。

1php artisan event:list

生產環境中的事件發現

為了提升應用程式的執行速度,你應該使用 optimizeevent:cache Artisan 命令快取應用程式所有監聽器的清單。通常,該命令應作為應用程式 部署過程 的一部分來執行。框架將使用此清單來加速事件註冊過程。可以使用 event:clear 命令來銷燬事件快取。

手動註冊事件

使用 Event 門面(Facade),你可以在應用程式 AppServiceProviderboot 方法中手動註冊事件及其對應的監聽器。

1use App\Domain\Orders\Events\PodcastProcessed;
2use App\Domain\Orders\Listeners\SendPodcastNotification;
3use Illuminate\Support\Facades\Event;
4 
5/**
6 * Bootstrap any application services.
7 */
8public function boot(): void
9{
10 Event::listen(
11 PodcastProcessed::class,
12 SendPodcastNotification::class,
13 );
14}

可以使用 event:list 命令列出應用程式中註冊的所有監聽器。

1php artisan event:list

閉包監聽器

通常,監聽器被定義為類;然而,你也可以在 AppServiceProviderboot 方法中手動註冊基於閉包的事件監聽器。

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

可佇列化的匿名事件監聽器

在註冊基於閉包的事件監聽器時,你可以將監聽器閉包包裝在 Illuminate\Events\queueable 函式中,以指示 Laravel 使用 佇列 執行該監聽器。

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

與佇列任務一樣,你可以使用 onConnectiononQueuedelay 方法來自定義佇列監聽器的執行。

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

如果你想處理匿名佇列監聽器的失敗情況,可以在定義 queueable 監聽器時向 catch 方法提供一個閉包。該閉包將接收事件例項和導致監聽器失敗的 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 // The queued listener failed...
10}));

萬用字元事件監聽器

你還可以使用 * 字元作為萬用字元引數來註冊監聽器,從而允許你在同一個監聽器上捕獲多個事件。萬用字元監聽器接收事件名稱作為其第一個引數,並將整個事件資料陣列作為其第二個引數。

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

定義事件

事件類本質上是一個數據容器,儲存著與事件相關的資訊。例如,假設 App\Events\OrderShipped 事件接收一個 Eloquent ORM 物件。

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}

正如你所見,該事件類不包含任何邏輯。它是購買的 App\Models\Order 例項的容器。如果事件物件被序列化(例如在使用 佇列監聽器 時),事件所使用的 SerializesModels 特性將優雅地處理任何 Eloquent 模型的序列化。

定義監聽器

接下來,讓我們看看示例事件的監聽器。事件監聽器在其 handle 方法中接收事件例項。當使用 --event 選項呼叫 make:listener Artisan 命令時,它會自動匯入正確的事件類,並在 handle 方法中對事件進行型別提示。在 handle 方法中,你可以執行響應事件所需的任何操作。

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 * Handle the event.
16 */
17 public function handle(OrderShipped $event): void
18 {
19 // Access the order using $event->order...
20 }
21}

你的事件監聽器還可以在建構函式中對任何所需的依賴項進行型別提示。所有事件監聽器都透過 Laravel 服務容器 解析,因此依賴項將自動注入。

停止事件傳播

有時,你可能希望停止事件傳播到其他監聽器。你可以透過在監聽器的 handle 方法中返回 false 來實現這一點。

佇列化事件監聽器

如果監聽器執行緩慢的任務(如傳送電子郵件或發出 HTTP 請求),佇列化監聽器將非常有用。在使用佇列監聽器之前,請確保 配置了佇列 並在伺服器或本地開發環境中啟動了佇列處理器。

要指定監聽器應被佇列化,請將 ShouldQueue 介面新增到監聽器類中。由 make:listener Artisan 命令生成的監聽器已經將此介面匯入到當前名稱空間中,因此你可以立即使用它。

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

就是這樣!現在,當此監聽器處理的事件被排程時,該監聽器將由事件排程器透過 Laravel 的 佇列系統 自動進入佇列。如果佇列在執行監聽器時未丟擲異常,該佇列任務將在處理完成後自動刪除。

自定義佇列連線、名稱和延遲

如果你想自定義事件監聽器的佇列連線、佇列名稱或佇列延遲時間,可以在監聽器類上使用 ConnectionQueueDelay 屬性。

1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderShipped;
6use Illuminate\Contracts\Queue\ShouldQueue;
7use Illuminate\Queue\Attributes\Connection;
8use Illuminate\Queue\Attributes\Delay;
9use Illuminate\Queue\Attributes\Queue;
10 
11#[Connection('sqs')]
12#[Queue('listeners')]
13#[Delay(60)]
14class SendShipmentNotification implements ShouldQueue
15{
16 // ...
17}

如果你想在執行時定義監聽器的佇列連線、佇列名稱或延遲,可以在監聽器上定義 viaConnectionviaQueuewithDelay 方法。

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}
16 
17/**
18 * Get the number of seconds before the job should be processed.
19 */
20public function withDelay(OrderShipped $event): int
21{
22 return $event->highPriority ? 0 : 60;
23}

條件化佇列監聽器

有時,你需要根據僅在執行時可用的資料來確定監聽器是否應該進入佇列。為此,可以在監聽器中新增 shouldQueue 方法來確定是否應該佇列化。如果 shouldQueue 方法返回 false,則監聽器將不會被放入佇列。

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}

手動與佇列互動

如果你需要手動訪問監聽器底層佇列任務的 deleterelease 方法,可以使用 Illuminate\Queue\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 ($condition) {
19 $this->release(30);
20 }
21 }
22}

佇列化事件監聽器與資料庫事務

當佇列監聽器在資料庫事務內被排程時,它們可能會在資料庫事務提交之前被佇列處理。當這種情況發生時,你在資料庫事務期間對模型或資料庫記錄所做的任何更新可能尚未反映在資料庫中。此外,在事務內建立的任何模型或資料庫記錄可能在資料庫中還不存在。如果你的監聽器依賴於這些模型,當排程佇列監聽器的任務被處理時,可能會出現意外錯誤。

如果你的佇列連線配置項 after_commit 被設定為 false,你仍然可以透過在監聽器類上實現 ShouldQueueAfterCommit 介面,來指明特定的佇列監聽器應該在所有開啟的資料庫事務提交後才被排程。

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

要了解更多關於解決這些問題的資訊,請檢視關於排隊作業與資料庫事務的文件。

佇列監聽器中介軟體

佇列監聽器也可以利用 任務中介軟體。任務中介軟體允許你將自定義邏輯包裝在佇列監聽器的執行過程中,減少監聽器本身的樣板程式碼。建立任務中介軟體後,可以透過從監聽器的 middleware 方法中返回它們來將其附加到監聽器。

1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderShipped;
6use App\Jobs\Middleware\RateLimited;
7use Illuminate\Contracts\Queue\ShouldQueue;
8 
9class SendShipmentNotification implements ShouldQueue
10{
11 /**
12 * Handle the event.
13 */
14 public function handle(OrderShipped $event): void
15 {
16 // Process the event...
17 }
18 
19 /**
20 * Get the middleware the listener should pass through.
21 *
22 * @return array<int, object>
23 */
24 public function middleware(OrderShipped $event): array
25 {
26 return [new RateLimited];
27 }
28}

加密的佇列監聽器

Laravel 允許你透過 加密 確保佇列監聽器資料的隱私和完整性。要開始使用,只需將 ShouldBeEncrypted 介面新增到監聽器類中。一旦將此介面新增到類中,Laravel 就會在將監聽器推入佇列之前自動對其進行加密。

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

唯一事件監聽器

唯一監聽器需要支援 的快取驅動程式。目前,memcachedredisdynamodbdatabasefilearray 快取驅動程式都支援原子鎖。

有時,你可能希望確保在任何時間點佇列中只有一個特定監聽器的例項。你可以透過在監聽器類上實現 ShouldBeUnique 介面來實現這一點。

1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\LicenseSaved;
6use Illuminate\Contracts\Queue\ShouldBeUnique;
7use Illuminate\Contracts\Queue\ShouldQueue;
8 
9class AcquireProductKey implements ShouldQueue, ShouldBeUnique
10{
11 public function __invoke(LicenseSaved $event): void
12 {
13 // ...
14 }
15}

在上面的示例中,AcquireProductKey 監聽器是唯一的。因此,如果該監聽器的另一個例項已經在佇列中且尚未處理完成,則該監聽器將不會被放入佇列。這確保了即使許可證在短時間內被多次儲存,每個許可證也只會獲取一個產品金鑰。

在某些情況下,你可能想要定義一個特定的“鍵”來使監聽器變得唯一,或者你可能想要指定一個超時時間,超過該時間後監聽器不再保持唯一。為此,你可以在監聽器類上定義 uniqueIduniqueFor 屬性或方法。這些方法接收事件例項,允許你利用事件資料來構建返回值。

1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\LicenseSaved;
6use Illuminate\Contracts\Queue\ShouldBeUnique;
7use Illuminate\Contracts\Queue\ShouldQueue;
8 
9class AcquireProductKey implements ShouldQueue, ShouldBeUnique
10{
11 /**
12 * The number of seconds after which the listener's unique lock will be released.
13 *
14 * @var int
15 */
16 public $uniqueFor = 3600;
17 
18 public function __invoke(LicenseSaved $event): void
19 {
20 // ...
21 }
22 
23 /**
24 * Get the unique ID for the listener.
25 */
26 public function uniqueId(LicenseSaved $event): string
27 {
28 return 'listener:'.$event->license->id;
29 }
30}

在上面的示例中,AcquireProductKey 監聽器按許可證 ID 實現唯一性。因此,在現有監聽器處理完成之前,任何針對同一許可證的新監聽器排程都將被忽略。這防止了為同一許可證重複獲取產品金鑰。此外,如果現有監聽器在一小時內未被處理,唯一鎖將被釋放,具有相同唯一鍵的另一個監聽器就可以進入佇列。

如果你的應用程式從多個 Web 伺服器或容器排程事件,你應該確保所有伺服器都在與同一個中央快取伺服器通訊,以便 Laravel 能夠準確判斷監聽器是否唯一。

保持監聽器在處理開始前的唯一性

預設情況下,唯一監聽器在處理完成或耗盡所有重試嘗試後會被“解鎖”。然而,在某些情況下,你可能希望監聽器在處理開始前就解鎖。為此,你的監聽器應實現 ShouldBeUniqueUntilProcessing 契約,而不是 ShouldBeUnique 契約。

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

唯一監聽器鎖

在底層,當一個 ShouldBeUnique 監聽器被排程時,Laravel 會嘗試使用 uniqueId 鍵獲取一個 。如果鎖已被佔用,監聽器就不會被排程。該鎖會在監聽器處理完成或耗盡所有重試嘗試時釋放。預設情況下,Laravel 將使用預設快取驅動程式來獲取此鎖。然而,如果你希望使用另一個驅動程式來獲取鎖,可以定義一個返回所需快取驅動程式的 uniqueVia 方法。

1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\LicenseSaved;
6use Illuminate\Contracts\Cache\Repository;
7use Illuminate\Support\Facades\Cache;
8 
9class AcquireProductKey implements ShouldQueue, ShouldBeUnique
10{
11 // ...
12 
13 /**
14 * Get the cache driver for the unique listener lock.
15 */
16 public function uniqueVia(LicenseSaved $event): Repository
17 {
18 return Cache::driver('redis');
19 }
20}

如果你只需要限制監聽器的併發處理,請改用 WithoutOverlapping 任務中介軟體。

處理失敗的任務

有時你的佇列事件監聽器可能會失敗。如果佇列監聽器超過了佇列處理器定義的嘗試次數上限,監聽器上的 failed 方法將被呼叫。failed 方法會接收事件例項以及導致失敗的 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}

指定佇列監聽器最大嘗試次數

如果你的某個佇列監聽器遇到錯誤,你通常不希望它無限期地重試。因此,Laravel 提供了多種方式來指定監聽器可以嘗試的次數或時長。

你可以在監聽器類上使用 Tries 屬性來指定監聽器在被視為失敗之前可以嘗試的次數。

1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderShipped;
6use Illuminate\Contracts\Queue\ShouldQueue;
7use Illuminate\Queue\Attributes\Tries;
8use Illuminate\Queue\InteractsWithQueue;
9 
10#[Tries(5)]
11class SendShipmentNotification implements ShouldQueue
12{
13 use InteractsWithQueue;
14 
15 // ...
16}

作為定義失敗前嘗試次數的替代方案,你可以定義監聽器停止嘗試的時間點。這允許監聽器在給定的時間框架內嘗試任意次數。要定義停止嘗試的時間點,請在監聽器類中新增一個 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()->plus(minutes: 5);
9}

如果同時定義了 retryUntiltries,Laravel 將優先考慮 retryUntil 方法。

指定佇列監聽器退避策略(Backoff)

如果你想配置監聽器在遇到異常後重試前應等待多少秒,可以在監聽器類上使用 Backoff 屬性。

1<?php
2 
3namespace App\Listeners;
4 
5use Illuminate\Contracts\Queue\ShouldQueue;
6use Illuminate\Queue\Attributes\Backoff;
7 
8#[Backoff(3)]
9class SendShipmentNotification implements ShouldQueue
10{
11 // ...
12}

如果你需要更復雜的邏輯來確定監聽器的退避時間,可以在監聽器類上定義一個 backoff 方法。

1/**
2 * Calculate the number of seconds to wait before retrying the queued listener.
3 */
4public function backoff(OrderShipped $event): int
5{
6 return 3;
7}

你可以透過從 backoff 方法返回一個退避值陣列,輕鬆配置“指數”退避。在此示例中,第一次重試的延遲為 1 秒,第二次重試為 5 秒,第三次重試為 10 秒,如果還有剩餘嘗試次數,後續每次重試均為 10 秒。

1/**
2 * Calculate the number of seconds to wait before retrying the queued listener.
3 *
4 * @return list<int>
5 */
6public function backoff(OrderShipped $event): array
7{
8 return [1, 5, 10];
9}

指定佇列監聽器最大異常數

有時你可能希望指定一個佇列監聽器可以嘗試多次,但如果重試是由給定的未處理異常數觸發的(而不是透過 release 方法直接釋放),則應失敗。為此,你可以在監聽器類上使用 TriesMaxExceptions 屬性。

1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderShipped;
6use Illuminate\Contracts\Queue\ShouldQueue;
7use Illuminate\Queue\Attributes\MaxExceptions;
8use Illuminate\Queue\Attributes\Tries;
9use Illuminate\Queue\InteractsWithQueue;
10 
11#[Tries(25)]
12#[MaxExceptions(3)]
13class SendShipmentNotification implements ShouldQueue
14{
15 use InteractsWithQueue;
16 
17 /**
18 * Handle the event.
19 */
20 public function handle(OrderShipped $event): void
21 {
22 // Process the event...
23 }
24}

在這個例子中,監聽器最多會被重試 25 次。但是,如果監聽器丟擲三次未處理的異常,它將會失敗。

指定佇列監聽器超時

通常,你會大致瞭解佇列監聽器預計需要多長時間。因此,Laravel 允許你指定一個“超時”值。如果監聽器的處理時間超過了超時值指定的秒數,處理該監聽器的處理器將以錯誤退出。你可以使用監聽器類上的 Timeout 屬性來定義允許監聽器執行的最大秒數。

1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderShipped;
6use Illuminate\Contracts\Queue\ShouldQueue;
7use Illuminate\Queue\Attributes\Timeout;
8 
9#[Timeout(120)]
10class SendShipmentNotification implements ShouldQueue
11{
12 // ...
13}

如果你想指示監聽器在超時時應被標記為失敗,可以在監聽器類上使用 FailOnTimeout 屬性。

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

排程事件

要排程事件,可以呼叫事件上的靜態 dispatch 方法。此方法由 Illuminate\Foundation\Events\Dispatchable 特性提供。傳遞給 dispatch 方法的任何引數都將傳遞給事件的建構函式。

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

如果你想有條件地排程事件,可以使用 dispatchIfdispatchUnless 方法。

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

在測試時,斷言某些事件已被排程而無需實際觸發它們的監聽器會很有幫助。Laravel 的 內建測試輔助工具 可以輕鬆實現這一點。

在資料庫事務後排程事件

有時,你可能希望指示 Laravel 僅在活動資料庫事務提交後才排程事件。為此,你可以在事件類上實現 ShouldDispatchAfterCommit 介面。

此介面指示 Laravel 直到當前資料庫事務提交後才排程事件。如果事務失敗,事件將被丟棄。如果在排程事件時沒有正在進行的資料庫事務,事件將立即被排程。

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

延遲事件

延遲事件允許你將模型事件的排程和事件監聽器的執行推遲到特定的程式碼塊完成之後。當你需要確保在觸發事件監聽器之前建立所有相關記錄時,這特別有用。

要延遲事件,請向 Event::defer() 方法提供一個閉包。

1use App\Models\User;
2use Illuminate\Support\Facades\Event;
3 
4Event::defer(function () {
5 $user = User::create(['name' => 'Victoria Otwell']);
6 
7 $user->posts()->create(['title' => 'My first post!']);
8});

閉包內觸發的所有事件都將在閉包執行完成後被排程。這確保了事件監聽器可以訪問在延遲執行期間建立的所有相關記錄。如果閉包內發生異常,延遲的事件將不會被排程。

要僅延遲特定事件,請將事件陣列作為第二個引數傳遞給 defer 方法。

1use App\Models\User;
2use Illuminate\Support\Facades\Event;
3 
4Event::defer(function () {
5 $user = User::create(['name' => 'Victoria Otwell']);
6 
7 $user->posts()->create(['title' => 'My first post!']);
8}, ['eloquent.created: '.User::class]);

事件訂閱者

編寫事件訂閱者

事件訂閱者是可以在自身內部訂閱多個事件的類,允許你在單個類中定義多個事件處理器。訂閱者應定義一個 subscribe 方法,該方法接收一個事件排程器例項。你可以在給定的排程器上呼叫 listen 方法來註冊事件監聽器。

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(Login $event): void {}
15 
16 /**
17 * Handle user logout events.
18 */
19 public function handleUserLogout(Logout $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}

如果你的事件監聽器方法定義在訂閱者自身內部,你可能會發現從訂閱者的 subscribe 方法返回事件名稱和方法名稱的陣列更方便。Laravel 在註冊事件監聽器時會自動確定訂閱者的類名。

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(Login $event): void {}
15 
16 /**
17 * Handle user logout events.
18 */
19 public function handleUserLogout(Logout $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}

註冊事件訂閱者

編寫訂閱者後,如果訂閱者中的處理器方法遵循 Laravel 的 事件發現約定,Laravel 會自動註冊它們。否則,你可以使用 Event 門面的 subscribe 方法手動註冊你的訂閱者。通常,這應該在應用程式 AppServiceProviderboot 方法中完成。

1<?php
2 
3namespace App\Providers;
4 
5use App\Listeners\UserEventSubscriber;
6use Illuminate\Support\Facades\Event;
7use Illuminate\Support\ServiceProvider;
8 
9class AppServiceProvider extends ServiceProvider
10{
11 /**
12 * Bootstrap any application services.
13 */
14 public function boot(): void
15 {
16 Event::subscribe(UserEventSubscriber::class);
17 }
18}

測試

在測試排程事件的程式碼時,你可能希望指示 Laravel 不要實際執行事件的監聽器,因為監聽器的程式碼可以直接與排程事件的程式碼分開進行測試。當然,要測試監聽器本身,你可以在測試中例項化一個監聽器物件並直接呼叫 handle 方法。

使用 Event 門面的 fake 方法,你可以防止監聽器執行,執行被測程式碼,然後使用 assertDispatchedassertNotDispatchedassertNothingDispatched 方法斷言應用程式排程了哪些事件。

1<?php
2 
3use App\Events\OrderFailedToShip;
4use App\Events\OrderShipped;
5use Illuminate\Support\Facades\Event;
6 
7test('orders can be shipped', function () {
8 Event::fake();
9 
10 // Perform order shipping...
11 
12 // Assert that an event was dispatched...
13 Event::assertDispatched(OrderShipped::class);
14 
15 // Assert an event was dispatched twice...
16 Event::assertDispatched(OrderShipped::class, 2);
17 
18 // Assert an event was dispatched once...
19 Event::assertDispatchedOnce(OrderShipped::class);
20 
21 // Assert an event was not dispatched...
22 Event::assertNotDispatched(OrderFailedToShip::class);
23 
24 // Assert that no events were dispatched...
25 Event::assertNothingDispatched();
26});
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 // Perform order shipping...
20 
21 // Assert that an event was dispatched...
22 Event::assertDispatched(OrderShipped::class);
23 
24 // Assert an event was dispatched twice...
25 Event::assertDispatched(OrderShipped::class, 2);
26 
27 // Assert an event was dispatched once...
28 Event::assertDispatchedOnce(OrderShipped::class);
29 
30 // Assert an event was not dispatched...
31 Event::assertNotDispatched(OrderFailedToShip::class);
32 
33 // Assert that no events were dispatched...
34 Event::assertNothingDispatched();
35 }
36}

你可以向 assertDispatchedassertNotDispatched 方法傳遞一個閉包,以斷言排程了一個透過給定“真值測試”的事件。如果至少有一個通過了給定真值測試的事件被排程,則斷言將成功。

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

如果你只是想斷言某個事件監聽器正在監聽給定的事件,可以使用 assertListening 方法。

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

呼叫 Event::fake() 後,將不會執行任何事件監聽器。因此,如果你的測試使用了依賴於事件的模型工廠(例如在模型的 creating 事件期間建立 UUID),你應該在使用工廠之後呼叫 Event::fake()

偽造部分事件

如果你只想為一組特定的事件偽造事件監聽器,可以將它們傳遞給 fakefakeFor 方法。

1test('orders can be processed', function () {
2 Event::fake([
3 OrderCreated::class,
4 ]);
5 
6 $order = Order::factory()->create();
7 
8 Event::assertDispatched(OrderCreated::class);
9 
10 // Other events are dispatched as normal...
11 $order->update([
12 // ...
13 ]);
14});
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 // Other events are dispatched as normal...
15 $order->update([
16 // ...
17 ]);
18}

你可以使用 except 方法偽造除指定事件集之外的所有事件。

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

作用域事件偽造

如果你只想在測試的一部分期間偽造事件監聽器,可以使用 fakeFor 方法。

1<?php
2 
3use App\Events\OrderCreated;
4use App\Models\Order;
5use Illuminate\Support\Facades\Event;
6 
7test('orders can be processed', function () {
8 $order = Event::fakeFor(function () {
9 $order = Order::factory()->create();
10 
11 Event::assertDispatched(OrderCreated::class);
12 
13 return $order;
14 });
15 
16 // Events are dispatched as normal and observers will run...
17 $order->update([
18 // ...
19 ]);
20});
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 // Events are dispatched as normal and observers will run...
26 $order->update([
27 // ...
28 ]);
29 }
30}