简介

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

事件是解耦应用程序各个方面的绝佳方式,因为单个事件可以拥有多个互不依赖的监听器。例如,你可能希望在每次订单发货时向用户发送 Slack 通知。与其将订单处理代码与 Slack 通知代码耦合在一起,不如触发 App\Events\OrderShipped 事件,监听器可以接收该事件并利用它发送 Slack 通知。

生成事件与监听器

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

php artisan make:event PodcastProcessed

php artisan make:listener SendPodcastNotification --event=PodcastProcessed

为了方便起见,你也可以不带额外参数调用 make:event 和 make:listener Artisan 命令。当你这样做时,Laravel 会自动提示你输入类名,并且在创建监听器时,还会提示你输入该监听器应监听的事件:

php artisan make:event

php artisan make:listener

注册事件与监听器

事件发现

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

use App\Events\PodcastProcessed;

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

你可以使用 PHP 的联合类型来监听多个事件:

/**
 * 处理事件。
 */
public function handle(PodcastProcessed|PodcastPublished $event): void
{
    // ...
}

如果你打算将监听器存放在其他目录或多个目录中,可以在应用程序的 bootstrap/app.php 文件中使用 withEvents 方法,指示 Laravel 扫描这些目录:

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

你可以使用 * 字符作为通配符,扫描多个相似的目录来查找监听器:

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

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

php artisan event:list

生产环境中的事件发现

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

动态事件发现

若要动态控制某个监听器是否会被发现,可以在监听器类上实现 ShouldBeDiscovered 接口,并定义一个返回布尔值的 shouldBeDiscovered 方法。如果该方法返回 false,则该监听器在事件发现过程中不会被注册:

use Illuminate\Contracts\Events\ShouldBeDiscovered;

class SendPodcastNotification implements ShouldBeDiscovered
{
    /**
     * 处理事件。
     */
    public function handle(PodcastProcessed $event): void
    {
        // ...
    }

    /**
     * 判断该监听器是否应被发现。
     */
    public static function shouldBeDiscovered(): bool
    {
        return app()->environment('production');
    }
}

手动注册事件

通过 Event 门面,你可以在应用程序的 AppServiceProvider 的 boot 方法中手动注册事件及其对应的监听器:

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

/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    Event::listen(
        PodcastProcessed::class,
        SendPodcastNotification::class,
    );
}

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

php artisan event:list

闭包监听器

通常情况下,监听器以类的形式定义;不过,你也可以在应用程序的 AppServiceProvider 的 boot 方法中手动注册基于闭包的事件监听器:

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

/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    Event::listen(function (PodcastProcessed $event) {
        // ...
    });
}

可排队的匿名事件监听器

在注册基于闭包的事件监听器时,你可以将监听器闭包包裹在 Illuminate\Events\queueable 函数中,以此指示 Laravel 通过队列来执行该监听器:

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

/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    Event::listen(queueable(function (PodcastProcessed $event) {
        // ...
    }));
}

与排队任务一样,你可以使用 onConnection、onQueue 和 delay 方法来自定义排队监听器的执行:

