跳转至内容

活动

简介

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}