跳到主要內容

事件

📝 此頁面為 Laravel 官方文檔的繁體中文翻譯。查看原始英文版本

事件

簡介

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

事件是解耦應用程式各個方面的絕佳方式,因為單個事件可以有多個彼此不依賴的監聽器。例如,您可能希望在每次訂單發貨時向使用者發送 Slack 通知。與其將訂單處理代碼與 Slack 通知代碼耦合,您可以觸發一個 App\Events\OrderShipped 事件,監聽器可以接收並用於派發 Slack 通知。

生成事件和監聽器

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

php artisan make:event PodcastProcessed

php artisan make:listener SendPodcastNotification --event=PodcastProcessed

為了方便,您還可以在不附加額外參數的情況下呼叫 make:eventmake:listener Artisan 命令。當您這樣做時,Laravel 將自動提示您輸入類別名稱,並且在建立監聽器時,它應該監聽的事件:

php artisan make:event

php artisan make:listener

註冊事件和監聽器

事件發現

預設情況下,Laravel 將透過掃描應用程式的 Listeners 目錄來自動發現和註冊您的事件監聽器。當 Laravel 找到任何以 handle__invoke 開頭的監聽器類別方法時,Laravel 將把這些方法註冊為方法簽名中鍵入提示的事件的事件監聽器:

use App\Events\PodcastProcessed;

class SendPodcastNotification
{
    /**
     * 處理事件。
     */
    public function handle(PodcastProcessed $event): void
    {
        // ...
    }
}

您可以使用 PHP 的聯集類型監聽多個事件:

/**
 * 處理事件。
 */
public function handle(PodcastProcessed|PodcastPublished $event): void
{
    // ...
}

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

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

您可以使用 * 字元作為萬用字元來掃描多個類似目錄中的監聽器:

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

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

php artisan event:list

生產環境中的事件發現

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

動態事件發現

要動態控制是否發現給定的監聽器,您可以在監聽器類別上實現 ShouldBeDiscovered 介面,並定義一個返回布林值的 shouldBeDiscovered 方法。如果該方法返回 false,則在事件發現期間不會註冊該監聽器:

use Illuminate\Contracts\Events\ShouldBeDiscovered;

class SendPodcastNotification implements ShouldBeDiscovered
{
    /**
     * 處理事件。
     */
    public function handle(PodcastProcessed $event): void
    {
        // ...
    }

    /**
     * 判斷是否應發現監聽器。
     */
    public static function shouldBeDiscovered(): bool
    {
        return app()->environment('production');
    }
}

手動註冊事件

使用 Event 外觀,您可以在應用程式的 AppServiceProviderboot 方法中手動註冊事件及其對應的監聽器:

use App\Domain\Orders\Events\PodcastProcessed;
use App\Domain\Orders\Listeners\SendPodcastNotification;
use Illuminate\Support\Facades\Event;

/**
 * 啟動任何應用程式服務。
 */
public function boot(): void
{
    Event::listen(
        PodcastProcessed::class,
        SendPodcastNotification::class,
    );
}

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

php artisan event:list

閉包監聽器

通常,監聽器被定義為類別;但是,您還可以在應用程式的 AppServiceProviderboot 方法中手動註冊基於閉包的事件監聽器:

use App\Events\PodcastProcessed;
use Illuminate\Support\Facades\Event;

/**
 * 啟動任何應用程式服務。
 */
public function boot(): void
{
    Event::listen(function (PodcastProcessed $event) {
        // ...
    });
}

可佇列的匿名事件監聽器

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

use App\Events\PodcastProcessed;
use function Illuminate\Events\queueable;
use Illuminate\Support\Facades\Event;

/**
 * 啟動任何應用程式服務。
 */
public function boot(): void
{
    Event::listen(queueable(function (PodcastProcessed $event) {
        // ...
    }));
}

像佇列作業一樣,您可以使用 onConnectiononQueuedelay 方法來自訂佇列監聽器的執行:

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

