跳至内容

Events

介绍

Laravel 的事件提供了一种简单的观察者模式实现,允许您订阅和监听应用程序中发生的各种事件。事件类通常存储在app/Events目录中,而它们的监听器则存储在 目录中app/Listeners。如果您在应用程序中没有看到这些目录,也不用担心,因为它们会在您使用 Artisan 控制台命令生成事件和监听器时自动创建。

事件是解耦应用程序各个方面的绝佳方式,因为单个事件可以有多个互不依赖的监听器。例如,您可能希望每次订单发货时都向用户发送 Slack 通知。与其将订单处理代码与 Slack 通知代码耦合在一起,不如引发一个App\Events\OrderShipped事件,让监听器接收并使用它发送 Slack 通知。

生成事件和监听器

要快速生成事件和监听器,您可以使用make:eventmake:listenerArtisan 命令:

1php artisan make:event PodcastProcessed
2 
3php artisan make:listener SendPodcastNotification --event=PodcastProcessed

为了方便起见,你也可以在不添加任何参数的情况下调用make:eventmake:listenerArtisan 命令。这样做时,Laravel 会自动Prompts你输入类名,并在创建监听器时Prompts你输入要监听的事件:

1php artisan make:event
2 
3php artisan make:listener

注册事件和监听器

事件发现

默认情况下,Laravel 会扫描应用程序目录,自动查找并注册事件监听器。当 Laravel 发现任何以Listeners开头的监听器类方法时,Laravel 会将这些方法注册为方法签名中类型Prompts的事件的监听器: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}

withEvents如果您计划将监听器存储在不同的目录或多个目录中,您可以指示 Laravel 使用应用程序文件中的方法扫描这些目录bootstrap/app.php

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:cacheArtisan 命令缓存所有应用程序监听器的清单。通常,此命令应作为应用程序部署过程的一部分运行。框架将使用此清单来加快事件注册过程。event:clear命令可用于销毁事件缓存。

手动注册事件

使用外观,您可以在应用程序的方法Event中手动注册事件及其相应的侦听器bootAppServiceProvider

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

闭包监听器

boot通常,侦听器被定义为类;但是,您也可以在应用程序的方法中手动注册基于闭包的事件侦听器AppServiceProvider

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}

可排队的匿名事件监听器

当注册基于闭包的事件监听器时,你可以将监听器闭包包装在函数中,以指示 Laravel 使用队列Illuminate\Events\queueable执行监听器

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()->addSeconds(10)));

如果您想处理匿名队列监听器失败的情况,可以catch在定义queueable监听器时为该方法提供一个闭包。此闭包将接收事件实例以及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已购买实例的容器。如果使用 PHP函数(例如使用队列监听器SerializesModels)对事件对象进行序列化,则该事件所使用的 trait 将优雅地序列化任何 Eloquent 模型serialize

定义监听器

接下来,我们来看看示例事件的监听器。事件监听器在其handle方法中接收事件实例。Artisanmake:listener命令在调用时,如果使用选项--event,会自动导入相应的事件类,并在handle方法中Prompts事件的类型。在方法中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}

停止事件传播

有时,您可能希望停止将事件传播到其他侦听器。您可以通过false从侦听器的handle方法返回来实现。

队列事件监听器

如果您的监听器要执行诸如发送电子邮件或发出 HTTP 请求之类的慢速任务,那么队列监听器可能会非常有用。在使用队列监听器之前,请务必在服务器或本地开发环境中配置队列并启动队列工作器。

要指定监听器加入队列,请将ShouldQueue接口添加到监听器类中。Artisanmake:listener命令生成的监听器已将此接口导入到当前命名空间,因此您可以立即使用它:

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 的队列系统将该监听器加入队列。如果队列执行监听器时没有抛出任何异常,则队列中的任务将在处理完成后自动删除。

自定义队列连接、名称和延迟

如果您想要自定义事件监听器的队列连接、队列名称或队列延迟时间,您可以在监听器类上定义$connection$queue或属性:$delay

1<?php
2 
3namespace App\Listeners;
4 
5use App\Events\OrderShipped;
6use Illuminate\Contracts\Queue\ShouldQueue;
7 
8class SendShipmentNotification implements ShouldQueue
9{
10 /**
11 * The name of the connection the job should be sent to.
12 *
13 * @var string|null
14 */
15 public $connection = 'sqs';
16 
17 /**
18 * The name of the queue the job should be sent to.
19 *
20 * @var string|null
21 */
22 public $queue = 'listeners';
23 
24 /**
25 * The time (seconds) before the job should be processed.
26 *
27 * @var int
28 */
29 public $delay = 60;
30}

如果您想在运行时定义监听器的队列连接、队列名称或延迟,您可以在监听器上定义viaConnectionviaQueue或方法:withDelay

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\InteractsWithQueuetrait 来实现。此 trait 在生成的监听器中默认导入,并提供对以下方法的访问:

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 (true) {
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}

处理失败的作业

有时,队列中的事件监听器可能会失败。如果队列监听器超出了队列工作器定义的最大尝试次数,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\InteractsWithQueue;
8 
9class SendShipmentNotification implements ShouldQueue
10{
11 use InteractsWithQueue;
12 
13 /**
14 * The number of times the queued listener may be attempted.
15 *
16 * @var int
17 */
18 public $tries = 5;
19}

除了定义监听器在失败前可以尝试的次数之外,您还可以定义监听器停止尝试的时间。这样,您可以在给定的时间范围内尝试任意次数的监听器。要定义监听器停止尝试的时间,请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()->addMinutes(5);
9}

如果同时定义了retryUntiltries,则 Laravel 优先使用该retryUntil方法。

指定排队侦听器退避

如果您想配置 Laravel 在重试遇到异常的侦听器之前应等待的秒数,您可以通过backoff在侦听器类上定义一个属性来实现:

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

如果您需要更复杂的逻辑来确定侦听器的退避时间,您可以backoff在侦听器类上定义一个方法:

1/**
2 * Calculate the number of seconds to wait before retrying the queued listener.
3 */
4public function backoff(): 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(): array
7{
8 return [1, 5, 10];
9}

调度事件

要分派事件,可以调用dispatch事件的静态方法。此方法通过 trait 在事件上可用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内置的测试Helpers让这一切变得轻而易举。

数据库事务后调度事件

有时,你可能希望 Laravel 仅在Events数据库事务提交后才派发事件。为此,你可以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}

事件订阅者

编写事件订阅者

事件订阅者是可以在其内部订阅多个事件的类,允许你在单个类中定义多个事件处理程序。订阅者应该定义一个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 的事件发现约定subscribe,Laravel 将自动在订阅器中注册它们。否则,您可以使用外观层的方法手动注册订阅器。通常,这应该在应用程序的 方法Event中完成bootAppServiceProvider

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 not dispatched...
19 Event::assertNotDispatched(OrderFailedToShip::class);
20 
21 // Assert that no events were dispatched...
22 Event::assertNothingDispatched();
23});
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 not dispatched...
28 Event::assertNotDispatched(OrderFailedToShip::class);
29 
30 // Assert that no events were dispatched...
31 Event::assertNothingDispatched();
32 }
33}

你可以将闭包传递给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(),不会执行任何事件监听器。因此,如果您的测试使用依赖于事件的模型Factories(例如在模型creating事件期间创建 UUID),则应在使用FactoriesEvent::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}