Skip to content

事件

介绍

Laravel 的事件提供了一个简单的观察者模式实现,允许您订阅和监听应用程序中发生的各种事件。事件类通常存储在 app/Events 目录中,而它们的监听器存储在 app/Listeners 中。如果您在应用程序中看不到这些目录,请不要担心,因为它们是在您使用 Artisan 控制台命令生成事件和监听器时为您创建的。

事件是解耦应用程序各个方面的好方法,因为单个事件可以有多个彼此不依赖的监听器。例如,您可能希望在每次订单发货时向用户发送 Slack 通知。您可以引发 App\Events\OrderShipped 事件,监听器可以接收该事件并使用它来调度 Slack 通知,而不是将订单处理代码耦合到 Slack 通知代码。

生成事件和监听器

要快速生成事件和监听器,您可以使用 make:eventmake:listener Artisan 命令:

shell
php artisan make:event PodcastProcessed

php artisan make:listener SendPodcastNotification --event=PodcastProcessed

为方便起见,你也可以调用 make:eventmake:listener Artisan 命令,而无需额外的参数。当你这样做时,Laravel 会自动提示你输入类名,并在创建监听器时提示它应该使用的监听事件:

shell
php artisan make:event

php artisan make:listener

注册事件和监听器

事件发现

默认情况下,Laravel 将通过扫描应用程序的 Listeners 目录来自动查找并注册您的事件监听器。当 Laravel 找到任何以 handle__invoke 开头的监听器类方法时,Laravel 会将这些方法注册为方法签名中类型提示的事件的事件监听器:

php
    use App\Events\PodcastProcessed;

    class SendPodcastNotification
    {
        /**
         * 处理给定的事件
         */
        public function handle(PodcastProcessed $event): void
        {
            // ...
        }
    }

如果您计划将监听器存储在不同的目录或多个目录中,您可以指示 Laravel 使用应用程序的 bootstrap/app.php 文件中的 withEvents 方法扫描这些目录:

php
    ->withEvents(discover: [
        __DIR__.'/../app/Domain/Orders/Listeners',
    ])

event:list 命令可用于列出在您的应用程序中注册的所有监听器:

shell
php artisan event:list

生产中的事件发现

为了提高应用程序的速度,您应该使用 optimizeevent:cache Artisan 命令缓存应用程序的所有监听器的清单。通常,此命令应作为应用程序部署过程的一部分运行。框架将使用此清单来加快事件注册过程。event:clear 命令可用于销毁事件缓存。

手动注册事件

使用 Event Facade,您可以在应用程序的 AppServiceProviderboot 方法中手动注册事件及其相应的监听器:

php
    use App\Domain\Orders\Events\PodcastProcessed;
    use App\Domain\Orders\Listeners\SendPodcastNotification;
    use Illuminate\Support\Facades\Event;

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Event::listen(
            PodcastProcessed::class,
            SendPodcastNotification::class,
        );
    }

event:list 命令可用于列出在您的应用程序中注册的所有监听器:

shell
php artisan event:list

Closure 监听器

通常,监听器定义为类;但是,您也可以在应用程序的 AppServiceProviderboot 方法中手动注册基于 Closure 的事件监听器:

php
    use App\Events\PodcastProcessed;
    use Illuminate\Support\Facades\Event;

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Event::listen(function (PodcastProcessed $event) {
            // ...
        });
    }

可排队的匿名事件监听器

当注册基于闭包的事件监听器时,你可以将监听器闭包包装在 Illuminate\Events\queueable 函数中,以指示 Laravel 使用队列执行监听器:

php
    use App\Events\PodcastProcessed;
    use function Illuminate\Events\queueable;
    use Illuminate\Support\Facades\Event;

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Event::listen(queueable(function (PodcastProcessed $event) {
            // ...
        }));
    }

与排队作业一样,你可以使用 onConnectiononQueuedelay 方法来自定义排队监听器的执行:

php
    Event::listen(queueable(function (PodcastProcessed $event) {
        // ...
    })->onConnection('redis')->onQueue('podcasts')->delay(now()->addSeconds(10)));

