跳到内容

事件

简介

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

事件是解耦应用程序各个方面的好方法,因为单个事件可以有多个彼此不依赖的监听器。例如,你可能希望在每次订单发货时向用户发送 Slack 通知。你可以引发一个 App\Events\OrderShipped 事件,而不是将订单处理代码耦合到 Slack 通知代码,监听器可以接收该事件并用于分发 Slack 通知。

生成事件和监听器

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

php artisan make:event PodcastProcessed
 
php artisan make:listener SendPodcastNotification --event=PodcastProcessed

为了方便起见,你也可以调用 make:eventmake:listener Artisan 命令,而不添加其他参数。当你这样做时,Laravel 将自动提示你输入类名,并且在创建监听器时,会提示你输入它应该监听的事件

php artisan make:event
 
php artisan make:listener

注册事件和监听器

事件发现

默认情况下,Laravel 将通过扫描应用程序的 Listeners 目录来自动查找和注册你的事件监听器。当 Laravel 找到任何以 handle__invoke 开头的监听器类方法时,Laravel 将这些方法注册为该方法签名中类型提示的事件的事件监听器

use App\Events\PodcastProcessed;
 
class SendPodcastNotification
{
/**
* Handle the given event.
*/
public function handle(PodcastProcessed $event): void
{
// ...
}
}

你可以使用 PHP 的联合类型来监听多个事件

/**
* Handle the given event.
*/
public function handle(PodcastProcessed|PodcastPublished $event): void
{
// ...
}

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

->withEvents(discover: [
__DIR__.'/../app/Domain/Orders/Listeners',
])

可以使用 event:list 命令列出你的应用程序中注册的所有监听器

php artisan event:list

生产环境中的事件发现

为了提高应用程序的速度,你应该使用 optimizeevent:cache Artisan 命令缓存所有应用程序监听器的清单。通常,此命令应作为应用程序部署过程的一部分运行。框架将使用此清单来加速事件注册过程。event:clear 命令可用于销毁事件缓存。

手动注册事件

使用 Event 外观,你可以在应用程序的 AppServiceProviderboot 方法中手动注册事件及其对应的监听器

use App\Domain\Orders\Events\PodcastProcessed;
use App\Domain\Orders\Listeners\SendPodcastNotification;
use Illuminate\Support\Facades\Event;
 
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Event::listen(
PodcastProcessed::class,
SendPodcastNotification::class,
);
}

可以使用 event:list 命令列出你的应用程序中注册的所有监听器

php artisan event:list

闭包监听器

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

use App\Events\PodcastProcessed;
use Illuminate\Support\Facades\Event;
 
/**
* Bootstrap any application services.
*/
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;
 
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Event::listen(queueable(function (PodcastProcessed $event) {
// ...
}));
}

与排队作业类似,你可以使用 onConnectiononQueuedelay 方法来自定义排队监听器的执行

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) {
// The queued listener failed...
}));

通配符事件监听器

你还可以使用 * 字符作为通配符参数注册监听器,从而允许你在同一个监听器上捕获多个事件。通配符监听器接收事件名称作为其第一个参数,并将整个事件数据数组作为其第二个参数

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;
 
/**
* Create a new event instance.
*/
public function __construct(
public Order $order,
) {}
}

如你所见,此事件类不包含任何逻辑。它是已购买的 App\Models\Order 实例的容器。如果使用 PHP 的 serialize 函数(例如在使用排队监听器时)序列化事件对象,则该事件使用的 SerializesModels 特性将优雅地序列化任何 Eloquent 模型。

定义监听器

接下来,让我们看一下示例事件的监听器。事件监听器在其 handle 方法中接收事件实例。当使用 --event 选项调用 make:listener Artisan 命令时,将自动导入正确的事件类并在 handle 方法中类型提示该事件。在 handle 方法中,你可以执行响应事件所需的任何操作

<?php
 
namespace App\Listeners;
 
use App\Events\OrderShipped;
 
class SendShipmentNotification
{
/**
* Create the event listener.
*/
public function __construct() {}
 
/**
* Handle the event.
*/
public function handle(OrderShipped $event): void
{
// Access the order using $event->order...
}
}
lightbulb

你的事件监听器也可以在其构造函数中类型提示它们需要的任何依赖项。所有事件监听器都通过 Laravel 服务容器解析,因此将自动注入依赖项。

停止事件的传播

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

排队事件监听器

如果你的监听器将执行缓慢的任务(例如发送电子邮件或发出 HTTP 请求),则对监听器进行排队会很有益。在使用排队监听器之前,请确保配置你的队列并在服务器或本地开发环境中启动队列工作进程。

要指定应将监听器排队,请将 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;
 
class SendShipmentNotification implements ShouldQueue
{
/**
* The name of the connection the job should be sent to.
*
* @var string|null
*/
public $connection = 'sqs';
 
/**
* The name of the queue the job should be sent to.
*
* @var string|null
*/
public $queue = 'listeners';
 
/**
* The time (seconds) before the job should be processed.
*
* @var int
*/
public $delay = 60;
}

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

/**
* Get the name of the listener's queue connection.
*/
public function viaConnection(): string
{
return 'sqs';
}
 
/**
* Get the name of the listener's queue.
*/
public function viaQueue(): string
{
return 'listeners';
}
 
/**
* Get the number of seconds before the job should be processed.
*/
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
{
/**
* Reward a gift card to the customer.
*/
public function handle(OrderCreated $event): void
{
// ...
}
 
/**
* Determine whether the listener should be queued.
*/
public function shouldQueue(OrderCreated $event): bool
{
return $event->order->subtotal >= 5000;
}
}

手动与队列交互

如果您需要手动访问监听器底层队列作业的 deleterelease 方法,可以使用 Illuminate\Queue\InteractsWithQueue 特性。该特性在生成的监听器中默认导入,并提供对这些方法的访问。

<?php
 
namespace App\Listeners;
 
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
 
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
 
/**
* Handle the event.
*/
public function handle(OrderShipped $event): void
{
if (true) {
$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;
}
lightbulb

要了解有关解决这些问题的更多信息,请查看有关 队列作业和数据库事务 的文档。

处理失败的任务

有时,您队列中的事件监听器可能会失败。如果队列监听器超过了队列工作进程定义的最大尝试次数,则会在您的监听器上调用 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;
 
/**
* Handle the event.
*/
public function handle(OrderShipped $event): void
{
// ...
}
 
/**
* Handle a job failure.
*/
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;
 
/**
* The number of times the queued listener may be attempted.
*
* @var int
*/
public $tries = 5;
}

作为定义监听器在失败之前可以尝试次数的替代方法,您可以定义监听器不再尝试的时间。这允许在给定的时间范围内尝试监听器任意次数。要定义监听器不再尝试的时间,请向您的监听器类添加一个 retryUntil 方法。此方法应返回一个 DateTime 实例。

use DateTime;
 
/**
* Determine the time at which the listener should timeout.
*/
public function retryUntil(): DateTime
{
return now()->addMinutes(5);
}

指定队列监听器的退避

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

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

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

/**
* Calculate the number of seconds to wait before retrying the queued listener.
*/
public function backoff(): int
{
return 3;
}

您可以通过从 backoff 方法返回退避值数组来轻松配置“指数”退避。在此示例中,第一次重试的延迟将为 1 秒,第二次重试为 5 秒,第三次重试为 10 秒,如果剩余的尝试次数更多,则每次后续重试为 10 秒。

/**
* Calculate the number of seconds to wait before retrying the queued listener.
*
* @return array<int, int>
*/
public function backoff(): array
{
return [1, 5, 10];
}

分发事件

要分派事件,您可以在事件上调用静态的 dispatch 方法。此方法通过 Illuminate\Foundation\Events\Dispatchable 特性在事件上可用。传递给 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
{
/**
* 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');
}
}

如果您想有条件地分派事件,可以使用 dispatchIfdispatchUnless 方法。

OrderShipped::dispatchIf($condition, $order);
 
OrderShipped::dispatchUnless($condition, $order);
lightbulb

在测试时,断言已分派某些事件而不实际触发其监听器可能会很有帮助。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;
 
/**
* Create a new event instance.
*/
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
{
/**
* 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']
);
}
}

如果您的事件监听器方法是在订阅者本身中定义的,您可能会发现从订阅者的 subscribe 方法返回事件和方法名称的数组更方便。注册事件监听器时,Laravel 将自动确定订阅者的类名。

<?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',
];
}
}

注册事件订阅者

编写订阅者后,如果订阅者遵循 Laravel 的 事件发现约定,Laravel 将自动注册订阅者中的处理程序方法。否则,您可以使用 Event facade 的 subscribe 方法手动注册您的订阅者。通常,这应该在应用程序的 AppServiceProviderboot 方法中完成。

<?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);
}
}

测试

在测试分派事件的代码时,您可能希望指示 Laravel 实际上不执行事件的监听器,因为监听器的代码可以单独于分派相应事件的代码直接进行测试。当然,要测试监听器本身,您可以在测试中实例化一个监听器实例并直接调用 handle 方法。

使用 Event facade 的 fake 方法,您可以防止监听器执行,执行正在测试的代码,然后使用 assertDispatchedassertNotDispatchedassertNothingDispatched 方法断言应用程序分派了哪些事件。

<?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 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 not dispatched...
Event::assertNotDispatched(OrderFailedToShip::class);
 
// Assert that no events were dispatched...
Event::assertNothingDispatched();
}
}

您可以将闭包传递给 assertDispatchedassertNotDispatched 方法,以断言已分派一个通过给定“真值测试”的事件。如果至少分派了一个通过给定真值测试的事件,则断言将成功。

Event::assertDispatched(function (OrderShipped $event) use ($order) {
return $event->order->id === $order->id;
});

如果您只想断言事件监听器正在监听给定事件,则可以使用 assertListening 方法。

Event::assertListening(
OrderShipped::class,
SendShipmentNotification::class
);
exclamation

调用 Event::fake() 后,将不会执行任何事件监听器。因此,如果您的测试使用依赖于事件的模型工厂(例如在模型的 creating 事件期间创建 UUID),则应在使用工厂后调用 Event::fake()

模拟部分事件

如果您只想伪造一组特定事件的事件监听器,您可以将它们传递给 fakefakeFor 方法。

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([...]);
}

您可以使用 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;
});
 
// 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([...]);
}
}