如果您希望處理匿名佇列監聽器失敗,可以在定義 queueable 監聽器時向 catch 方法提供一個閉包。此閉包將接收事件實例和導致監聽器失敗的 Throwable 實例:

use App\Events\PodcastProcessed;
use function Illuminate\Events\queueable;
use Illuminate\Support\Facades\Event;
use Throwable;

Event::listen(queueable(function (PodcastProcessed $event) {
    // ...
})->catch(function (PodcastProcessed $event, Throwable $e) {
    // 佇列監聽器失敗...
}));

萬用字元事件監聽器

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

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

定義事件

事件類別本質上是一個資料容器,包含與事件相關的資訊。例如,假設 App\Events\OrderShipped 事件接收一個 Eloquent ORM 物件:

<?php

namespace App\Events;

use App\Models\Order;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderShipped
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * 建立一個新的事件實例。
     */
    public function __construct(
        public Order $order,
    ) {}
}

如您所見,此事件類別不包含任何邏輯。它是已購買的 App\Models\Order 實例的容器。事件使用的 SerializesModels trait 在使用 PHP 的 serialize 函數序列化事件物件時(例如使用佇列監聽器時),將優雅地序列化任何 Eloquent 模型。

定義監聽器

接下來,讓我們看看我們範例事件的監聽器。事件監聽器在其 handle 方法中接收事件實例。帶有 --event 選項呼叫的 make:listener Artisan 命令將自動導入正確的事件類別,並在 handle 方法中鍵入提示該事件。在 handle 方法中,您可以執行回應事件所需的任何操作:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;

class SendShipmentNotification
{
    /**
     * 建立事件監聽器。
     */
    public function __construct() {}

    /**
     * 處理事件。
     */
    public function handle(OrderShipped $event): void
    {
        // 使用 $event->order 存取訂單...
    }
}

[!NOTE] 您的事件監聽器還可以在其構造函數上鍵入提示它們需要的任何依賴。所有事件監聽器都透過 Laravel 服務容器解析,因此依賴將被自動注入。

停止事件的傳播

有時,您可能希望阻止事件傳播到其他監聽器。您可以透過從監聽器的 handle 方法返回 false 來執行此操作。

佇列事件監聽器

如果您的監聽器將執行緩慢的任務(例如發送電子郵件或發出 HTTP 請求),則將監聽器放入佇列可能是有益的。在使用佇列監聽器之前,請確保配置您的佇列並在伺服器或本地開發環境上啟動一個佇列工作者。

要指定監聽器應被放入佇列,請向監聽器類別添加 ShouldQueue 介面。由 make:listener Artisan 命令生成的監聽器已經將此介面導入當前命名空間,因此您可以立即使用它:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendShipmentNotification implements ShouldQueue
{
    // ...
}

就這樣!現在,當派發由該監聽器處理的事件時,監聽器將由事件派發器使用 Laravel 的佇列系統自動放入佇列。如果監聽器在由佇列執行時沒有拋出異常,則佇列作業將在完成處理後自動刪除。

自訂佇列連接、名稱和延遲

如果您希望自訂事件監聽器的佇列連接、佇列名稱或佇列延遲時間,可以在監聽器類別上使用 ConnectionQueueDelay 屬性:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Connection;
use Illuminate\Queue\Attributes\Delay;
use Illuminate\Queue\Attributes\Queue;

#[Connection('sqs')]
#[Queue('listeners')]
#[Delay(60)]
class SendShipmentNotification implements ShouldQueue
{
    // ...
}

如果您希望在運行時定義監聽器的佇列連接、佇列名稱或延遲,可以在監聽器上定義 viaConnectionviaQueuewithDelay 方法:

/**
 * 獲取監聽器的佇列連接名稱。
 */
public function viaConnection(): string
{
    return 'sqs';
}

