事件系统
这是一篇协同翻译的文章,你可以点击『我来翻译』按钮来参与翻译。
事件
简介
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
{
// 处理事件...
}
}
In this example, the listener will be retried up to 25 times. However, the listener will fail if three unhandled exceptions are thrown by the listener.
Specifying Queued Listener Timeout
Often, you know roughly how long you expect your queued listeners to take. For this reason, Laravel allows you to specify a "timeout" value. If a listener is processing for longer than the number of seconds specified by the timeout value, the worker processing the listener will exit with an error. You may define the maximum number of seconds a listener should be allowed to run by using the Timeout attribute on your listener class:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Timeout;
#[Timeout(120)]
class SendShipmentNotification implements ShouldQueue
{
// ...
}
If you would like to indicate that a listener should be marked as failed on timeout, you may use the FailOnTimeout attribute on the listener class:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\FailOnTimeout;
#[FailOnTimeout]
class SendShipmentNotification implements ShouldQueue
{
// ...
}
Dispatching Events
To dispatch an event, you may call the static dispatch method on the event. This method is made available on the event by the Illuminate\Foundation\Events\Dispatchable trait. Any arguments passed to the dispatch method will be passed to the event's constructor:
<?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');
}
}
If you would like to conditionally dispatch an event, you may use the dispatchIf and dispatchUnless methods:
OrderShipped::dispatchIf($condition, $order);
OrderShipped::dispatchUnless($condition, $order);
[!NOTE]
When testing, it can be helpful to assert that certain events were dispatched without actually triggering their listeners. Laravel's built-in testing helpers make it a cinch.
Dispatching Events After Database Transactions
Sometimes, you may want to instruct Laravel to only dispatch an event after the active database transaction has committed. To do so, you may implement the ShouldDispatchAfterCommit interface on the event class.
This interface instructs Laravel to not dispatch the event until the current database transaction is committed. If the transaction fails, the event will be discarded. If no database transaction is in progress when the event is dispatched, the event will be dispatched immediately:
<?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
Deferred events allow you to delay the dispatching of model events and execution of event listeners until after a specific block of code has completed. This is particularly useful when you need to ensure that all related records are created before event listeners are triggered.
To defer events, provide a closure to the Event::defer() method:
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!']);
});
All events triggered within the closure will be dispatched after the closure is executed. This ensures that event listeners have access to all related records that were created during the deferred execution. If an exception occurs within the closure, the deferred events will not be dispatched.
To defer only specific events, pass an array of events as the second argument to the defer method:
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
Event subscribers are classes that may subscribe to multiple events from within the subscriber class itself, allowing you to define several event handlers within a single class. Subscribers should define a subscribe method, which receives an event dispatcher instance. You may call the listen method on the given dispatcher to register event listeners:
<?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']
);
}
}
If your event listener methods are defined within the subscriber itself, you may find it more convenient to return an array of events and method names from the subscriber's subscribe method. Laravel will automatically determine the subscriber's class name when registering the event listeners:
<?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
After writing the subscriber, Laravel will automatically register handler methods within the subscriber if they follow Laravel's event discovery conventions. Otherwise, you may manually register your subscriber using the subscribe method of the Event facade. Typically, this should be done within the boot method of your application's AppServiceProvider:
<?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
When testing code that dispatches events, you may wish to instruct Laravel to not actually execute the event's listeners, since the listener's code can be tested directly and separately of the code that dispatches the corresponding event. Of course, to test the listener itself, you may instantiate a listener instance and invoke the handle method directly in your test.
Using the Event facade's fake method, you may prevent listeners from executing, execute the code under test, and then assert which events were dispatched by your application using the assertDispatched, assertNotDispatched, and assertNothingDispatched methods:
<?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();
}
}
You may pass a closure to the assertDispatched or assertNotDispatched methods in order to assert that an event was dispatched that passes a given "truth test". If at least one event was dispatched that passes the given truth test then the assertion will be successful:
Event::assertDispatched(function (OrderShipped $event) use ($order) {
return $event->order->id === $order->id;
});
If you would simply like to assert that an event listener is listening to a given event, you may use the assertListening method:
Event::assertListening(
OrderShipped::class,
SendShipmentNotification::class
);
[!WARNING]
After callingEvent::fake(), no event listeners will be executed. So, if your tests use model factories that rely on events, such as creating a UUID during a model'screatingevent, you should callEvent::fake()after using your factories.
Faking a Subset of Events
If you only want to fake event listeners for a specific set of events, you may pass them to the fake or fakeFor method:
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([
// ...
]);
}
You may fake all events except for a set of specified events using the except method:
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
推荐文章: