事件
介绍
Laravel 的事件提供了一个简单的观察者模式实现,允许您订阅和监听应用程序中发生的各种事件。事件类通常存储在 app/Events
目录中,而它们的监听器存储在 app/Listeners
中。如果您在应用程序中没有看到这些目录,不用担心,因为在您使用 Artisan 控制台命令生成事件和监听器时,它们会为您创建。
事件是解耦应用程序各个方面的好方法,因为单个事件可以有多个不依赖于彼此的监听器。例如,您可能希望每次订单发货时向用户发送 Slack 通知。与其将订单处理代码与 Slack 通知代码耦合在一起,您可以触发一个 App\Events\OrderShipped
事件,监听器可以接收该事件并用于发送 Slack 通知。
注册事件和监听器
Laravel 应用程序中包含的 App\Providers\EventServiceProvider
提供了一个方便的地方来注册所有应用程序的事件监听器。listen
属性包含一个所有事件(键)及其监听器(值)的数组。您可以根据应用程序的需要向此数组添加任意数量的事件。例如,让我们添加一个 OrderShipped
事件:
use App\Events\OrderShipped;
use App\Listeners\SendShipmentNotification;
/**
* 应用程序的事件监听器映射。
*
* @var array
*/
protected $listen = [
OrderShipped::class => [
SendShipmentNotification::class,
],
];
event:list
命令可用于显示应用程序注册的所有事件和监听器的列表。
生成事件和监听器
当然,手动为每个事件和监听器创建文件是繁琐的。相反,您可以将监听器和事件添加到 EventServiceProvider
中,并使用 event:generate
Artisan 命令。此命令将生成 EventServiceProvider
中列出的任何尚不存在的事件或监听器:
php artisan event:generate
或者,您可以使用 make:event
和 make:listener
Artisan 命令生成单个事件和监听器:
php artisan make:event PodcastProcessed
php artisan make:listener SendPodcastNotification --event=PodcastProcessed
手动注册事件
通常,事件应通过 EventServiceProvider
的 $listen
数组注册;然而,您也可以在 EventServiceProvider
的 boot
方法中手动注册基于类或闭包的事件监听器:
use App\Events\PodcastProcessed;
use App\Listeners\SendPodcastNotification;
use Illuminate\Support\Facades\Event;
/**
* 为应用程序注册任何其他事件。
*
* @return void
*/
public function boot()
{
Event::listen(
PodcastProcessed::class,
[SendPodcastNotification::class, 'handle']
);
Event::listen(function (PodcastProcessed $event) {
//
});
}
可队列的匿名事件监听器
在手动注册基于闭包的事件监听器时,您可以将监听器闭包包装在 Illuminate\Events\queueable
函数中,以指示 Laravel 使用队列执行监听器:
use App\Events\PodcastProcessed;
use function Illuminate\Events\queueable;
use Illuminate\Support\Facades\Event;
/**
* 为应用程序注册任何其他事件。
*
* @return void
*/
public function boot()
{
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 ($eventName, array $data) {
//
});
事件发现
与其在 EventServiceProvider
的 $listen
数组中手动注册事件和监听器,您可以启用自动事件发现。当启用事件发现时,Laravel 将通过扫描应用程序的 Listeners
目录自动查找并注册您的事件和监听器。此外,EventServiceProvider
中显式定义的任何事件仍将被注册。
Laravel 通过使用 PHP 的反射服务扫描监听器类来查找事件监听器。当 Laravel 找到任何以 handle
或 __invoke
开头的监听器类方法时,Laravel 将注册这些方法作为事件监听器,用于方法签名中类型提示的事件:
use App\Events\PodcastProcessed;
class SendPodcastNotification
{
/**
* 处理给定的事件。
*
* @param \App\Events\PodcastProcessed $event
* @return void
*/
public function handle(PodcastProcessed $event)
{
//
}
}
事件发现默认是禁用的,但您可以通过覆盖应用程序的 EventServiceProvider
的 shouldDiscoverEvents
方法来启用它:
/**
* 确定是否应自动发现事件和监听器。
*
* @return bool
*/
public function shouldDiscoverEvents()
{
return true;
}
默认情况下,应用程序的 app/Listeners
目录中的所有监听器将被扫描。如果您想定义其他目录进行扫描,可以在 EventServiceProvider
中覆盖 discoverEventsWithin
方法:
/**
* 获取应用于发现事件的监听器目录。
*
* @return array
*/
protected function discoverEventsWithin()
{
return [
$this->app->path('Listeners'),
];
}
生产环境中的事件发现
在生产环境中,框架在每个请求上扫描所有监听器是低效的。因此,在部署过程中,您应该运行 event:cache
Artisan 命令来缓存应用程序的所有事件和监听器的清单。框架将使用此清单来加速事件注册过程。event:clear
命令可用于销毁缓存。
定义事件
事件类本质上是一个数据容器,用于保存与事件相关的信息。例如,假设一个 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;
/**
* 订单实例。
*
* @var \App\Models\Order
*/
public $order;
/**
* 创建一个新的事件实例。
*
* @param \App\Models\Order $order
* @return void
*/
public function __construct(Order $order)
{
$this->order = $order;
}
}
如您所见,此事件类不包含任何逻辑。它是一个 App\Models\Order
实例的容器。事件使用的 SerializesModels
trait 将在事件对象使用 PHP 的 serialize
函数序列化时优雅地序列化任何 Eloquent 模型,例如在使用队列监听器时。
定义监听器
接下来,让我们看看示例事件的监听器。事件监听器在其 handle
方法中接收事件实例。event:generate
和 make:listener
Artisan 命令将自动导入正确的事件类并在 handle
方法上进行类型提示。在 handle
方法中,您可以执行任何必要的操作来响应事件:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
class SendShipmentNotification
{
/**
* 创建事件监听器。
*
* @return void
*/
public function __construct()
{
//
}
/**
* 处理事件。
*
* @param \App\Events\OrderShipped $event
* @return void
*/
public function handle(OrderShipped $event)
{
// 使用 $event->order 访问订单...
}
}
您的事件监听器也可以在其构造函数中类型提示任何所需的依赖项。所有事件监听器都是通过 Laravel 服务容器解析的,因此依赖项将自动注入。
停止事件的传播
有时,您可能希望停止事件向其他监听器的传播。您可以通过从监听器的 handle
方法返回 false
来实现。
队列事件监听器
如果您的监听器将执行诸如发送电子邮件或发出 HTTP 请求等慢速任务,队列监听器可能会很有用。在使用队列监听器之前,请确保配置您的队列并在服务器或本地开发环境中启动队列工作程序。
要指定监听器应被队列化,请在监听器类中添加 ShouldQueue
接口。由 event:generate
和 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
{
/**
* 作业应发送到的连接名称。
*
* @var string|null
*/
public $connection = 'sqs';
/**
* 作业应发送到的队列名称。
*
* @var string|null
*/
public $queue = 'listeners';
/**
* 作业应在处理前的时间(秒)。
*
* @var int
*/
public $delay = 60;
}
如果您想在运行时定义监听器的队列连接或队列名称,可以在监听器上定义 viaConnection
或 viaQueue
方法:
/**
* 获取监听器的队列连接名称。
*
* @return string
*/
public function viaConnection()
{
return 'sqs';
}
/**
* 获取监听器的队列名称。
*
* @return string
*/
public function viaQueue()
{
return 'listeners';
}
有条件地队列监听器
有时,您可能需要根据仅在运行时可用的数据来确定是否应将监听器排队。为此,可以在监听器中添加一个 shouldQueue
方法,以确定是否应将监听器排队。如果 shouldQueue
方法返回 false
,则不会执行监听器:
<?php
namespace App\Listeners;
use App\Events\OrderCreated;
use Illuminate\Contracts\Queue\ShouldQueue;
class RewardGiftCard implements ShouldQueue
{
/**
* 向客户奖励礼品卡。
*
* @param \App\Events\OrderCreated $event
* @return void
*/
public function handle(OrderCreated $event)
{
//
}
/**
* 确定是否应将监听器排队。
*
* @param \App\Events\OrderCreated $event
* @return bool
*/
public function shouldQueue(OrderCreated $event)
{
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;
/**
* 处理事件。
*
* @param \App\Events\OrderShipped $event
* @return void
*/
public function handle(OrderShipped $event)
{
if (true) {
$this->release(30);
}
}
}
队列事件监听器和数据库事务
当队列监听器在数据库事务中被调度时,它们可能会在数据库事务提交之前被队列处理。当这种情况发生时,您在数据库事务中对模型或数据库记录所做的任何更新可能尚未反映在数据库中。此外,在事务中创建的任何模型或数据库记录可能不存在于数据库中。如果您的监听器依赖于这些模型,则在处理调度队列监听器的作业时可能会发生意外错误。
如果您的队列连接的 after_commit
配置选项设置为 false
,您仍然可以通过在监听器类中定义 $afterCommit
属性来指示特定的队列监听器应在所有打开的数据库事务提交后被调度:
<?php
namespace App\Listeners;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
public $afterCommit = true;
}
要了解更多关于解决这些问题的信息,请查看有关队列作业和数据库事务的文档。
处理失败的任务
有时,您的队列事件监听器可能会失败。如果队列监听器超过了队列工作程序定义的最大尝试次数,则会调用监听器的 failed
方法。failed
方法接收事件实例和导致失败的 Throwable
:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
/**
* 处理事件。
*
* @param \App\Events\OrderShipped $event
* @return void
*/
public function handle(OrderShipped $event)
{
//
}
/**
* 处理作业失败。
*
* @param \App\Events\OrderShipped $event
* @param \Throwable $exception
* @return void
*/
public function failed(OrderShipped $event, $exception)
{
//
}
}
指定队列监听器的最大尝试次数
如果您的队列监听器遇到错误,您可能不希望它无限期地重试。因此,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
实例:
/**
* 确定监听器应超时的时间。
*
* @return \DateTime
*/
public function retryUntil()
{
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\Request;
class OrderShipmentController extends Controller
{
/**
* 发货给定的订单。
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$order = Order::findOrFail($request->order_id);
// 订单发货逻辑...
OrderShipped::dispatch($order);
}
}
如果您想有条件地调度事件,可以使用 dispatchIf
和 dispatchUnless
方法:
OrderShipped::dispatchIf($condition, $order);
OrderShipped::dispatchUnless($condition, $order);
在测试时,断言某些事件已被调度而不实际触发其监听器可能会很有帮助。Laravel 的内置测试助手使这变得很简单。
事件订阅者
编写事件订阅者
事件订阅者是可以在订阅者类本身中订阅多个事件的类,允许您在单个类中定义多个事件处理程序。订阅者应定义一个 subscribe
方法,该方法将传递一个事件调度器实例。您可以在给定的调度器上调用 listen
方法来注册事件监听器:
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
class UserEventSubscriber
{
/**
* 处理用户登录事件。
*/
public function handleUserLogin($event) {}
/**
* 处理用户注销事件。
*/
public function handleUserLogout($event) {}
/**
* 为订阅者注册监听器。
*
* @param \Illuminate\Events\Dispatcher $events
* @return void
*/
public function subscribe($events)
{
$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;
class UserEventSubscriber
{
/**
* 处理用户登录事件。
*/
public function handleUserLogin($event) {}
/**
* 处理用户注销事件。
*/
public function handleUserLogout($event) {}
/**
* 为订阅者注册监听器。
*
* @param \Illuminate\Events\Dispatcher $events
* @return array
*/
public function subscribe($events)
{
return [
Login::class => 'handleUserLogin',
Logout::class => 'handleUserLogout',
];
}
}
注册事件订阅者
编写订阅者后,您可以将其注册到事件调度器。您可以使用 EventServiceProvider
上的 $subscribe
属性注册订阅者。例如,让我们将 UserEventSubscriber
添加到列表中:
<?php
namespace App\Providers;
use App\Listeners\UserEventSubscriber;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* 应用程序的事件监听器映射。
*
* @var array
*/
protected $listen = [
//
];
/**
* 要注册的订阅者类。
*
* @var array
*/
protected $subscribe = [
UserEventSubscriber::class,
];
}