/**
 * 獲取監聽器的佇列名稱。
 */
public function viaQueue(): string
{
    return 'listeners';
}

/**
 * 獲取在處理作業之前應等待的秒數。
 */
public function withDelay(OrderShipped $event): int
{
    return $event->highPriority ? 0 : 60;
}

有條件地放入佇列監聽器

有時,您可能需要根據僅在運行時可用的某些資料來判斷是否應將監聽器放入佇列。為此,可以向監聽器添加 shouldQueue 方法來判斷是否應將監聽器放入佇列。如果 shouldQueue 方法返回 false,則不會將監聽器放入佇列:

<?php

namespace App\Listeners;

use App\Events\OrderCreated;
use Illuminate\Contracts\Queue\ShouldQueue;

class RewardGiftCard implements ShouldQueue
{
    /**
     * 向客戶獎勵禮品卡。
     */
    public function handle(OrderCreated $event): void
    {
        // ...
    }

    /**
     * 判斷是否應將監聽器放入佇列。
     */
    public function shouldQueue(OrderCreated $event): bool
    {
        return $event->order->subtotal >= 5000;
    }
}

手動與佇列互動

如果您需要手動存取監聽器的底層佇列作業的 deleterelease 方法,可以使用 Illuminate\Queue\InteractsWithQueue trait 來執行此操作。此 trait 在生成的監聽器上預設導入,並提供對這些方法的存取:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    /**
     * 處理事件。
     */
    public function handle(OrderShipped $event): void
    {
        if ($condition) {
            $this->release(30);
        }
    }
}

佇列事件監聽器和資料庫交易

當佇列監聽器在資料庫交易內被派發時,它們可能會在資料庫交易提交之前被佇列處理。當這種情況發生時,在資料庫交易期間對模型或資料庫記錄進行的任何更新可能尚未反映在資料庫中。此外,在交易內建立的任何模型或資料庫記錄可能不存在於資料庫中。如果您的監聽器依賴這些模型,則在處理派發佇列監聽器的作業時可能會發生意外錯誤。

如果您的佇列連接的 after_commit 配置選項設定為 false,您仍然可以透過在監聽器類別上實現 ShouldQueueAfterCommit 介面來指示特定的佇列監聽器應在所有打開的資料庫交易提交後被派發:

<?php

namespace App\Listeners;

use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
use Illuminate\Queue\InteractsWithQueue;

class SendShipmentNotification implements ShouldQueueAfterCommit
{
    use InteractsWithQueue;
}

[!NOTE] 要了解更多關於解決這些問題的資訊,請查閱有關佇列作業和資料庫交易的文檔。

佇列監聽器中介層

佇列監聽器還可以使用作業中介層。作業中介層允許您在佇列監聽器的執行周圍包裝自訂邏輯,從而減少監聽器本身的樣板代碼。建立作業中介層後,可以透過從監聽器的 middleware 方法返回它們將它們附加到監聽器:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use App\Jobs\Middleware\RateLimited;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendShipmentNotification implements ShouldQueue
{
    /**
     * 處理事件。
     */
    public function handle(OrderShipped $event): void
    {
        // 處理事件...
    }

    /**
     * 獲取監聽器應透過的中介層。
     *
     * @return array
     */
    public function middleware(OrderShipped $event): array
    {
        return [new RateLimited];
    }
}

加密的佇列監聽器

Laravel 允許您透過加密來確保佇列監聽器資料的隱私性和完整性。首先,只需向監聽器類別添加 ShouldBeEncrypted 介面。一旦將此介面添加到類別中,Laravel 將在將監聽器推送到佇列之前自動加密它:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendShipmentNotification implements ShouldQueue, ShouldBeEncrypted
{
    // ...
}

唯一的事件監聽器

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

有時,您可能希望確保在任何時候只有一個特定監聽器的實例在佇列上。您可以透過在監聽器類別上實現 ShouldBeUnique 介面來執行此操作:

<?php

namespace App\Listeners;

use App\Events\LicenseSaved;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;

class AcquireProductKey implements ShouldQueue, ShouldBeUnique
{
    public function __invoke(LicenseSaved $event): void
    {
        // ...
    }
}

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

在某些情況下,您可能希望定義一個特定的「金鑰」使監聽器唯一,或者您可能希望指定一個逾時時間,超過該時間後監聽器不再保持唯一。為此,您可以在監聽器類別上定義 uniqueIduniqueFor 屬性或方法。這些方法接收事件實例,允許您使用事件資料來建立返回值:

<?php

namespace App\Listeners;

use App\Events\LicenseSaved;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;

class AcquireProductKey implements ShouldQueue, ShouldBeUnique
{
    /**
     * 監聽器的唯一鎖將被釋放的秒數。
     *
     * @var int
     */
    public $uniqueFor = 3600;

    public function __invoke(LicenseSaved $event): void
    {
        // ...
    }

    /**
     * 獲取監聽器的唯一 ID。
     */
    public function uniqueId(LicenseSaved $event): string
    {
        return 'listener:'.$event->license->id;
    }
}

在上面的範例中,AcquireProductKey 監聽器按許可證 ID 唯一。因此,任何相同許可證的新派發都將被忽略,直到現有監聽器完成處理。這防止了為相同許可證獲取重複的產品密鑰。此外,如果現有監聽器在一小時內未被處理,則唯一鎖將被釋放,並且另一個具有相同唯一金鑰的監聽器可以被放入佇列。

[!WARNING] 如果您的應用程式從多個 Web 伺服器或容器派發事件,則應確保所有伺服器都與同一中央快取伺服器通訊,以便 Laravel 可以準確判斷監聽器是否唯一。

保持監聽器唯一直到處理開始

預設情況下,唯一的監聽器在監聽器完成處理或失敗其所有重試嘗試後被「解鎖」。但是,可能有些情況您希望您的監聽器在被處理之前立即解鎖。為此,您的監聽器應該實現 ShouldBeUniqueUntilProcessing 合約而不是 ShouldBeUnique 合約:

<?php

namespace App\Listeners;

use App\Events\LicenseSaved;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Contracts\Queue\ShouldQueue;

class AcquireProductKey implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
    // ...
}

唯一監聽器鎖

在幕後,當派發 ShouldBeUnique 監聽器時,Laravel 嘗試使用 uniqueId 鍵獲取一個。如果鎖已被持有,則不會派發該監聽器。當監聽器完成處理或失敗其所有重試嘗試時,釋放此鎖。預設情況下,Laravel 將使用預設快取驅動程式來獲取此鎖。但是,如果您希望使用另一個驅動程式來獲取鎖,可以定義一個 uniqueVia 方法,該方法應返回用於獲取鎖的快取驅動程式:

<?php

namespace App\Listeners;

use App\Events\LicenseSaved;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;

class AcquireProductKey implements ShouldQueue, ShouldBeUnique
{
    // ...

    /**
     * 獲取用於唯一監聽器鎖的快取驅動程式。
     */
    public function uniqueVia(LicenseSaved $event): Repository
    {
        return Cache::driver('redis');
    }
}

[!NOTE] 如果您只需要限制監聽器的並發處理,請改用 WithoutOverlapping 作業中介層。

處理失敗的作業

有時您的佇列事件監聽器可能會失敗。如果佇列監聽器超過佇列工作者定義的最大嘗試次數,將在您的監聽器上呼叫 failed 方法。failed 方法接收事件實例和導致失敗的 Throwable

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Throwable;

class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    /**
     * 處理事件。
     */
    public function handle(OrderShipped $event): void
    {
        // ...
    }

    /**
     * 處理作業失敗。
     */
    public function failed(OrderShipped $event, Throwable $exception): void
    {
        // ...
    }
}

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

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

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

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\InteractsWithQueue;

