事件
介绍
Laravel 的事件提供了一个简单的观察者实现,允许您订阅和监听应用程序中发生的各种事件。事件类通常存储在 app/Events 目录中,而它们的监听器存储在 app/Listeners 中。如果您在应用程序中没有看到这些目录,不用担心,因为在您使用 Artisan 控制台命令生成事件和监听器时,它们会为您创建。
事件是解耦应用程序各个方面的好方法,因为单个事件可以有多个相互独立的监听器。例如,您可能希望在每次订单发货时向用户发送 Slack 通知。与其将订单处理代码与 Slack 通知代码耦合在一起,不如触发一个 OrderShipped 事件,监听器可以接收并将其转换为 Slack 通知。
注册事件和监听器
Laravel 应用程序中包含的 EventServiceProvider 提供了一个方便的地方来注册所有应用程序的事件监听器。listen 属性包含一个所有事件(键)及其监听器(值)的数组。您可以根据应用程序的需要向此数组添加任意数量的事件。例如,让我们添加一个 OrderShipped 事件:
/**
* 应用程序的事件监听器映射。
*
* @var array
*/
protected $listen = [
'App\Events\OrderShipped' => [
'App\Listeners\SendShipmentNotification',
],
];生成事件和监听器
当然,手动为每个事件和监听器创建文件是繁琐的。相反,将监听器和事件添加到您的 EventServiceProvider 并使用 event:generate 命令。此命令将生成在您的 EventServiceProvider 中列出的任何事件或监听器。已经存在的事件和监听器将保持不变:
php artisan event:generate手动注册事件
通常,事件应通过 EventServiceProvider 的 $listen 数组注册;然而,您也可以在 EventServiceProvider 的 boot 方法中手动注册基于闭包的事件:
/**
* 为应用程序注册任何其他事件。
*
* @return void
*/
public function boot()
{
parent::boot();
Event::listen('event.name', function ($foo, $bar) {
//
});
}通配符事件监听器
您甚至可以使用 * 作为通配符参数注册监听器,允许您在同一监听器上捕获多个事件。通配符监听器接收事件名称作为第一个参数,整个事件数据数组作为第二个参数:
Event::listen('event.*', function ($eventName, array $data) {
//
});事件发现
与其在 EventServiceProvider 的 $listen 数组中手动注册事件和监听器,您可以启用自动事件发现。当启用事件发现时,Laravel 将通过扫描应用程序的 Listeners 目录自动查找并注册您的事件和监听器。此外,EventServiceProvider 中显式定义的任何事件仍将被注册。
Laravel 通过扫描监听器类使用反射来查找事件监听器。当 Laravel 找到任何以 handle 开头的监听器类方法时,Laravel 将注册这些方法作为事件监听器,用于方法签名中类型提示的事件:
use App\Events\PodcastProcessed;
class SendPodcastProcessedNotification
{
/**
* 处理给定的事件。
*
* @param \App\Events\PodcastProcessed
* @return void
*/
public function handle(PodcastProcessed $event)
{
//
}
}事件发现默认是禁用的,但您可以通过覆盖应用程序的 EventServiceProvider 的 shouldDiscoverEvents 方法来启用它:
/**
* 确定是否应自动发现事件和监听器。
*
* @return bool
*/
public function shouldDiscoverEvents()
{
return true;
}默认情况下,应用程序的 Listeners 目录中的所有监听器将被扫描。如果您想定义其他目录进行扫描,可以在 EventServiceProvider 中覆盖 discoverEventsWithin 方法:
/**
* 获取用于发现事件的监听器目录。
*
* @return array
*/
protected function discoverEventsWithin()
{
return [
$this->app->path('Listeners'),
];
}在生产环境中,您可能不希望框架在每个请求上扫描所有监听器。因此,在部署过程中,您应该运行 event:cache Artisan 命令来缓存应用程序所有事件和监听器的清单。框架将使用此清单来加速事件注册过程。可以使用 event:clear 命令销毁缓存。
NOTE
event:list 命令可用于显示应用程序注册的所有事件和监听器的列表。
定义事件
事件类是一个数据容器,用于保存与事件相关的信息。例如,假设我们生成的 OrderShipped 事件接收一个 Eloquent ORM 对象:
<?php
namespace App\Events;
use App\Order;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderShipped
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $order;
/**
* 创建一个新的事件实例。
*
* @param \App\Order $order
* @return void
*/
public function __construct(Order $order)
{
$this->order = $order;
}
}如您所见,此事件类不包含逻辑。它是一个用于保存已购买的 Order 实例的容器。事件使用的 SerializesModels trait 将在事件对象使用 PHP 的 serialize 函数序列化时优雅地序列化任何 Eloquent 模型。
定义监听器
接下来,让我们看看示例事件的监听器。事件监听器在其 handle 方法中接收事件实例。event:generate 命令将自动导入正确的事件类并在 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 访问订单...
}
}NOTE
您的事件监听器也可以在其构造函数中类型提示任何所需的依赖项。所有事件监听器都是通过 Laravel 服务容器 解析的,因此依赖项将自动注入。
停止事件的传播
有时,您可能希望停止事件向其他监听器的传播。您可以通过从监听器的 handle 方法返回 false 来实现。
队列事件监听器
如果您的监听器将执行诸如发送电子邮件或发出 HTTP 请求等慢速任务,队列监听器可能会很有帮助。在开始使用队列监听器之前,请确保 配置您的队列 并在服务器或本地开发环境中启动队列监听器。
要指定监听器应排队,请将 ShouldQueue 接口添加到监听器类中。由 event:generate 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;
}如果您想在运行时定义监听器的队列,可以在监听器上定义一个 viaQueue 方法:
/**
* 获取监听器队列的名称。
*
* @return string
*/
public function viaQueue()
{
return 'listeners';
}有条件地排队监听器
有时,您可能需要根据仅在运行时可用的一些数据来确定是否应排队监听器。为此,可以在监听器中添加一个 shouldQueue 方法来确定是否应排队监听器。如果 shouldQueue 方法返回 false,则不会执行监听器:
<?php
namespace App\Listeners;
use App\Events\OrderPlaced;
use Illuminate\Contracts\Queue\ShouldQueue;
class RewardGiftCard implements ShouldQueue
{
/**
* 向客户奖励礼品卡。
*
* @param \App\Events\OrderPlaced $event
* @return void
*/
public function handle(OrderPlaced $event)
{
//
}
/**
* 确定是否应排队监听器。
*
* @param \App\Events\OrderPlaced $event
* @return bool
*/
public function shouldQueue(OrderPlaced $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);
}
}
}处理失败的任务
有时,您的队列事件监听器可能会失败。如果队列监听器超过队列工作者定义的最大尝试次数,将调用监听器的 failed 方法。failed 方法接收事件实例和导致失败的异常:
<?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)
{
//
}
}调度事件
要调度事件,您可以将事件实例传递给 event 辅助函数。该辅助函数将事件调度到其所有注册的监听器。由于 event 辅助函数是全局可用的,您可以在应用程序的任何地方调用它:
<?php
namespace App\Http\Controllers;
use App\Events\OrderShipped;
use App\Http\Controllers\Controller;
use App\Order;
class OrderController extends Controller
{
/**
* 发货给定订单。
*
* @param int $orderId
* @return Response
*/
public function ship($orderId)
{
$order = Order::findOrFail($orderId);
// 订单发货逻辑...
event(new OrderShipped($order));
}
}或者,如果您的事件使用 Illuminate\Foundation\Events\Dispatchable trait,您可以调用事件的静态 dispatch 方法。传递给 dispatch 方法的任何参数都将传递给事件的构造函数:
OrderShipped::dispatch($order);NOTE
在测试时,断言某些事件已被调度而不实际触发其监听器可能会很有帮助。Laravel 的 内置测试助手 使这变得很简单。
事件订阅者
编写事件订阅者
事件订阅者是可以在类本身中订阅多个事件的类,允许您在单个类中定义多个事件处理程序。订阅者应定义一个 subscribe 方法,该方法将传递一个事件调度器实例。您可以在给定的调度器上调用 listen 方法来注册事件监听器:
<?php
namespace App\Listeners;
class UserEventSubscriber
{
/**
* 处理用户登录事件。
*/
public function handleUserLogin($event) {}
/**
* 处理用户注销事件。
*/
public function handleUserLogout($event) {}
/**
* 为订阅者注册监听器。
*
* @param \Illuminate\Events\Dispatcher $events
*/
public function subscribe($events)
{
$events->listen(
'Illuminate\Auth\Events\Login',
'App\Listeners\UserEventSubscriber@handleUserLogin'
);
$events->listen(
'Illuminate\Auth\Events\Logout',
'App\Listeners\UserEventSubscriber@handleUserLogout'
);
}
}注册事件订阅者
编写订阅者后,您可以将其注册到事件调度器。您可以使用 EventServiceProvider 上的 $subscribe 属性注册订阅者。例如,让我们将 UserEventSubscriber 添加到列表中:
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* 应用程序的事件监听器映射。
*
* @var array
*/
protected $listen = [
//
];
/**
* 要注册的订阅者类。
*
* @var array
*/
protected $subscribe = [
'App\Listeners\UserEventSubscriber',
];
}