Skip to content

事件

简介

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

事件是解耦应用各个方面的好方法,因为单个事件可以拥有多个彼此不依赖的监听器。例如,你可能希望在每次订单发货时向用户发送 Slack 通知。与其将订单处理代码与 Slack 通知代码耦合在一起,不如触发一个 App\Events\OrderShipped 事件,监听器可以接收该事件并用它来发送 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
    {
        // ...
    }
}

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

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

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

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

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

php
->withEvents(discover: [
    __DIR__.'/../app/Domain/*/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;

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

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

shell
php artisan event:list

闭包监听器

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

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

/**
 * 引导任何应用服务。
 */
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;

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

与队列任务类似,你可以使用 onConnectiononQueuedelay 方法来自定义队列监听器的执行:

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

如果你想处理匿名队列监听器的失败,可以在定义 queueable 监听器时向 catch 方法提供一个闭包。该闭包将接收事件实例和导致监听器失败的 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
{
    // ...
}

就是这样!现在,当此监听器处理的事件被触发时,监听器将使用 Laravel 的队列系统由事件分发器自动加入队列。如果监听器被队列执行时没有抛出异常,队列任务将在处理完成后自动删除。

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

如果你想自定义事件监听器的队列连接、队列名称或队列延迟时间,你可以在监听器类上使用 ConnectionQueueDelay 属性:

php
<?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
{
    // ...
}

如果你想在运行时定义监听器的队列连接、队列名称或延迟,可以在监听器上定义 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 ($condition) {
            $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

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

队列监听器中间件

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

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

namespace App\Listeners;

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

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

唯一事件监听器

WARNING

唯一监听器需要支持的缓存驱动。目前,memcachedredisdynamodbdatabasefilearray 缓存驱动支持原子锁。

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

php
<?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 监听器是唯一的。因此,如果监听器的另一个实例已经在队列中且尚未完成处理,则监听器将不会被加入队列。这确保了即使许可证在短时间内被多次保存,每个许可证也只会获取一个产品密钥。

在某些情况下,你可能想要定义一个使监听器唯一的特定"键",或者你可能想要指定一个超时时间,超过该时间后监听器不再保持唯一。为此,你可以在监听器类上定义 uniqueIduniqueFor 属性或方法。这些方法接收事件实例,允许你使用事件数据来构造返回值:

php
<?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 唯一。因此,对同一许可证的任何新监听器分发将被忽略,直到现有监听器完成处理。这可以防止为同一许可证获取重复的产品密钥。此外,如果现有监听器在一小时内没有被处理,唯一锁将被释放,具有相同唯一键的另一个监听器可以被加入队列。

WARNING

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

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

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

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

NOTE

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

处理失败的任务

有时你的队列事件监听器可能会失败。如果队列监听器超过了队列工作进程定义的最大尝试次数,监听器上的 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 提供了各种方式来指定监听器可以尝试多少次或持续多长时间。

你可以在监听器类上使用 Tries 属性来指定监听器在被视为失败之前可以尝试的次数:

php
<?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 方法。此方法应返回一个 DateTime 实例:

php
use DateTime;

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

如果同时定义了 retryUntiltries,Laravel 将优先使用 retryUntil 方法。

指定队列监听器退避时间

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

php
<?php

namespace App\Listeners;

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

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

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

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

你可以通过从 backoff 方法返回退避值数组来轻松配置"指数"退避。在此示例中,第一次重试的延迟为 1 秒,第二次重试为 5 秒,第三次重试为 10 秒,如果还有更多剩余尝试,则每次后续重试为 10 秒:

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

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

有时你可能希望指定一个队列监听器可以被尝试多次,但如果重试是由给定数量的未处理异常触发的(而不是直接由 release 方法释放的),则应该失败。为此,你可以在监听器类上使用 TriesMaxExceptions 属性:

php
<?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
<?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
<?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 trait 在事件上可用。传递给 dispatch 方法的任何参数都将传递给事件的构造函数:

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

如果你想有条件地分发事件,可以使用 dispatchIfdispatchUnless 方法:

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

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

NOTE

在测试时,断言某些事件已被分发而不实际触发其监听器会很有帮助。Laravel 的内置测试辅助工具使这变得轻而易举。

在数据库事务之后分发事件

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

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

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

延迟事件

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

要延迟事件,请向 Event::defer() 方法提供一个闭包:

php
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 方法:

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

注册事件订阅者

编写完订阅者后,如果它们遵循 Laravel 的事件发现约定,Laravel 将自动注册订阅者中的处理方法。否则,你可以使用 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
{
    /**
     * 引导任何应用服务。
     */
    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::assertDispatchedOnce(OrderShipped::class);

    // 断言事件未被分发...
    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::assertDispatchedOnce(OrderShipped::class);

        // 断言事件未被分发...
        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() 后,不会执行任何事件监听器。因此,如果你的测试使用了依赖事件的模型工厂(例如在模型的 creating 事件期间创建 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([
            // ...
        ]);
    }
}