#[Tries(5)]
class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    // ...
}

作為定義在失敗之前可以嘗試監聽器次數的替代方案,您可以定義不應再嘗試監聽器的時間。這允許監聽器在給定的時間框架內被嘗試任意次數。要定義不應再嘗試監聽器的時間,請向您的監聽器類別添加 retryUntil 方法。此方法應返回一個 DateTimeInterface 實例:

use DateTimeInterface;

/**
 * 判斷監聽器應逾時的時間。
 */
public function retryUntil(): DateTimeInterface
{
    return now()->plus(minutes: 5);
}

如果同時定義了 retryUntiltries,Laravel 優先使用 retryUntil 方法。

指定佇列監聽器退避

如果您希望配置 Laravel 在重試遇到異常的監聽器之前應等待的秒數,可以使用監聽器類別上的 Backoff 屬性:

<?php

namespace App\Listeners;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Backoff;

#[Backoff(3)]
class SendShipmentNotification implements ShouldQueue
{
    // ...
}

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

/**
 * 計算在重試佇列監聽器之前應等待的秒數。
 */
public function backoff(OrderShipped $event): int
{
    return 3;
}

您可以透過從 backoff 方法返回一個退避值陣列來輕鬆配置「指數」退避。在此範例中,重試延遲將為第一次重試 1 秒,第二次重試 5 秒,第三次重試 10 秒,如果還有更多剩餘嘗試,則每次後續重試 10 秒:

/**
 * 計算在重試佇列監聽器之前應等待的秒數。
 *
 * @return list
 */
public function backoff(OrderShipped $event): array
{
    return [1, 5, 10];
}

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

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

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\MaxExceptions;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\InteractsWithQueue;

#[Tries(25)]
#[MaxExceptions(3)]
class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    /**
     * 處理事件。
     */
    public function handle(OrderShipped $event): void
    {
        // 處理事件...
    }
}

在此範例中,監聽器將被重試最多 25 次。但是,如果監聽器拋出三個未處理的異常,則監聽器將失敗。

指定佇列監聽器逾時

通常,您大致知道您預期佇列監聽器需要多長時間。為此,Laravel 允許您指定一個「逾時」值。如果監聽器處理的時間超過逾時值指定的秒數,則處理該監聽器的工作者將以錯誤退出。您可以使用監聽器類別上的 Timeout 屬性來定義允許監聽器執行的最大秒數:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Timeout;

#[Timeout(120)]
class SendShipmentNotification implements ShouldQueue
{
    // ...
}

如果您希望指示在逾時時應將監聽器標記為失敗,可以在監聽器類別上使用 FailOnTimeout 屬性:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\FailOnTimeout;

#[FailOnTimeout]
class SendShipmentNotification implements ShouldQueue
{
    // ...
}

派發事件

要派發事件,您可以呼叫事件上的靜態 dispatch 方法。此方法由 Illuminate\Foundation\Events\Dispatchable trait 在事件上提供。傳遞給 dispatch 方法的任何參數都將傳遞給事件的構造函數:

<?php

namespace App\Http\Controllers;

use App\Events\OrderShipped;
use App\Models\Order;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

class OrderShipmentController extends Controller
{
    /**
     * 發貨給定訂單。
     */
    public function store(Request $request): RedirectResponse
    {
        $order = Order::findOrFail($request->order_id);

        // 訂單發貨邏輯...

        OrderShipped::dispatch($order);

        return redirect('/orders');
    }
}

如果您希望有條件地派發事件,可以使用 dispatchIfdispatchUnless 方法:

OrderShipped::dispatchIf($condition, $order);

OrderShipped::dispatchUnless($condition, $order);

[!NOTE] 測試時,斷言某些事件已派發而不實際觸發其監聽器可能很有用。Laravel 的內建測試輔助工具使其變得非常簡單。

在資料庫交易後派發事件

有時,您可能希望指示 Laravel 僅在活動資料庫交易提交後派發事件。為此,您可以在事件類別上實現 ShouldDispatchAfterCommit 介面。

此介面指示 Laravel 在當前資料庫交易提交之前不要派發事件。如果交易失敗,則事件將被丟棄。如果在派發事件時沒有進行資料庫交易,則事件將立即被派發:

<?php

namespace App\Events;

use App\Models\Order;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderShipped implements ShouldDispatchAfterCommit
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * 建立一個新的事件實例。
     */
    public function __construct(
        public Order $order,
    ) {}
}

延遲事件

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

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

use App\Models\User;
use Illuminate\Support\Facades\Event;

Event::defer(function () {
    $user = User::create(['name' => 'Victoria Otwell']);

    $user->posts()->create(['title' => 'My first post!']);
});

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

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

use App\Models\User;
use Illuminate\Support\Facades\Event;

Event::defer(function () {
    $user = User::create(['name' => 'Victoria Otwell']);

    $user->posts()->create(['title' => 'My first post!']);
}, ['eloquent.created: '.User::class]);

事件訂閱者

編寫事件訂閱者

事件訂閱者是可以從訂閱者類別本身訂閱多個事件的類別,允許您在單個類別中定義多個事件處理器。訂閱者應定義一個 subscribe 方法,該方法接收一個事件派發器實例。您可以呼叫給定派發器上的 listen 方法來註冊事件監聽器:

<?php

namespace App\Listeners;

use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use Illuminate\Events\Dispatcher;

class UserEventSubscriber
{
    /**
     * 處理使用者登入事件。
     */
    public function handleUserLogin(Login $event): void {}

    /**
     * 處理使用者登出事件。
     */
    public function handleUserLogout(Logout $event): void {}

    /**
     * 註冊訂閱者的監聽器。
     */
    public function subscribe(Dispatcher $events): void
    {
        $events->listen(
            Login::class,
            [UserEventSubscriber::class, 'handleUserLogin']
        );

        $events->listen(
            Logout::class,
            [UserEventSubscriber::class, 'handleUserLogout']
        );
    }
}

如果您在訂閱者本身中定義了事件監聽器方法,您會發現從訂閱者的 subscribe 方法返回一個包含事件和方法名稱的陣列更為方便。Laravel 在註冊事件監聽器時將自動判斷訂閱者的類別名稱:

<?php

namespace App\Listeners;

use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use Illuminate\Events\Dispatcher;

class UserEventSubscriber
{
    /**
     * 處理使用者登入事件。
     */
    public function handleUserLogin(Login $event): void {}

    /**
     * 處理使用者登出事件。
     */
    public function handleUserLogout(Logout $event): void {}

    /**
     * 註冊訂閱者的監聽器。
     *
     * @return array
     */
    public function subscribe(Dispatcher $events): array
    {
        return [
            Login::class => 'handleUserLogin',
            Logout::class => 'handleUserLogout',
        ];
    }
}

註冊事件訂閱者

編寫訂閱者後,如果處理器方法遵循 Laravel 的事件發現慣例,Laravel 將自動註冊訂閱者中的處理器方法。否則,您可以使用 Event 外觀的 subscribe 方法手動註冊訂閱者。通常,這應該在應用程式的 AppServiceProviderboot 方法中完成:

<?php

namespace App\Providers;

use App\Listeners\UserEventSubscriber;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * 啟動任何應用程式服務。
     */
    public function boot(): void
    {
        Event::subscribe(UserEventSubscriber::class);
    }
}

測試

測試派發事件的代碼時,您可能希望指示 Laravel 不要實際執行事件的監聽器,因為可以單獨且獨立於派發相應事件的代碼來測試監聽器的代碼。當然,要測試監聽器本身,您可以實例化一個監聽器實例並在測試中直接呼叫 handle 方法。