Event::listen(queueable(function (PodcastProcessed $event) {
    // ...
})->onConnection('redis')->onQueue('podcasts')->delay(now()->plus(seconds: 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 (string $eventName, array $data) {
    // ...
});

定义事件

事件类本质上是一个数据容器,用于保存与事件相关的信息。例如,假设 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;

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

如你所见,该事件类不包含任何逻辑。它是所购买的 App\Models\Order 实例的容器。如果事件对象使用 PHP 的 serialize 函数进行序列化(例如在使用排队监听器时),事件所使用的 SerializesModels 特征会优雅地序列化任何 Eloquent 模型。

定义监听器

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

<?php

namespace App\Listeners;

use App\Events\OrderShipped;

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

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

你的事件监听器也可以在其构造函数中对所需的任何依赖进行类型提示。所有事件监听器均由 Laravel 服务容器 解析,因此依赖会自动注入。

停止事件传播

有时,你可能希望阻止事件向其他监听器传播。你可以通过让监听器的 handle 方法返回 false 来实现这一点。

队列事件监听器

如果你的监听器要执行耗时任务(如发送电子邮件或发起 HTTP 请求),对监听器进行排队会带来益处。在使用队列监听器之前,请务必配置队列,并在服务器或本地开发环境中启动队列工作进程。

要指定监听器应进行排队,请将 ShouldQueue 接口添加到监听器类中。由 make:listener 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;
use Illuminate\Queue\Attributes\Connection;
use Illuminate\Queue\Attributes\Delay;
use Illuminate\Queue\Attributes\Queue;

#[Connection('sqs')]
#[Queue('listeners')]
#[Delay(60)]
class SendShipmentNotification implements ShouldQueue
{
    // ...
}

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

/**
 * 获取监听器的队列连接名称。
 */
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 方法,以确定监听器是否应排队。如果 shouldQueue 方法返回 false,监听器将不会排队:

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

手动与队列交互

如果需要手动访问监听器底层队列任务的 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;

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

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

当排队监听器在数据库事务内被分发时,它们可能会在数据库事务提交之前由队列处理。此时,你在数据库事务期间对模型或数据库记录所做的任何更新,可能尚未反映到数据库中。此外,在事务内创建的任何模型或数据库记录,也可能尚未存在于数据库中。如果监听器依赖于这些模型,那么在处理分发该排队监听器的任务时,就可能出现意外错误。

如果队列连接的 after_commit 配置选项设置为 false,你仍可通过在监听器类上实现 ShouldQueueAfterCommit 接口,来指定特定排队监听器应在所有未关闭的数据库事务提交之后再进行分发:

<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueueAfterCommit
{
    use InteractsWithQueue;
}

要进一步了解如何规避这些问题,请参阅关于排队任务与数据库事务的文档。

排队监听器中间件

排队监听器也可以使用任务中间件。任务中间件允许你在排队监听器的执行过程中包裹自定义逻辑,从而减少监听器本身的样板代码。创建任务中间件后,可以通过监听器的 middleware 方法返回它们,将其附加到监听器上:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use App\Jobs\Middleware\RateLimited;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendShipmentNotification implements ShouldQueue
{
    /**
     * 处理事件。
     */
    public function handle(OrderShipped $event): void
    {
        // 处理事件……
    }

    /**
     * 获取监听器应经过的中间件。
     *
     * @return array<int, object>
     */
    public function middleware(OrderShipped $event): array
    {
        return [new RateLimited];
    }
}

加密的排队监听器

Laravel 允许通过加密来确保排队监听器数据的私密性和完整性。要使用该功能,只需将 ShouldBeEncrypted 接口添加到监听器类中。一旦将该接口添加到类中,Laravel 就会在将监听器推送到队列之前自动对其进行加密:

<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue, ShouldBeEncrypted
{
    // ...
}

唯一事件监听器

唯一监听器需要支持锁的缓存驱动。目前,memcached、redis、dynamodb、database、file 和 array 缓存驱动支持原子锁。

有时,你可能希望确保在任何时刻,队列中只有一个特定监听器的实例。你可以通过在监听器类上实现 ShouldBeUnique 接口来实现这一点:

<?php

namespace App\Listeners;

use App\Events\LicenseSaved;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;

class AcquireProductKey implements ShouldQueue, ShouldBeUnique
{
    public function __invoke(LicenseSaved $event): void
    {
        // ...
    }
}

在上面的示例中,AcquireProductKey 监听器是唯一的。因此,如果该监听器的另一个实例已经在队列中且尚未完成处理,该监听器将不会被排队。这确保了每个许可证仅获取一个产品密钥,即使许可证在短时间内被多次保存。

在某些情况下,你可能希望定义一个特定的“键”来确保监听器的唯一性,或者希望指定一个超时时间,超过该时间后监听器不再保持唯一。为此,你可以在监听器类中定义 uniqueId 和 uniqueFor 属性或方法。这些方法会接收事件实例,从而允许你使用事件数据来构建返回值:

<?php

namespace App\Listeners;

use App\Events\LicenseSaved;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;

class AcquireProductKey implements ShouldQueue, ShouldBeUnique
{
    /**
     * 监听器的唯一锁将在此时之后多少秒后释放。
     *
     * @var int
     */
    public $uniqueFor = 3600;

    public function __invoke(LicenseSaved $event): void
    {
        // ...
    }

    /**
     * 获取监听器的唯一 ID。
     */
    public function uniqueId(LicenseSaved $event): string
    {
        return 'listener:'.$event->license->id;
    }
}

在上面的示例中,AcquireProductKey 监听器依据许可证 ID 保持唯一性。因此,在现有监听器完成处理之前,针对同一许可证的任何新监听器派发都会被忽略。这可以防止为同一许可证获取重复的产品密钥。此外,如果现有监听器未在一小时内完成处理,唯一锁将被释放,具有相同唯一键的另一个监听器可以被加入队列。

如果你的应用从多个 Web 服务器或容器中派发事件,应确保所有服务器都与同一中央缓存服务器通信,以便 Laravel 能够准确判断监听器是否唯一。

在处理开始前保持监听器的唯一性

默认情况下,唯一监听器会在监听器完成处理或所有重试尝试均失败后“解锁”。不过,在某些情况下,你可能希望监听器在即将被处理之前立即解锁。为此,你的监听器应实现 ShouldBeUniqueUntilProcessing 契约,而不是 ShouldBeUnique 契约:

<?php

namespace App\Listeners;

use App\Events\LicenseSaved;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Contracts\Queue\ShouldQueue;

class AcquireProductKey implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
    // ...
}

唯一监听器锁

在底层,当一个 ShouldBeUnique 监听器被派发时,Laravel 会尝试使用 uniqueId 键获取锁。如果该锁已被持有,监听器将不会被派发。当监听器完成处理或所有重试尝试失败后,此锁会被释放。默认情况下,Laravel 会使用默认缓存驱动来获取该锁。不过,如果你希望使用其他驱动来获取锁,可以定义一个返回应使用缓存驱动的 uniqueVia 方法:

<?php

namespace App\Listeners;

use App\Events\LicenseSaved;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;

class AcquireProductKey implements ShouldQueue, ShouldBeUnique
{
    // ...

    /**
     * 获取用于唯一监听器锁的缓存驱动。
     */
    public function uniqueVia(LicenseSaved $event): Repository
    {
        return Cache::driver('redis');
    }
}

如果你只需要限制监听器的并发处理,请改用 WithoutOverlapping 作业中间件。

防抖事件监听器

有时,你可能只想处理在较短时间内反复派发的事件中的最新实例。为此,你可以在排队监听器上添加 DebounceFor 特性:

<?php

namespace App\Listeners;

use App\Events\ProductUpdated;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\DebounceFor;

#[DebounceFor(30)]
class UpdateProductSearchIndex implements ShouldQueue
{
    /**
     * 处理事件。
     */
    public function handle(ProductUpdated $event): void
    {
        // 更新产品的搜索索引...
    }

    /**
     * 获取监听器的防抖 ID。
     */
    public function debounceId(ProductUpdated $event): string
    {
        return (string) $event->product->getKey();
    }
}

在上面的示例中,在 30 秒内为同一产品反复派发 ProductUpdated 事件时,监听器会被防抖,从而仅处理最新的事件。不同的防抖 ID 会独立处理。

如果你希望限制频繁派发的事件最多可延迟监听器的时间,可以在 DebounceFor 特性中提供 maxWait 参数:

#[DebounceFor(30, maxWait: 120)]
class UpdateProductSearchIndex implements ShouldQueue
{
    // ...
}

你可以通过在监听器上定义 debounceVia 方法,来自定义用于防抖跟踪的缓存存储。该方法接收事件实例,并应返回一个缓存仓库:

use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;

public function debounceVia(ProductUpdated $event): Repository
{
    return Cache::driver('redis');
}

防抖监听器和唯一监听器是互斥的。使用 DebounceFor 特性的监听器不应实现 ShouldBeUnique。

如果你的应用从多个 Web 服务器或容器中派发事件,应确保所有服务器都与同一中央缓存服务器通信。

处理失败的作业

有时,你的排队事件监听器可能会失败。如果排队监听器超出了队列工作器定义的最大尝试次数,监听器上的 failed 方法将会被调用。failed 方法接收事件实例以及导致失败的 Throwable:

<?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 提供了多种方式来指定监听器可以被尝试的次数或时长。

你可以在监听器类上使用 Tries 特性,来指定监听器在被判定失败前最多可以被尝试多少次:

<?php

namespace App\Listeners;

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

#[Tries(5)]
class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    // ...
}

