事件系统 简介 Laravel 的事件提供了一个简单的观察者实现,允许你在应用中订阅和监听各种发生的事件。事件类通常放在 app/Events 目录下,而这些事件类的***则放在 app/Listeners 目录下。如果在你的应用中你没有看到这些目录,不用担心,它们会在你使用 Artisan 控制台命令生成事件与***的时候自动创建。 事件是分离应用程序各个方面的好方法,因为单个事件可以有多个相互不依赖的***。例如,你可能希望每次发货后都向你的用户发送 Slack 通知。 你可以引发一个 OrderShipped 事件,而不是将你的订单处理代码耦合到 Slack 通知代码,***可以接收该事件并将其转换为 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 方法中手动的注册基于闭包的事件: ``` use App\Events\PodcastProcessed; /** * 注册应用的任何其他事件 * * @return void */ public function boot() { Event::listen(function (PodcastProcessed $event) { // }); } ``` 匿名事件***队列 手动注册事件***时,可以将***的 Closure 闭包放在 Illuminate\Events\queueable 函数中,以指示 Laravel 使用 queue 执行***: ``` 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 方法中提供一个闭包来处理: ``` 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 通过使用反射扫描***类来查找事件***。 当 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 命令则可用于销毁缓存。 技巧:event:list 命令可用于显示应用程序注册的所有事件和***的列表。 定义事件 事件类是一个保存与事件相关信息的容器。例如,假设我们生成的 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 $order; /** * 创建一个事件实例 * * @param \App\Models\Order $order * @return void */ public function __construct(Order $order) { $this->order = $order; } } ``` 如你所见,这个事件类中没有包含其它逻辑。它只是一个已购买的 Order 的实例的容器。如果使用 PHP 的 serialize 函数序列化事件对象,事件使用的 SerializesModels trait 将会优雅地序列化任何 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 来访问订单 ... } } ``` 技巧:你的事件***也可以在构造函数中加入任何依赖关系的类型提示。所有的事件***都是通过 Laravel 的 服务容器 解析的,因此所有的依赖都将会被自动注入。 停止事件传播 有时,你可以通过在***的 handle 方法中返回 false 来阻止事件被其它的***获取。 事件***队列 如果你的***中要执行诸如发送电子邮件或发出 HTTP 请求之类的耗时任务,你可以将任务丢给队列处理。在开始使用队列***之前,请确保在你的服务器或者本地开发环境中能够 配置队列 并启动一个队列***。 要指定***启动队列,你可以在***类中实现 ShouldQueue 接口。由 Artisan 命令 event:generate 生成的***已经将此接口导入到当前命名空间中,因此你可以直接使用: ``` <?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\Models\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); 技巧:在测试时,只需要断言特定事件被分发,而不需要真正地触发***。Laravel 的 内置测试辅助函数 可以轻松做到这一点。 事件订阅者 编写事件订阅者 事件订阅者是可以在自身内部订阅多个事件的类,即能够在单个类中定义多个事件处理器。订阅者应该定义一个 subscribe 方法,这个方法接收一个事件分发器实例。你可以调用给定事件分发器上的 listen 方法来注册事件***: ``` <?php namespace App\Listeners; class UserEventSubscriber { /** * 处理用户登录事件 */ public function handleUserLogin($event) {} /** * 处理用户注销事件 */ public function handleUserLogout($event) {} /** * 为事件订阅者注册*** * * @param \Illuminate\Events\Dispatcher $events * @return void */ public function subscribe($events) { $events->listen( 'Illuminate\Auth\Events\Login', [UserEventSubscriber::class, 'handleUserLogin'] ); $events->listen( 'Illuminate\Auth\Events\Logout', [UserEventSubscriber::class, 'handleUserLogout'] ); } } ``` 或者,你的订阅者的 subscribe 方法可以将事件数组返回到处理程序映射。在这种情况下,事件***映射将自动为你注册: ``` use Illuminate\Auth\Events\Login; use Illuminate\Auth\Events\Logout; /** * Register the listeners for the subscriber. * * @return array */ public function subscribe() { return [ Login::class => [UserEventSubscriber::class, 'handleUserLogin'], Logout::class => [UserEventSubscriber::class, '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', ]; } ```