如果要处理匿名排队监听器失败,可以在定义可排队监听器时为 catch 方法提供闭包。此 closure 将接收导致监听器失败的事件实例和 Throwable 实例:

php
    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) {
        // 排队监听器失败...
    }));

通配符事件监听器

您还可以使用 * 字符作为通配符参数来注册监听器,从而允许您在同一监听器上捕获多个事件。通配符监听器接收事件名称作为其第一个参数,将整个事件数据数组作为其第二个参数:

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

定义事件

事件类本质上是一个数据容器,用于保存与事件相关的信息。例如,假设 App\Events\OrderShipped 事件接收到 Eloquent ORM 对象:

php
    <?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 function __construct(
            public Order $order,
        ) {}
    }

如您所见,此事件类不包含任何逻辑。它是 App\Models\Order 实例的容器。如果事件对象是使用 PHP 的 serialize 函数序列化的,例如在使用排队监听器时,事件使用的 SerializesModels trait 将优雅地序列化任何 Eloquent 模型。

定义监听器

接下来,我们来看一下示例事件的监听器。事件监听器在其 handle 方法中接收事件实例。make:listener Artisan 命令在使用 --event 选项调用时,将自动导入正确的事件类并在 handle 方法中对事件进行类型提示。在 handle 方法中,您可以执行响应事件所需的任何操作:

php
    <?php

    namespace App\Listeners;

    use App\Events\OrderShipped;

    class SendShipmentNotification
    {
        /**
         * 创建事件监听器
         */
        public function __construct()
        {
            // ...
        }

        /**
         * 处理事件
         */
        public function handle(OrderShipped $event): void
        {
            // 使用 $event->order 访问订单...
        }
    }

NOTE

你的事件监听器还可以对其构造函数上需要的任何依赖项进行类型提示。所有事件监听器都通过 Laravel 服务容器解析,因此依赖项将自动注入。

停止事件的传播

有时,您可能希望停止将事件传播到其他监听器。您可以通过从监听器的 handle 方法返回 false 来实现此目的。

事件侦听队列

如果您的监听器要执行慢速任务(例如发送电子邮件或发出 HTTP 请求),则对监听器进行排队可能会很有用。在使用排队监听器之前,请确保在服务器或本地开发环境中配置队列并启动队列工作程序。

要指定监听器应排队,请将 ShouldQueue 接口添加到监听器类中。由 make:listener Artisan 命令生成的监听器已经将此接口导入到当前命名空间中,因此您可以立即使用它:

php
    <?php

    namespace App\Listeners;

    use App\Events\OrderShipped;
    use Illuminate\Contracts\Queue\ShouldQueue;

    class SendShipmentNotification implements ShouldQueue
    {
        // ...
    }

就是这样!现在,当这个监听器处理的事件被调度时,监听器将由 Event Dispatcher 使用 Laravel 的队列系统自动排队。如果队列执行监听器时没有抛出异常,则排队的作业在处理完后会自动删除。

自定义队列连接、名称和延迟

如果您想自定义事件监听器的队列连接、队列名称或队列延迟时间,您可以在监听器类上定义 $connection$queue$delay 属性:

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

如果你想在运行时定义监听器的队列连接、队列名称或延迟,你可以在监听器上定义 viaConnectionviaQueuewithDelay 方法:

php
    /**
     * 获取监听器的队列连接的名称
     */
    public function viaConnection(): string
    {
        return 'sqs';
    }

    /**
     * 获取监听器队列的名称
     */
    public function viaQueue(): string
    {
        return 'listeners';
    }

    /**
     * 获取处理作业前的秒数
     */
    public function withDelay(OrderShipped $event): int
    {
        return $event->highPriority ? 0 : 60;
    }

有条件地排队监听器

有时,您可能需要根据某些仅在运行时可用的数据来确定是否应将监听器排队。为此,可以将 shouldQueue 方法添加到监听器中,以确定是否应将监听器排队。如果 shouldQueue 方法返回 false,则监听器不会排队:

php
    <?php

    namespace App\Listeners;

    use App\Events\OrderCreated;
    use Illuminate\Contracts\Queue\ShouldQueue;

    class RewardGiftCard implements ShouldQueue
    {
        /**
         * 向客户奖励一张礼品卡
         */
        public function handle(OrderCreated $event): void
        {
            // ...
        }

        /**
         * 确定是否应将监听器排队
         */
        public function shouldQueue(OrderCreated $event): bool
        {
            return $event->order->subtotal >= 5000;
        }
    }

手动与队列交互

如果您需要手动访问监听器的底层队列作业的 deleterelease 方法,则可以使用 Illuminate\Queue\InteractsWithQueue trait 来实现。默认情况下,此 trait 在生成的监听器上导入,并提供对以下方法的访问:

php
    <?php

    namespace App\Listeners;

    use App\Events\OrderShipped;
    use Illuminate\Contracts\Queue\ShouldQueue;
    use Illuminate\Queue\InteractsWithQueue;

    class SendShipmentNotification implements ShouldQueue
    {
        use InteractsWithQueue;

        /**
         * 处理事件
         */
        public function handle(OrderShipped $event): void
        {
            if (true) {
                $this->release(30);
            }
        }
    }

排队事件监听器和数据库事务

在数据库事务中分派排队的监听器时,它们可能会在数据库事务提交之前由队列处理。发生这种情况时,您在数据库事务期间对模型或数据库记录所做的任何更新可能尚未反映在数据库中。此外,在事务中创建的任何模型或数据库记录都可能不存在于数据库中。如果您的监听器依赖于这些模型,则在处理分派排队监听器的作业时,可能会发生意外错误。

如果您的队列连接的 after_commit 配置选项设置为 false,您仍然可以通过在监听器类上实现 ShouldQueueAfterCommit 接口来指示在所有打开的数据库事务提交后应调度特定的排队监听器:

php
    <?php

    namespace App\Listeners;

    use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
    use Illuminate\Queue\InteractsWithQueue;

    class SendShipmentNotification implements ShouldQueueAfterCommit
    {
        use InteractsWithQueue;
    }

NOTE

要了解有关解决这些问题的更多信息,请查看有关排队作业和数据库事务的文档。

处理失败的作业

有时,排队的事件监听器可能会失败。如果排队的监听器超过队列工作程序定义的最大尝试次数,则会在监听器上调用failed的方法。failed的方法接收事件实例和导致失败的 Throwable:

php
    <?php

    namespace App\Listeners;

    use App\Events\OrderShipped;
    use Illuminate\Contracts\Queue\ShouldQueue;
    use Illuminate\Queue\InteractsWithQueue;
    use Throwable;

    class SendShipmentNotification implements ShouldQueue
    {
        use InteractsWithQueue;

        /**
         * 处理事件
         */
        public function handle(OrderShipped $event): void
        {
            // ...
        }

        /**
         * 处理作业失败
         */
        public function failed(OrderShipped $event, Throwable $exception): void
        {
            // ...
        }
    }

指定排队监听器最大尝试次数

如果其中一个排队的监听器遇到错误,您可能不希望它无限期地重试。因此,Laravel 提供了多种方法来指定可以尝试监听器的次数或时间。

您可以在 listener 类上定义 $tries 属性,以指定在认为监听器失败之前可以尝试多少次:

php
    <?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 实例:

php
    use DateTime;

    /**
     * 确定监听器应超时的时间
     */
    public function retryUntil(): DateTime
    {
        return now()->addMinutes(5);
    }

事件调度

dispatch 事件,您可以对事件调用 static dispatch 方法。此方法可通过 Illuminate\Foundation\Events\Dispatchable trait 在事件上使用。传递给 dispatch 方法的任何参数都将传递给事件的构造函数:

php
    <?php

    namespace App\Http\Controllers;

    use App\Events\OrderShipped;
    use App\Http\Controllers\Controller;
    use App\Models\Order;
    use Illuminate\Http\RedirectResponse;
    use Illuminate\Http\Request;

    class OrderShipmentController extends Controller
    {
        /**
         * 发货给定的订单
         */
        public function store(Request $request): RedirectResponse
        {
            $order = Order::findOrFail($request->order_id);

            // 订单发货逻辑...

            OrderShipped::dispatch($order);

            return redirect('/orders');
        }
    }