除了定义监听器在失败前可被尝试的次数,你还可以定义一个时间,超过该时间后监听器不再被尝试。这样,监听器可以在给定时间范围内被尝试任意次数。要定义监听器不再被尝试的时间,请在监听器类中添加 retryUntil 方法。该方法应返回一个 DateTimeInterface 实例:

use DateTimeInterface;

/**
 * 确定监听器应在何时超时。
 */
public function retryUntil(): DateTimeInterface
{
    return now()->plus(minutes: 5);
}

如果同时定义了 retryUntil 和 tries,Laravel 会优先使用 retryUntil 方法。

指定排队监听器的退避

如果你想配置 Laravel 在重试遇到异常的监听器之前应等待多少秒,可以在监听器类上使用 Backoff 特性:

<?php

namespace App\Listeners;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Backoff;

#[Backoff(3)]
class SendShipmentNotification implements ShouldQueue
{
    // ...
}

如果你需要更复杂的逻辑来确定监听器的退避时间,可以在监听器类上定义一个 backoff 方法:

/**
 * 计算重试排队监听器前需等待的秒数。
 */
public function backoff(OrderShipped $event): int
{
    return 3;
}

通过从 backoff 方法返回一个退避值数组,你可以轻松配置“指数”退避。在本示例中,第一次重试的延迟将为 1 秒,第二次重试为 5 秒,第三次重试为 10 秒,如果仍有更多重试次数,后续每次重试的延迟均为 10 秒:

/**
 * 计算重试队列监听器前需等待的秒数。
 *
 * @return list<int>
 */
public function backoff(OrderShipped $event): array
{
    return [1, 5, 10];
}

指定队列监听器的最大异常数

有时,你可能希望指定队列监听器可以被多次尝试,但若重试是由特定数量的未处理异常触发的(而非直接通过 release 方法释放),则应使其失败。为实现这一点,你可以在监听器类上使用 Tries 和 MaxExceptions 特性:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\MaxExceptions;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\InteractsWithQueue;

#[Tries(25)]
#[MaxExceptions(3)]
class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

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

在此示例中,监听器最多将被重试 25 次。不过,如果监听器抛出三次未处理的异常,该监听器将失败。

指定队列监听器的超时时间

通常,你大致了解队列监听器预期的执行时长。因此,Laravel 允许你指定一个“超时”值。如果监听器的处理时间超过了超时值所指定的秒数,处理该监听器的工作进程将报错退出。你可以在监听器类上使用 Timeout 特性,以定义监听器允许运行的最大秒数:

<?php

namespace App\Listeners;

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

#[Timeout(120)]
class SendShipmentNotification implements ShouldQueue
{
    // ...
}

如果你希望表明监听器超时时应被标记为失败,可以在监听器类上使用 FailOnTimeout 特性:

<?php

namespace App\Listeners;

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

#[FailOnTimeout]
class SendShipmentNotification implements ShouldQueue
{
    // ...
}

分发事件

要分发事件,你可以调用事件上的静态 dispatch 方法。该方法是通过 Illuminate\Foundation\Events\Dispatchable 特性在事件上提供的。传递给 dispatch 方法的任何参数都会被传递给事件的构造函数:

<?php

namespace App\Http\Controllers;

use App\Events\OrderShipped;
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');
    }
}

如果你希望有条件地分发事件,可以使用 dispatchIf 和 dispatchUnless 方法:

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

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

进行测试时,断言某些事件已被分发,而不实际触发其监听器会很有用。Laravel 内置的测试辅助方法让这件事变得轻而易举。

在数据库事务后分发事件

有时,你可能希望指示 Laravel 仅在当前数据库事务提交后分发事件。为此,你可以在事件类上实现 ShouldDispatchAfterCommit 接口。

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

<?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,
    ) {}
}

延迟事件

延迟事件允许你将模型事件的分发和事件监听器的执行推迟到特定代码块执行完成后。当你需要确保所有相关记录都已创建,之后再触发事件监听器时,这一点尤为有用。

要延迟事件,可将一个闭包传递给 Event::defer() 方法:

use App\Models\User;
use Illuminate\Support\Facades\Event;

Event::defer(function () {
    $user = User::create(['name' => 'Victoria Otwell']);

    $user->posts()->create(['title' => 'My first post!']);
});

闭包内触发的所有事件都将在该闭包执行完毕后进行分发。这能确保事件监听器可以访问延迟执行期间创建的所有相关记录。若闭包内出现异常,延迟事件将不会被分发。

若要仅延迟特定事件,可将事件数组作为第二个参数传递给 defer 方法:

use App\Models\User;
use Illuminate\Support\Facades\Event;

Event::defer(function () {
    $user = User::create(['name' => 'Victoria Otwell']);

    $user->posts()->create(['title' => 'My first post!']);
}, ['eloquent.created: '.User::class]);

事件订阅器

编写事件订阅器

事件订阅器是一类可在订阅器类内部订阅多个事件的类,它允许你在单个类中定义多个事件处理程序。订阅器应定义一个 subscribe 方法,该方法接收一个事件分发器实例。你可以在给定的分发器上调用 listen 方法来注册事件监听器:

<?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

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

注册事件订阅器

编写好订阅器后,若订阅器中的处理程序方法遵循 Laravel 的事件发现约定,Laravel 会自动注册这些方法。否则,你可以使用 Event 外观的 subscribe 方法手动注册订阅器。通常,这应在应用程序的 AppServiceProvider 的 boot 方法中完成:

<?php

namespace App\Providers;

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

class AppServiceProvider extends ServiceProvider
{
    /**
     * 引导任何应用服务。
     */
    public function boot(): void
    {
        Event::subscribe(UserEventSubscriber::class);
    }
}

测试

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

使用 Event 门面的 fake 方法,你可以阻止监听器执行,运行待测试的代码,然后使用 assertDispatched、assertNotDispatched 和 assertNothingDispatched 方法来断言应用分发了哪些事件:

<?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::assertDispatchedOnce(OrderShipped::class);

    // 断言事件未被分发...
    Event::assertNotDispatched(OrderFailedToShip::class);

    // 断言未分发任何事件...
    Event::assertNothingDispatched();
});
<?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::assertDispatchedOnce(OrderShipped::class);

        // 断言事件未被分发...
        Event::assertNotDispatched(OrderFailedToShip::class);

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

你可以将一个闭包传递给 assertDispatched 或 assertNotDispatched 方法,以断言分发了一个通过了给定“真值测试”的事件。只要至少有一个分发的事件通过了给定的真值测试,断言就会成功:

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

或者,你可以将一组预期的属性值作为第二个参数传递给 assertDispatched 或 assertNotDispatched:

Event::assertDispatched(OrderShipped::class, ['order' => $order]);

所有指定的属性都必须匹配。值使用严格相等进行比较,而 Eloquent 模型则使用其 is 方法进行比较。

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

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

调用 Event::fake() 后,所有事件监听器都不会执行。因此,如果你的测试使用了依赖事件的模型工厂,例如在模型的 creating 事件中创建 UUID,那么你应该在使用工厂之后调用 Event::fake()。

对部分事件进行伪造

如果你只想伪造特定事件集合的事件监听器,可以将这些事件传递给 fake 或 fakeFor 方法:

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

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

    Event::assertDispatched(OrderCreated::class);

    // 其他事件按正常方式分发...
    $order->update([
        // ...
    ]);
});
/**
 * 测试订单流程。
 */
public function test_orders_can_be_processed(): void
{
    Event::fake([
        OrderCreated::class,
    ]);

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

    Event::assertDispatched(OrderCreated::class);

    // 其他事件按正常方式分发...
    $order->update([
        // ...
    ]);
}

你可以使用 except 方法,除了一组指定的事件外,伪造所有其他事件:

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

作用域事件伪造

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

<?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

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([
            // ...
        ]);
    }
}