事件系统

未匹配的标注

事件

介绍

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 数组注册;但是,你也可以在EventServiceProviderboot 方法中手动注册基于类或闭包的事件监听器:

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) {
        //
    }));
}

与队列任务一样,可以使用 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) {
        // 队列侦听器失败...
    }));

通配符事件监听器

您甚至可以使用’ * ‘作为通配符参数注册侦听器,允许您在同一个侦听器上捕获多个事件。通配符监听器接收事件名作为其第一个参数,整个事件数据数组作为其第二个参数:

    Event::listen('event.*', function ($eventName, array $data) {
        //
    });

事件的发现

您可以启用自动事件发现,而不是在EventServiceProvider$listen数组中手动注册事件和侦听器。当事件发现启用,Laravel将自动发现和注册你的事件和监听器扫描你的应用程序的Listeners目录。此外,在 EventServiceProvider中列出的任何显式定义的事件仍将被注册。

Laravel通过使用PHP的反射服务扫描侦听器类来查找事件侦听器。当Laravel发现任何以handle开头的侦听器类方法时,Laravel会将这些方法注册为该方法签名中类型暗示的事件的事件监听器:

    use App\Events\PodcastProcessed;

    class SendPodcastNotification
    {
        /**
         *处理给定的事件。
         *
         * @param  \App\Events\PodcastProcessed
         * @return void
         */
        public function handle(PodcastProcessed $event)
        {
            //
        }
    }

事件发现在默认情况下是禁用的,但是你可以通过覆盖应用程序的 EventServiceProvidershouldDiscoverEvents方法来启用它:

    /**
     * 确定是否应用自动发现事件和侦听器。
     *
     * @return bool
     */
    public function shouldDiscoverEvents()
    {
        return true;
    }

默认情况下,应用程序app/listeners目录中的所有侦听器都将被扫描。如果你想要定义更多的目录来扫描,你可以覆盖EventServiceProvider中的discoverEventsWithin方法:

    /**
     * 获取应该用于发现事件的侦听器目录。
     *
     * @return array
     */
    protected function discoverEventsWithin()
    {
        return [
            $this->app->path('Listeners'),
        ];
    }

生产中的事件发现

在生产中,框架在每个请求上扫描所有侦听器的效率并不高。因此,在你的部署过程中,你应该运行event:cacheArtisan命令来缓存你的应用程序的所有事件和监听器清单。框架将使用该清单来加速事件注册过程。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:generatemake: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 生成的监听器已经将此接口导入到当前命名空间中,因此你可以直接使用:

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

手动访问队列

如果你需要手动访问监听器下面队列任务的 deleterelease方法,可以使用illighte\queue\interactiswithqueuetrait进行访问。默认情况下,此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;

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

注册事件订阅者

当编写完订阅者后,你已经准备好为事件分发器注册它们了。你可以使用 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,
    ];
}

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
上一篇 下一篇
Summer
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
贡献者:13
讨论数量: 0
发起讨论 只看当前版本


暂无话题~