跳到內容

事件

簡介

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
{
/**
* Handle the given event.
*/
public function handle(PodcastProcessed $event): void
{
// ...
}
}

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

/**
* Handle the given event.
*/
public function handle(PodcastProcessed|PodcastPublished $event): void
{
// ...
}

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

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

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

php artisan event:list

生產環境中的事件探索

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

手動註冊事件

您可以使用 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

閉包監聽器

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

use App\Events\PodcastProcessed;
use Illuminate\Support\Facades\Event;
 
/**
* Bootstrap any application services.
*/
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;
 
/**
* 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...
}));

萬用字元事件監聽器

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

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;
 
/**
* Create a new event instance.
*/
public function __construct(
public Order $order,
) {}
}

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

定義監聽器

接下來,讓我們看看範例事件的監聽器。事件監聽器會在它們的 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...
}
}
lightbulb

您的事件監聽器也可以在它們的建構函式中類型提示它們需要的任何依賴項。所有事件監聽器都會透過 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 的佇列系統自動將監聽器加入佇列。如果佇列執行監聽器時沒有拋出任何例外,則已加入佇列的工作會在處理完成後自動刪除。

自訂佇列連線、名稱與延遲

如果您想自訂事件監聽器的佇列連線、佇列名稱或佇列延遲時間,您可以在監聽器類別中定義 $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;
}

條件式地將監聽器加入佇列

有時,您可能需要根據只有在執行時才能取得的某些資料,來決定是否應將監聽器加入佇列。為了達成此目的,可以在監聽器中加入 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;
}
}

手動與佇列互動

如果您需要手動存取監聽器底層佇列工作的 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 (true) {
$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;
}
lightbulb

若要深入了解如何解決這些問題,請查閱關於佇列工作與資料庫交易的文件。

處理失敗的任務

有時,您的佇列事件監聽器可能會失敗。如果佇列監聽器超過佇列工作程式定義的最大嘗試次數,則會呼叫監聽器上的 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
{
// ...
}
}

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

如果您的佇列監聽器之一遇到錯誤,您可能不希望它無限期地重試。因此,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);
}

指定佇列監聽器的退避

如果您想設定 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(): int
{
return 3;
}

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

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

分發事件

若要分派事件,您可以呼叫事件上的靜態 dispatch 方法。此方法由 Illuminate\Foundation\Events\Dispatchable trait 在事件上提供。傳遞給 dispatch 方法的任何引數都會傳遞給事件的建構子。

<?php
 
namespace App\Http\Controllers;
 
use App\Events\OrderShipped;
use App\Http\Controllers\Controller;
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);
lightbulb

在測試時,斷言某些事件已分派,而無需實際觸發其監聽器會很有幫助。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;
 
/**
* Create a new event instance.
*/
public function __construct(
public Order $order,
) {}
}

事件訂閱者

撰寫事件訂閱者

事件訂閱者是可以從訂閱者類別本身訂閱多個事件的類別,讓您可以在單一類別中定義多個事件處理常式。訂閱者應定義 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',
];
}
}

註冊事件訂閱者

在編寫訂閱者之後,如果它們遵循 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);
}
}

測試

在測試分派事件的程式碼時,您可能希望指示 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 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 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
);
exclamation

在呼叫 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);
 
// 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,
]);

範圍化事件偽造

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