事件系统
这是一篇协同翻译的文章,你可以点击『我来翻译』按钮来参与翻译。
事件
简介
Laravel 的事件提供了一个简单的观察者模式实现,允许你订阅并监听应用程序中发生的各种事件。事件类通常存储在 app/Events 目录中,而它们的监听器则存储在 app/Listeners 中。如果你在应用程序中没有看到这些目录,也不用担心,因为当你使用 Artisan 控制台命令生成事件和监听器时,这些目录会自动为你创建。
事件是解耦应用程序各个方面的一种非常好的方式,因为单个事件可以拥有多个彼此不依赖的监听器。例如,你可能希望每次订单发货时都向用户发送一条 Slack 通知。与其将订单处理代码与 Slack 通知代码耦合在一起,不如触发一个 App\Events\OrderShipped 事件,由监听器接收该事件并用它来分发 Slack 通知。
生成事件和监听器
为了快速生成事件和监听器,你可以使用 make:event 和 make:listener Artisan 命令:
php artisan make:event PodcastProcessed
php artisan make:listener SendPodcastNotification --event=PodcastProcessed
为方便起见,你也可以在不提供额外参数的情况下调用 make:event 和 make: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
生产环境中的事件发现
为了提升应用程序的运行速度,你应该使用 optimize 或 event:cache Artisan 命令缓存应用程序所有监听器的清单。通常,该命令应该作为应用程序部署流程的一部分运行。框架会使用这个清单来加快事件注册过程。可以使用 event:clear 命令销毁事件缓存。
手动注册事件
使用 Event 门面,你可以在应用程序 AppServiceProvider 的 boot 方法中手动注册事件及其对应的监听器:
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
闭包监听器
通常,监听器被定义为类;不过,你也可以在应用程序 AppServiceProvider 的 boot 方法中手动注册基于闭包的事件监听器:
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) {
// ...
}));
}
与队列任务类似,你可以使用 onConnection、onQueue 和 delay 方法来自定义队列监听器的执行:
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 访问订单...
}
}
[!注意]
你的事件监听器也可以在构造函数中对其所需的任何依赖进行类型提示。所有事件监听器都会通过 Laravel 服务容器进行解析,因此依赖会被自动注入。
停止事件传播
有时,你可能希望停止事件继续传播到其他监听器。你可以通过在监听器的 handle 方法中返回 false 来实现。
队列事件监听器
如果你的监听器需要执行较慢的任务,例如发送电子邮件或发起 HTTP 请求,那么将监听器放入队列会很有帮助。在使用队列监听器之前,请确保已经配置队列,并在服务器或本地开发环境中启动队列 worker。
要指定某个监听器应进入队列,请为监听器类添加 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;
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
{
// ...
}
如果你想在运行时定义监听器的队列连接、队列名称或延迟时间,可以在监听器中定义 viaConnection、viaQueue 或 withDelay 方法:
/**
* 获取监听器的队列连接名称。
*/
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;
}
}
手动与队列交互
如果你需要手动访问监听器底层队列任务的 delete 和 release 方法,可以使用 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;
}
[!注意]
若要了解有关如何规避这些问题的更多信息,请查看有关队列任务和数据库事务的文档。
队列监听器中间件
队列监听器也可以使用任务中间件。任务中间件允许你在队列监听器执行的前后封装自定义逻辑,从而减少监听器本身中的样板代码。创建任务中间件后,可以通过监听器的 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<int, object>
*/
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
{
// ...
}
唯一事件监听器
[!警告]
唯一监听器需要使用支持锁的缓存驱动。目前,memcached、redis、dynamodb、database、file和array缓存驱动支持原子锁。
有时,你可能希望确保在任意时间点,队列中只存在某个特定监听器的一个实例。你可以通过让监听器类实现 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 监听器是唯一的。因此,如果队列中已经存在该监听器的另一个实例,并且尚未处理完成,那么该监听器将不会再次被加入队列。这样可以确保即使许可证在短时间内被多次保存,每个许可证也只会获取一个产品密钥。
在某些情况下,你可能希望定义一个特定的“键”来使监听器保持唯一,或者指定一个超时时间,超过该时间后监听器将不再保持唯一。为此,你可以在监听器类中定义 uniqueId 和 uniqueFor 属性或方法。这些方法会接收事件实例,因此你可以使用事件数据来构造返回值:
<?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 保持唯一。因此,在现有监听器完成处理之前,针对同一许可证再次分发该监听器的操作都会被忽略。这样可以防止为同一个许可证重复获取产品密钥。此外,如果现有监听器在一小时内没有被处理,唯一锁将被释放,此时可以将具有相同唯一键的另一个监听器加入队列。
[!警告]
如果你的应用程序从多个 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 任务中间件。
有时,你的队列事件监听器可能会执行失败。如果队列监听器超过了队列 worker 所定义的最大尝试次数,则会调用监听器上的 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);
}
如果同时定义了 retryUntil 和 tries,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<int>
*/
public function backoff(OrderShipped $event): array
{
return [1, 5, 10];
}
指定队列监听器的最大异常次数
有时,你可能希望指定一个队列监听器可以尝试很多次,但如果重试是由达到一定数量的未处理异常所触发的,则应将其视为失败(而不是通过 release 方法直接释放后重试)。为此,你可以在监听器类上使用 Tries 和 MaxExceptions 属性:
<?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 次。但是,如果监听器抛出了 3 次未处理的异常,那么该监听器将被标记为失败。
指定队列监听器超时时间
通常,你大致知道队列监听器预计需要运行多长时间。因此,Laravel 允许你指定一个“超时”值。
如果监听器的处理时间超过了超时值中指定的秒数,负责处理该监听器的 worker 将以错误状态退出。
你可以在监听器类上使用 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');
}
}
如果你希望根据条件来决定是否分发事件,可以使用 dispatchIf 和 dispatchUnless 方法:
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<string, string>
*/
public function subscribe(Dispatcher $events): array
{
return [
Login::class => 'handleUserLogin',
Logout::class => 'handleUserLogout',
];
}
}
注册事件订阅者
编写好订阅者后,如果其中的处理方法符合 Laravel 的事件发现约定,Laravel 会自动注册这些处理方法。
否则,你也可以使用 Event facade 的 subscribe 方法手动注册订阅者。通常,这应该在应用程序 AppServiceProvider 的 boot 方法中完成:
<?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 facade 的 fake 方法,你可以阻止监听器真正执行,然后运行待测试代码,最后使用 assertDispatched、assertNotDispatched 和 assertNothingDispatched 等方法断言应用程序分发了哪些事件:
<?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();
}
}
你可以向 assertDispatched 或 assertNotDispatched 方法传入一个闭包,用来断言是否分发了一个满足指定“条件测试”的事件。
只要至少有一个已分发的事件通过了这个条件测试,断言就会成功:
Event::assertDispatched(function (OrderShipped $event) use ($order) {
return $event->order->id === $order->id;
});
如果你只是想断言某个事件监听器是否正在监听指定事件,可以使用 assertListening 方法:
Event::assertListening(
OrderShipped::class,
SendShipmentNotification::class
);
[!警告]
调用Event::fake()之后,任何事件监听器都不会被执行。 因此,如果你的测试使用了依赖事件的模型工厂,例如在模型的creating事件中生成 UUID,那么你应该在使用工厂之后再调用Event::fake()。
模拟部分事件
如果你只想模拟某一组特定事件的监听器,可以将这些事件传递给 fake 或 fakeFor 方法:
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,
]);
Scoped Event Fakes
If you only want to fake event listeners for a portion of your test, you may use the fakeFor method:
<?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([
// ...
]);
}
}
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
Laravel 13 中文文档
关于 LearnKu
推荐文章: