事件系统
事件系统
介绍
Laravel 的事件系统提供了一个简单的观察者模式的实现,可以订阅和监听你的应用中发生的各种事件。通常,事件的类存储在 app/Events
目录,监听器的类存储在 app/Listeners
目录。不要担心在你的应用中没有看到这两个目录,之后通过 Artisan 命令行创建事件和监听器的时候,目录会自动被创建。
事件系统可以作为一个非常棒的方式来解耦你的应用的方方面面,因为一个事件可以有多个互相独立的监听器。例如,你希望每当有订单发出的时候都给用户发送一个 Slack 通知,大可不必将处理订单的代码和发送 Slack 通知的代码放在一起。只需要触发一个 App\Events\OrderShipped
事件,然后由此事件的监听器发送 Slack 通知。
生成事件和监听器
想要快速地生成事件和监听器,你可以使用 Artisan 命令 make:event
和 make:listener
:
php artisan make:event PodcastProcessed
php artisan make:listener SendPodcastNotification --event=PodcastProcessed
方便起见,你也可以不加任何参数地去使用 make:event
和 make:listener
这两个命令。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
{
// ...
}
}
如果你计划将监听器的类储存在非默认目录下或者多个子目录下,可以在 bootstrap/app.php
文件中使用 withEvents
方法指示 Laravel 去扫描指定目录:
->withEvents(discover: [
__DIR__.'/../app/Domain/Listeners',
])
event:list
命令可以列出所有注册的监听器:
php artisan event:list
生产环境中的事件发现
使用 Artisan 命令 optimize
或 event:cache
缓存一份监听器的清单可以有效提升应用的速度。通常,这条命令应该是应用 部署 过程中的一部份。生成的清单会被框架用来加速事件注册的流程。若需要清除事件缓存,可使用 event:clear
命令。
手动注册事件
你可以在 AppServiceProvider
中的 boot
方法中,使用 Event
Facade 手动注册事件和对应的监听器:
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()->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) {
// 当队列监听器失败时...
}));
手动注册事件
使用 Event
facade, 你可以在应用程序的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 使用队列执行监听器: queue:
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()->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) {
// 队列监听器失败...
}));
事件监听器使用通配符
注册监听器时,你可以在参数中使用 *
通配符,以此监听多个不同的事件。通配符监听器以事件名做为第一个参数,以事件的数据做为第二个参数:
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
订单的实例容器。当事件对象被 PHP 的 serialize
方法序列化时,事件使用的 SerializesModels
Trait 会优雅地序列化 Eloquent 模型,比如使用 队列监听器的时候。
定义监听器
接下来,让我们看一下示例事件的监听器。事件监听器通过 handle
方法接收事件实例。使用 Artisan 命令 make:listener
时,使用 --event
选项,会自动导入合适的事件类以及 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
接口添加到监听器类。由 Artisan 命令 make:listener
生成的监听器已经将此接口导入当前命名空间,因此你可以直接使用:
<?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
{
/**
* 任务发送到的连接的名称。
*
* @var string|null
*/
public $connection = 'sqs';
/**
* 任务发送到的队列的名称。
*
* @var string|null
*/
public $queue = 'listeners';
/**
* 延迟处理作业的时间(秒)。
*
* @var int
*/
public $delay = 60;
}
如果你想在运行时定义监听器的队列连接、名称或延迟,可以在监听器上定义 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 。Laravel 生成的监听器类文件中默认导入此 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 (true) {
$this->release(30);
}
}
}
队列事件监听器和数据库事务
当队列中的监听器在数据库事务中被调度时,可能会在数据库事务执行完成前被队列执行。发生这种情况时,数据库事务内对模型或数据库所做的更新可能尚未生效。此外,在事务中创建的模型或数据可能也尚未存在。如果你的监听器依赖于这些尚未存在的数据,那么被调度的队列监听器执行时将会发生未知错误。
如果应用的配置文件中,队列连接的 after_commit
设置为 false
,你仍然可以通过在监听器的类里实现 ShouldHandleEventsAfterCommit
接口来指定监听器应该在所有数据库事件执行完成后再调度:
<?php
namespace App\Listeners;
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue, ShouldHandleEventsAfterCommit
{
use InteractsWithQueue;
}
[!注意]
要了解有关解决这些问题的更多信息,请查看有关 队列任务和数据库事务 的文档。
处理失败的队列
有时队列的事件监听器可能会执行失败。如果队列监听器达到了队列配置里重试的最大次数, 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\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
/**
* 尝试执行队列监听器的次数。
*
* @var int
*/
public $tries = 5;
}
除了定义重试的次数限制外,你也可以定义一个时间限制,让监听器在指定的时间内不限次数的重试。通过添加 retryUntil
方法到监听器的类中来定义这个限制的时间,此方法应当返回一个 DateTime
实例:
use DateTime;
/**
* 设置监听器超时的时间。
*/
public function retryUntil(): DateTime
{
return now()->addMinutes(5);
}
调度事件
你可以通过调用事件的静态方法 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
{
/**
* 运送给定的订单。
*/
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);
[!注意]
测试时,可以只去断言事件是否被调度而无需真正地触发其监听器。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,
) {}
}
事件订阅者
编写事件订阅者
事件订阅者以类的形式,让你可以在订阅者的类中订阅多个事件,允许你在一个类中定义多个处理事件的方法。订阅者类中需定义一个 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']
);
}
}
如果事件监听器方法是在订阅者类中被定义的,在 subscriber
方法中直接返回事件和方法名的数组会更加的便利。当注册监听器的时候,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',
];
}
}
注册事件订阅者
编写好订阅者后,你就可以将其注册到事件调度器中了。可以使用 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::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::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,
]);
作用域事件模拟
如果你只想为部份测试模拟事件监听器,你可以使用 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;
});
// 事件按正常方式调度,观察者将会运行...
$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
{
/**
* 测试订单程序
*/
public function test_orders_can_be_processed(): void
{
$order = Event::fakeFor(function () {
$order = Order::factory()->create();
Event::assertDispatched(OrderCreated::class);
return $order;
});
// 事件按正常方式调度,观察者将会运行...
$order->update([...]);
}
}
原文地址:cndocs/11.x/ev...
译文地址:cndocs/11.x/ev...