LaravelDocs(中文)

事件 (Events)

Laravel events 提供簡單的觀察者模式實作

簡介 (Introduction)

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

事件是解耦應用程式各個方面的好方法,因為單個事件可以有多個不相互依賴的監聽器。例如,你可能希望每次訂單發貨時向使用者發送 Slack 通知。你可以觸發一個 App\Events\OrderShipped 事件,監聽器可以接收該事件並用它來分派 Slack 通知,而不是將訂單處理程式碼與 Slack 通知程式碼耦合。

產生事件和監聽器 (Generating Events and Listeners)

要快速產生事件和監聽器,你可以使用 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

註冊事件和監聽器 (Registering Events and Listeners)

事件發現 (Event Discovery)

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

use App\Events\PodcastProcessed;

class SendPodcastNotification
{
    /**
     * Handle the event.
     */
    public function handle(PodcastProcessed $event): void
    {
        // ...
    }
}

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

/**
 * Handle the event.
 */
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

生產環境中的事件發現 (Event Discovery In Production)

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

手動註冊事件 (Manually Registering Events)

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

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

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Event::listen(
        PodcastProcessed::class,
        SendPodcastNotification::class,
    );
}

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

php artisan event:list

閉包監聽器 (Closure Listeners)

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

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

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Event::listen(function (PodcastProcessed $event) {
        // ...
    });
}

可佇列的匿名事件監聽器 (Queuable Anonymous Event Listeners)

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

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

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Event::listen(queueable(function (PodcastProcessed $event) {
        // ...
    }));
}

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

Event::listen(queueable(function (PodcastProcessed $event) {
    // ...
})->onConnection('redis')->onQueue('podcasts')->delay(now()->addSeconds(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) {
    // The queued listener failed...
}));

萬用字元事件監聽器 (Wildcard Event Listeners)

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

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

定義事件 (Defining Events)

事件類別本質上是一個資料容器,它保存與事件相關的資訊。例如,讓我們假設一個 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;

    /**
     * Create a new event instance.
     */
    public function __construct(
        public Order $order,
    ) {}
}

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

定義監聽器 (Defining Listeners)

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

<?php

namespace App\Listeners;

use App\Events\OrderShipped;

class SendShipmentNotification
{
    /**
     * Create the event listener.
     */
    public function __construct() {}

    /**
     * Handle the event.
     */
    public function handle(OrderShipped $event): void
    {
        // Access the order using $event->order...
    }
}

[!NOTE] 你的事件監聽器也可以在其建構函式上類型提示它們需要的任何依賴項。所有事件監聽器都透過 Laravel 服務容器解析,因此依賴項將自動注入。

停止事件的傳播 (Stopping The Propagation Of An Event)

有時,你可能希望停止事件向其他監聽器的傳播。你可以透過從監聽器的 handle 方法返回 false 來完成此操作。

佇列事件監聽器 (Queued Event Listeners)

如果你的監聽器將執行緩慢的任務(如發送電子郵件或發出 HTTP 請求),佇列監聽器可能會有所幫助。在使用佇列監聽器之前,請確保設定你的佇列並在伺服器或本地開發環境上啟動佇列工作程序。

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

<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
    // ...
}

就是這樣!現在,當由此監聽器處理的事件被分派時,監聽器將由事件分派程序使用 Laravel 的佇列系統自動佇列。如果當監聽器由佇列執行時沒有拋出異常,佇列任務將在完成處理後自動刪除。

自訂佇列連線、名稱和延遲 (Customizing The Queue Connection Queue Name)

如果你想自訂事件監聽器的佇列連線、佇列名稱或佇列延遲時間,你可以在監聽器類別上定義 $connection$queue$delay 屬性:

<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
    /**
     * The name of the connection the job should be sent to.
     *
     * @var string|null
     */
    public $connection = 'sqs';

    /**
     * The name of the queue the job should be sent to.
     *
     * @var string|null
     */
    public $queue = 'listeners';

    /**
     * The time (seconds) before the job should be processed.
     *
     * @var int
     */
    public $delay = 60;
}

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

/**
 * Get the name of the listener's queue connection.
 */
public function viaConnection(): string
{
    return 'sqs';
}

/**
 * Get the name of the listener's queue.
 */
public function viaQueue(): string
{
    return 'listeners';
}

/**
 * Get the number of seconds before the job should be processed.
 */
public function withDelay(OrderShipped $event): int
{
    return $event->highPriority ? 0 : 60;
}

條件式佇列監聽器 (Conditionally Queueing Listeners)

有時,你可能需要根據一些僅在執行時期可用的資料來確定監聽器是否應該被佇列。要完成此操作,可以將 shouldQueue 方法新增到監聽器以確定監聽器是否應該被佇列。如果 shouldQueue 方法返回 false,監聽器將不會被佇列:

<?php

namespace App\Listeners;

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

class RewardGiftCard implements ShouldQueue
{
    /**
     * Reward a gift card to the customer.
     */
    public function handle(OrderCreated $event): void
    {
        // ...
    }

    /**
     * Determine whether the listener should be queued.
     */
    public function shouldQueue(OrderCreated $event): bool
    {
        return $event->order->subtotal >= 5000;
    }
}

手動與佇列互動 (Manually Interacting With the Queue)

如果你需要手動存取監聽器底層佇列任務的 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;

    /**
     * Handle the event.
     */
    public function handle(OrderShipped $event): void
    {
        if ($condition) {
            $this->release(30);
        }
    }
}

佇列事件監聽器和資料庫交易 (Queued Event Listeners and Database Transactions)

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

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

<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueueAfterCommit
{
    use InteractsWithQueue;
}

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

佇列監聽器中介軟體 (Queued Listener Middleware)

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

<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
    /**
     * Handle the event.
     */
    public function handle(OrderShipped $event): void
    {
        // Process the event...
    }

    /**
     * Get the middleware the listener should pass through.
     *
     * @return array<int, object>
     */
    public function middleware(OrderShipped $event): array
    {
        return [new RateLimited];
    }
}

加密的佇列監聽器 (Encrypted Queued Listeners)

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
{
    // ...
}

處理失敗的任務 (Handling Failed Jobs)

有時你的佇列事件監聽器可能會失敗。如果佇列監聽器超過佇列工作程序定義的最大嘗試次數,將在你的監聽器上呼叫 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;

    /**
     * Handle the event.
     */
    public function handle(OrderShipped $event): void
    {
        // ...
    }

    /**
     * Handle a job failure.
     */
    public function failed(OrderShipped $event, Throwable $exception): void
    {
        // ...
    }
}

指定佇列監聽器最大嘗試次數 (Specifying Queued Listener Maximum Attempts)

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

你可以在監聽器類別上定義 tries 屬性或方法來指定在被視為失敗之前可以嘗試監聽器多少次:

<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    /**
     * The number of times the queued listener may be attempted.
     *
     * @var int
     */
    public $tries = 5;
}

作為定義監聽器在失敗之前可以嘗試多少次的替代方案,你可以定義一個時間,在此時間之後不應再嘗試監聽器。這允許在給定的時間框內嘗試監聽器任意次數。要定義不應再嘗試監聽器的時間,請將 retryUntil 方法新增到你的監聽器類別。此方法應該返回一個 DateTime 實例:

use DateTime;

/**
 * Determine the time at which the listener should timeout.
 */
public function retryUntil(): DateTime
{
    return now()->addMinutes(5);
}

如果 retryUntiltries 都被定義,Laravel 會優先使用 retryUntil 方法。

指定佇列監聽器退避 (Specifying Queued Listener Backoff)

如果你想設定 Laravel 在重試遇到異常的監聽器之前應等待多少秒,你可以在監聽器類別上定義 backoff 屬性:

/**
 * The number of seconds to wait before retrying the queued listener.
 *
 * @var int
 */
public $backoff = 3;

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

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

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

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

指定佇列監聽器最大異常數 (Specifying Queued Listener Max Exceptions)

有時你可能希望指定佇列監聽器可以嘗試多次,但如果重試是由給定數量的未處理異常觸發的(而不是直接由 release 方法釋放),則應該失敗。要完成此操作,你可以在監聽器類別上定義 maxExceptions 屬性:

<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    /**
     * The number of times the queued listener may be attempted.
     *
     * @var int
     */
    public $tries = 25;

    /**
     * The maximum number of unhandled exceptions to allow before failing.
     *
     * @var int
     */
    public $maxExceptions = 3;

    /**
     * Handle the event.
     */
    public function handle(OrderShipped $event): void
    {
        // Process the event...
    }
}

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

指定佇列監聽器逾時 (Specifying Queued Listener Timeout)

通常,你大致知道你期望佇列監聽器需要多長時間。因此,Laravel 允許你指定「逾時」值。如果監聽器處理的時間超過逾時值指定的秒數,處理監聽器的工作程序將以錯誤退出。你可以透過在監聽器類別上定義 timeout 屬性來定義監聽器允許執行的最大秒數:

<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
    /**
     * The number of seconds the listener can run before timing out.
     *
     * @var int
     */
    public $timeout = 120;
}

如果你想指示監聽器應該在逾時時被標記為失敗,你可以在監聽器類別上定義 failOnTimeout 屬性:

<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
    /**
     * Indicate if the listener should be marked as failed on timeout.
     *
     * @var bool
     */
    public $failOnTimeout = true;
}

分派事件 (Dispatching Events)

要分派一個事件,你可以在事件上呼叫靜態 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
{
    /**
     * Ship the given order.
     */
    public function store(Request $request): RedirectResponse
    {
        $order = Order::findOrFail($request->order_id);

        // Order shipment logic...

        OrderShipped::dispatch($order);

        return redirect('/orders');
    }
}

如果你想有條件地分派一個事件,你可以使用 dispatchIfdispatchUnless 方法:

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

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

[!NOTE] 當測試時,斷言某些事件已被分派而不實際觸發它們的監聽器可能會有所幫助。Laravel 的內建測試輔助函式讓這變得輕而易舉。

在資料庫交易後分派事件 (Dispatching Events After Database Transactions)

有時,你可能希望指示 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;

    /**
     * Create a new event instance.
     */
    public function __construct(
        public Order $order,
    ) {}
}

延遲事件 (Deferring Events)

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

要延遲事件,請向 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]);

事件訂閱者 (Event Subscribers)

編寫事件訂閱者 (Writing Event Subscribers)

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

<?php

namespace App\Listeners;

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

class UserEventSubscriber
{
    /**
     * Handle user login events.
     */
    public function handleUserLogin(Login $event): void {}

    /**
     * Handle user logout events.
     */
    public function handleUserLogout(Logout $event): void {}

    /**
     * Register the listeners for the subscriber.
     */
    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
{
    /**
     * Handle user login events.
     */
    public function handleUserLogin(Login $event): void {}

    /**
     * Handle user logout events.
     */
    public function handleUserLogout(Logout $event): void {}

    /**
     * Register the listeners for the subscriber.
     *
     * @return array<string, string>
     */
    public function subscribe(Dispatcher $events): array
    {
        return [
            Login::class => 'handleUserLogin',
            Logout::class => 'handleUserLogout',
        ];
    }
}

註冊事件訂閱者 (Registering Event Subscribers)

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

<?php

namespace App\Providers;

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

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Event::subscribe(UserEventSubscriber::class);
    }
}

測試 (Testing)

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

使用 Event facade 的 fake 方法,你可以防止監聽器執行,執行測試中的程式碼,然後使用 assertDispatchedassertNotDispatchedassertNothingDispatched 方法斷言你的應用程式分派了哪些事件:

<?php

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

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

    // Perform order shipping...

    // Assert that an event was dispatched...
    Event::assertDispatched(OrderShipped::class);

    // Assert an event was dispatched twice...
    Event::assertDispatched(OrderShipped::class, 2);

    // Assert an event was dispatched once...
    Event::assertDispatchedOnce(OrderShipped::class);

    // Assert an event was not dispatched...
    Event::assertNotDispatched(OrderFailedToShip::class);

    // Assert that no events were dispatched...
    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
{
    /**
     * Test order shipping.
     */
    public function test_orders_can_be_shipped(): void
    {
        Event::fake();

        // Perform order shipping...

        // Assert that an event was dispatched...
        Event::assertDispatched(OrderShipped::class);

        // Assert an event was dispatched twice...
        Event::assertDispatched(OrderShipped::class, 2);

        // Assert an event was dispatched once...
        Event::assertDispatchedOnce(OrderShipped::class);

        // Assert an event was not dispatched...
        Event::assertNotDispatched(OrderFailedToShip::class);

        // Assert that no events were dispatched...
        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()

僽造事件子集 (Faking a Subset of Events)

如果你只想為特定的事件集僽造事件監聽器,你可以將它們傳遞給 fakefakeFor 方法:

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

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

    Event::assertDispatched(OrderCreated::class);

    // Other events are dispatched as normal...
    $order->update([
        // ...
    ]);
});
/**
 * Test order process.
 */
public function test_orders_can_be_processed(): void
{
    Event::fake([
        OrderCreated::class,
    ]);

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

    Event::assertDispatched(OrderCreated::class);

    // Other events are dispatched as normal...
    $order->update([
        // ...
    ]);
}

你可以使用 except 方法僽造除了一組指定事件之外的所有事件:

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

作用域事件僽造 (Scoped Events Fakes)

如果你只想為測試的一部分僽造事件監聽器,你可以使用 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;
    });

    // Events are dispatched as normal and observers will run...
    $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
{
    /**
     * Test order process.
     */
    public function test_orders_can_be_processed(): void
    {
        $order = Event::fakeFor(function () {
            $order = Order::factory()->create();

            Event::assertDispatched(OrderCreated::class);

            return $order;
        });

        // Events are dispatched as normal and observers will run...
        $order->update([
            // ...
        ]);
    }
}