介绍 Laravel 的事件系统提供了一个简单的观察者模式的实现,允许你能够订阅和监听在你的应用中的发生的各种事件。事件类一般来说存储在 app/Events 目录,监听者的类存储在 app/Listeners 目录。不要担心在你的应用中没有看到这两个目录,因为通过 Artisan 命令行来创建事件和监听者的时候目录会同时被创建。 事件系统可以作为一个非常棒的方式来解耦你的系统的方方面面,因为一个事件可以有多个完全不相关的监听者。例如,你希望每当有订单发出的时候都给你发送一个 Slack 通知。你大可不必将你的处理订单的代码和发送 slack 消息的代码放在一起,你只需要触发一个 App\Events\OrderShipped 事件,然后事件监听者可以收到这个事件然后发送 slack 通知 注册事件和*** 在系统的服务提供者 App\Providers\EventServiceProvider 中提供了一个简单的方式来注册你所有的事件监听者。属性 listen 包含所有的事件 (作为键) 和对应的*** (值)。你可以添加任意多系统需要的***在这个数组中,让我们添加一个 OrderShipped 事件: ``` use App\Events\OrderShipped; use App\Listeners\SendShipmentNotification; /** * 系统中的事件和***的对应关系。 * * @var array */ protected $listen = [ OrderShipped::class => [ SendShipmentNotification::class, ], ]; ``` 技巧:可以用 Artisan 命令行 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 实例容器。 如果事件对象是使用 PHP 的 SerializesModels 函数序列化的,事件使用的 SerializesModels trait 将会优雅地序列化任何 Eloquent 模型,比如在使用 队列侦听器。 定义*** 接下来,让我们看一下示例事件的侦听器。事件***在其 handle 方法中接收事件实例。 artisan 命令 event:generate 和 make:listener 会自动导入正确的事件类,并在 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 和 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 方法: ``` /** * 获取***的队列连接名称。 * * @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\InteractsWithQueuetrait 进行访问。默认情况下,此 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 特性提供给事件的。 传递给 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); } } ``` 技巧:在测试时,断言某些事件是在没有实际触发其侦听器的情况下进行的会很有帮助。 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, ]; } ```