如果您想有条件地 dispatch 事件,您可以使用 dispatchIfdispatchUnless 方法:

php
    OrderShipped::dispatchIf($condition, $order);

    OrderShipped::dispatchUnless($condition, $order);

NOTE

在测试时,断言某些事件被分派而不实际触发其监听器可能会有所帮助。Laravel 的内置测试助手使它变得轻而易举。

在数据库事务之后调度事件

有时,您可能希望指示 Laravel 仅在活动数据库事务提交后调度事件。为此,您可以在 event 类上实现 ShouldDispatchAfterCommit 接口。

此接口指示 Laravel 在提交当前数据库事务之前不分派事件。如果事务失败,该事件将被丢弃。如果在 dispatch 事件时没有正在进行的数据库事务,则将立即 dispatch 事件:

php
    <?php

    namespace App\Events;

    use App\Models\Order;
    use Illuminate\Broadcasting\InteractsWithSockets;
    use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
    use Illuminate\Foundation\Events\Dispatchable;
    use Illuminate\Queue\SerializesModels;

    class OrderShipped implements ShouldDispatchAfterCommit
    {
        use Dispatchable, InteractsWithSockets, SerializesModels;

        /**
         * 创建新的事件实例
         */
        public function __construct(
            public Order $order,
        ) {}
    }

事件订阅者

编写事件订阅者

事件订阅者是可以从订阅者类本身订阅多个事件的类,允许您在单个类中定义多个事件处理程序。订阅者应定义一个 subscribe 方法,该方法将传递一个事件调度器实例。您可以对给定的调度程序调用 listen 方法来注册事件监听器:

php
    <?php

    namespace App\Listeners;

    use Illuminate\Auth\Events\Login;
    use Illuminate\Auth\Events\Logout;
    use Illuminate\Events\Dispatcher;

    class UserEventSubscriber
    {
        /**
         * 处理用户登录事件
         */
        public function handleUserLogin(Login $event): void {}

        /**
         * 处理用户注销事件
         */
        public function handleUserLogout(Logout $event): void {}

        /**
         * 为订阅者注册监听器
         */
        public function subscribe(Dispatcher $events): void
        {
            $events->listen(
                Login::class,
                [UserEventSubscriber::class, 'handleUserLogin']
            );

            $events->listen(
                Logout::class,
                [UserEventSubscriber::class, 'handleUserLogout']
            );
        }
    }

如果你的事件监听器方法是在订阅者本身中定义的,你可能会发现从订阅者的 subscribe 方法返回事件和方法名称的数组更方便。Laravel 将在注册事件监听器时自动确定订阅者的类名:

php
    <?php

    namespace App\Listeners;

    use Illuminate\Auth\Events\Login;
    use Illuminate\Auth\Events\Logout;
    use Illuminate\Events\Dispatcher;

    class UserEventSubscriber
    {
        /**
         * 处理用户登录事件
         */
        public function handleUserLogin(Login $event): void {}

        /**
         * 处理用户注销事件
         */
        public function handleUserLogout(Logout $event): void {}

        /**
         * 为订阅者注册监听器
         *
         * @return array<string, string>
         */
        public function subscribe(Dispatcher $events): array
        {
            return [
                Login::class => 'handleUserLogin',
                Logout::class => 'handleUserLogout',
            ];
        }
    }

注册事件订阅者

编写订阅服务器后,您就可以向事件调度器注册它了。您可以使用 Event Facade 的 subscribe 方法注册订阅者。通常,这应该在应用程序的 AppServiceProviderboot 方法中完成:

php
    <?php

    namespace App\Providers;

    use App\Listeners\UserEventSubscriber;
    use Illuminate\Support\Facades\Event;
    use Illuminate\Support\ServiceProvider;

    class AppServiceProvider extends ServiceProvider
    {
        /**
         * Bootstrap any application services.
         */
        public function boot(): void
        {
            Event::subscribe(UserEventSubscriber::class);
        }
    }

测试