使用 Event 外觀的 fake 方法,您可以阻止監聽器執行,執行正在測試的代碼,然後使用 assertDispatchedassertNotDispatchedassertNothingDispatched 方法斷言應用程式派發了哪些事件:

<?php

use App\Events\OrderFailedToShip;
use App\Events\OrderShipped;
use Illuminate\Support\Facades\Event;

test('orders can be shipped', function () {
    Event::fake();

    // 執行訂單發貨...

    // 斷言事件已派發...
    Event::assertDispatched(OrderShipped::class);

    // 斷言事件已派發兩次...
    Event::assertDispatched(OrderShipped::class, 2);

    // 斷言事件已派發一次...
    Event::assertDispatchedOnce(OrderShipped::class);

    // 斷言事件未派發...
    Event::assertNotDispatched(OrderFailedToShip::class);

    // 斷言沒有派發任何事件...
    Event::assertNothingDispatched();
});
<?php

namespace Tests\Feature;

use App\Events\OrderFailedToShip;
use App\Events\OrderShipped;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * 測試訂單發貨。
     */
    public function test_orders_can_be_shipped(): void
    {
        Event::fake();

        // 執行訂單發貨...

        // 斷言事件已派發...
        Event::assertDispatched(OrderShipped::class);

        // 斷言事件已派發兩次...
        Event::assertDispatched(OrderShipped::class, 2);

        // 斷言事件已派發一次...
        Event::assertDispatchedOnce(OrderShipped::class);

        // 斷言事件未派發...
        Event::assertNotDispatched(OrderFailedToShip::class);

        // 斷言沒有派發任何事件...
        Event::assertNothingDispatched();
    }
}

您可以向 assertDispatchedassertNotDispatched 方法傳遞一個閉包,以斷言派發了一個通過給定「真理測試」的事件。如果至少派發了一個通過給定真理測試的事件,則斷言將成功:

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

如果您只想斷言一個事件監聽器正在監聽給定的事件,可以使用 assertListening 方法:

Event::assertListening(
    OrderShipped::class,
    SendShipmentNotification::class
);

[!WARNING] 呼叫 Event::fake() 後,不會執行任何事件監聽器。因此,如果您的測試使用依賴事件的模型工廠(例如在模型的 creating 事件中建立 UUID),您應該在使用工廠之後呼叫 Event::fake()

偽造部分事件

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

test('orders can be processed', function () {
    Event::fake([
        OrderCreated::class,
    ]);

    $order = Order::factory()->create();

    Event::assertDispatched(OrderCreated::class);

    // 其他事件正常派發...
    $order->update([
        // ...
    ]);
});
/**
 * 測試訂單處理。
 */
public function test_orders_can_be_processed(): void
{
    Event::fake([
        OrderCreated::class,
    ]);

    $order = Order::factory()->create();

    Event::assertDispatched(OrderCreated::class);

    // 其他事件正常派發...
    $order->update([
        // ...
    ]);
}

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

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

範圍事件偽造

如果您只想為測試的一部分偽造事件監聽器,可以使用 fakeFor 方法:

<?php

use App\Events\OrderCreated;
use App\Models\Order;
use Illuminate\Support\Facades\Event;

test('orders can be processed', function () {
    $order = Event::fakeFor(function () {
        $order = Order::factory()->create();

        Event::assertDispatched(OrderCreated::class);

        return $order;
    });

    // 事件正常派發,觀察者將運行...
    $order->update([
        // ...
    ]);
});
<?php

namespace Tests\Feature;

use App\Events\OrderCreated;
use App\Models\Order;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * 測試訂單處理。
     */
    public function test_orders_can_be_processed(): void
    {
        $order = Event::fakeFor(function () {
            $order = Order::factory()->create();

            Event::assertDispatched(OrderCreated::class);

            return $order;
        });

        // 事件正常派發,觀察者將運行...
        $order->update([
            // ...
        ]);
    }
}