在测试分派事件的代码时,您可能希望指示 Laravel 不要实际执行事件的监听器,因为监听器的代码可以直接独立于分派相应事件的代码进行测试。当然,要测试监听器本身,你可以实例化一个监听器实例,并直接在测试中调用 handle 方法。

使用 Event Facade 的 fake 方法,您可以阻止监听器执行、执行被测代码,然后使用 assertDispatchedassertNotDispatchedassertNothingDispatched 方法断言应用程序调度了哪些事件:

php
<?php

use App\Events\OrderFailedToShip;
use App\Events\OrderShipped;
use Illuminate\Support\Facades\Event;

test('orders can be shipped', function () {
    Event::fake();

    // 执行订单发货...

    // 断言事件已调度...
    Event::assertDispatched(OrderShipped::class);

    // 断言事件被调度了两次...
    Event::assertDispatched(OrderShipped::class, 2);

    // 断言事件未调度...
    Event::assertNotDispatched(OrderFailedToShip::class);

    // 断言未调度任何事件...
    Event::assertNothingDispatched();
});
php
<?php

namespace Tests\Feature;

use App\Events\OrderFailedToShip;
use App\Events\OrderShipped;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * 测试订单发货
     */
    public function test_orders_can_be_shipped(): void
    {
        Event::fake();

        // 执行订单发货...

        // 断言已调度事件...
        Event::assertDispatched(OrderShipped::class);

        // 断言事件被调度了两次...
        Event::assertDispatched(OrderShipped::class, 2);

        // 断言未调度事件...
        Event::assertNotDispatched(OrderFailedToShip::class);

        // 断言未调度任何事件...
        Event::assertNothingDispatched();
    }
}

你可以将一个闭包传递给 assertDispatchedassertNotDispatched 方法,以断言一个事件被分派了,它通过了给定的 “真值测试”。如果至少调度了一个通过给定真度测试的事件,则断言将成功:

php
    Event::assertDispatched(function (OrderShipped $event) use ($order) {
        return $event->order->id === $order->id;
    });

如果你只是想断言事件监听器正在侦听给定的事件,你可以使用 assertListening 方法:

php
    Event::assertListening(
        OrderShipped::class,
        SendShipmentNotification::class
    );

WARNING

调用 Event::fake() 后,不会执行任何事件监听器。因此,如果你的测试使用依赖于事件的模型工厂,例如在模型的创建事件期间创建一个 UUID,你应该在使用工厂后调用 Event::fake()。

伪造事件的子集

如果你只想为一组特定的事件伪造事件监听器,你可以将它们传递给fakefakeFor方法:

php
test('orders can be processed', function () {
    Event::fake([
        OrderCreated::class,
    ]);

    $order = Order::factory()->create();

    Event::assertDispatched(OrderCreated::class);

    // 其他事件照常调度...
    $order->update([...]);
});
php
/**
 * 测试订单流程
 */
public function test_orders_can_be_processed(): void
{
    Event::fake([
        OrderCreated::class,
    ]);

    $order = Order::factory()->create();

    Event::assertDispatched(OrderCreated::class);

    // 其他事件将照常调度...
    $order->update([...]);
}

您可以使用 except 方法伪造除一组指定事件之外的所有事件:

php
    Event::fake()->except([
        OrderCreated::class,
    ]);

范围事件伪造

如果你只想为测试的一部分伪造事件监听器,你可以使用 fakeFor 方法:

php
<?php

use App\Events\OrderCreated;
use App\Models\Order;
use Illuminate\Support\Facades\Event;

test('orders can be processed', function () {
    $order = Event::fakeFor(function () {
        $order = Order::factory()->create();

        Event::assertDispatched(OrderCreated::class);

        return $order;
    });

    // 事件照常调度,观察者将运行 ...
    $order->update([...]);
});
php
<?php

namespace Tests\Feature;

use App\Events\OrderCreated;
use App\Models\Order;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * 测试订单流程
     */
    public function test_orders_can_be_processed(): void
    {
        $order = Event::fakeFor(function () {
            $order = Order::factory()->create();

            Event::assertDispatched(OrderCreated::class);

            return $order;
        });

        // 事件照常调度,观察者将运行 ...
        $order->update([...]);
    }
}