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 开头的方法时,会将这些方法注册为方法签名中类型提示所对应事件的监听器:

php
use App\Events\PodcastProcessed;

class SendPodcastNotification
{
    /**
     * Handle the event.
     */
    public function handle(PodcastProcessed $event): void
    {
        // ...
    }
}

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

php
/**
 * Handle the event.
 */
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 门面,可在应用的 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

闭包监听器

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

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()->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) {
    // The queued listener failed...
}));

通配符事件监听器

你也可以使用 * 字符作为通配符参数注册监听器,从而在同一监听器上捕获多个事件。通配符监听器的第一个参数是事件名称,第二个参数是完整的事件数据数组:

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;

    /**
     * Create a new event instance.
     */
    public function __construct(
        public Order $order,
    ) {}
}

可以看到,该事件类不包含任何逻辑。它只是已购买的 App\Models\Order 实例的容器。事件所使用的 SerializesModels trait 会在事件对象通过 PHP 的 serialize 函数序列化时(例如使用队列监听器时)优雅地序列化其中的 Eloquent 模型。

定义监听器

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

php
<?php

namespace App\Listeners;

use App\Events\OrderShipped;

class SendShipmentNotification
{
    /**
     * Create the event listener.
     */
    public function __construct() {}

    /**
     * Handle the event.
     */
    public function handle(OrderShipped $event): void
    {
        // Access the order using $event->order...
    }
}

INFO

事件监听器也可以在构造函数中类型提示所需的任意依赖。所有事件监听器都通过 Laravel 服务容器 解析,因此依赖会自动注入。

停止事件传播

有时,你可能希望停止事件向其他监听器传播。可在监听器的 handle 方法中返回 false 来实现。

队列事件监听器

若监听器将执行发送邮件或发起 HTTP 请求等较慢的任务,将监听器入队会很有帮助。使用队列监听器之前,请确保已配置队列,并在服务器或本地开发环境中启动队列 Worker。

若要指定监听器应入队,请为监听器类添加 ShouldQueue 接口。由 make:listener Artisan 命令生成的监听器已将该接口导入到当前命名空间,可立即使用:

php
<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
    // ...
}

就是这样!现在,当该监听器所处理的事件被派发时,事件调度器会使用 Laravel 的队列系统自动将监听器入队。若队列执行监听器时未抛出异常,队列任务在处理完成后会自动删除。

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

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

php
<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
    /**
     * The name of the connection the job should be sent to.
     *
     * @var string|null
     */
    public $connection = 'sqs';

    /**
     * The name of the queue the job should be sent to.
     *
     * @var string|null
     */
    public $queue = 'listeners';

    /**
     * The time (seconds) before the job should be processed.
     *
     * @var int
     */
    public $delay = 60;
}

若要在运行时定义监听器的队列连接、队列名称或延迟,可在监听器上定义 viaConnectionviaQueuewithDelay 方法:

php
/**
 * Get the name of the listener's queue connection.
 */
public function viaConnection(): string
{
    return 'sqs';
}

/**
 * Get the name of the listener's queue.
 */
public function viaQueue(): string
{
    return 'listeners';
}

/**
 * Get the number of seconds before the job should be processed.
 */
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
{
    /**
     * Reward a gift card to the customer.
     */
    public function handle(OrderCreated $event): void
    {
        // ...
    }

    /**
     * Determine whether the listener should be queued.
     */
    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;

    /**
     * Handle the event.
     */
    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;
}

INFO

若要进一步了解如何处理这些问题,请参阅有关队列任务与数据库事务的文档。

队列监听器中间件

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

php
<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
    /**
     * Handle the event.
     */
    public function handle(OrderShipped $event): void
    {
        // Process the event...
    }

    /**
     * Get the middleware the listener should pass through.
     *
     * @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
{
    /**
     * The number of seconds after which the listener's unique lock will be released.
     *
     * @var int
     */
    public $uniqueFor = 3600;

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

    /**
     * Get the unique ID for the listener.
     */
    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
{
    // ...

    /**
     * Get the cache driver for the unique listener lock.
     */
    public function uniqueVia(LicenseSaved $event): Repository
    {
        return Cache::driver('redis');
    }
}

INFO

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

处理失败任务

有时队列事件监听器可能会失败。若队列监听器超过队列 Worker 定义的最大尝试次数,将调用监听器上的 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;

    /**
     * Handle the event.
     */
    public function handle(OrderShipped $event): void
    {
        // ...
    }

    /**
     * Handle a job failure.
     */
    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\InteractsWithQueue;

class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    /**
     * The number of times the queued listener may be attempted.
     *
     * @var int
     */
    public $tries = 5;
}

作为指定监听器失败前可尝试次数的替代方案,你可以定义一个时间点,超过该时间后不再尝试该监听器。这样可在给定时间范围内任意次数地尝试监听器。要定义不再尝试的时间点,请在监听器类中添加 retryUntil 方法。该方法应返回一个 DateTime 实例:

php
use DateTime;

/**
 * Determine the time at which the listener should timeout.
 */
public function retryUntil(): DateTime
{
    return now()->plus(minutes: 5);
}

若同时定义了 retryUntiltries,Laravel 会优先使用 retryUntil 方法。

指定队列监听器的退避时间

若要配置 Laravel 在重试遇到异常的监听器之前应等待多少秒,可在监听器类上定义 backoff 属性:

php
/**
 * The number of seconds to wait before retrying the queued listener.
 *
 * @var int
 */
public $backoff = 3;

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

php
/**
 * Calculate the number of seconds to wait before retrying the queued listener.
 */
public function backoff(OrderShipped $event): int
{
    return 3;
}

通过从 backoff 方法返回退避值数组,可轻松配置「指数」退避。在此示例中,第一次重试延迟 1 秒,第二次 5 秒,第三次 10 秒;若还有剩余尝试次数,之后每次重试均为 10 秒:

php
/**
 * Calculate the number of seconds to wait before retrying the queued listener.
 *
 * @return list<int>
 */
public function backoff(OrderShipped $event): array
{
    return [1, 5, 10];
}

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

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

php
<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    /**
     * The number of times the queued listener may be attempted.
     *
     * @var int
     */
    public $tries = 25;

    /**
     * The maximum number of unhandled exceptions to allow before failing.
     *
     * @var int
     */
    public $maxExceptions = 3;

    /**
     * Handle the event.
     */
    public function handle(OrderShipped $event): void
    {
        // Process the event...
    }
}

在此示例中,监听器最多重试 25 次。但若监听器抛出三个未处理异常,监听器将失败。

指定队列监听器超时

通常你大致知道排队监听器需要多长时间。因此,Laravel 允许你指定「超时」值。如果监听器处理时间超过超时值指定的秒数,处理该监听器的 worker 将以错误退出。你可以通过在监听器类上定义 timeout 属性来指定监听器允许运行的最长秒数:

php
<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
    /**
     * The number of seconds the listener can run before timing out.
     *
     * @var int
     */
    public $timeout = 120;
}

若希望监听器在超时时被标记为失败,可在监听器类上定义 failOnTimeout 属性:

php
<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
    /**
     * Indicate if the listener should be marked as failed on timeout.
     *
     * @var bool
     */
    public $failOnTimeout = true;
}

派发事件

要派发事件,可调用事件上的静态 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
{
    /**
     * Ship the given order.
     */
    public function store(Request $request): RedirectResponse
    {
        $order = Order::findOrFail($request->order_id);

        // Order shipment logic...

        OrderShipped::dispatch($order);

        return redirect('/orders');
    }
}

若要有条件地派发事件,可使用 dispatchIfdispatchUnless 方法:

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

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

INFO

在测试时,断言某些事件已被派发而不实际触发其监听器会很有帮助。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;

    /**
     * Create a new event instance.
     */
    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
{
    /**
     * Handle user login events.
     */
    public function handleUserLogin(Login $event): void {}

    /**
     * Handle user logout events.
     */
    public function handleUserLogout(Logout $event): void {}

    /**
     * Register the listeners for the subscriber.
     */
    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
{
    /**
     * Handle user login events.
     */
    public function handleUserLogin(Login $event): void {}

    /**
     * Handle user logout events.
     */
    public function handleUserLogout(Logout $event): void {}

    /**
     * Register the listeners for the subscriber.
     *
     * @return array<string, string>
     */
    public function subscribe(Dispatcher $events): array
    {
        return [
            Login::class => 'handleUserLogin',
            Logout::class => 'handleUserLogout',
        ];
    }
}

注册事件订阅者

编写订阅者后,若其遵循 Laravel 的事件发现约定,Laravel 会自动注册订阅者内的处理方法。否则,可使用 Event 门面的 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 门面的 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();

    // Perform order shipping...

    // Assert that an event was dispatched...
    Event::assertDispatched(OrderShipped::class);

    // Assert an event was dispatched twice...
    Event::assertDispatched(OrderShipped::class, 2);

    // Assert an event was dispatched once...
    Event::assertDispatchedOnce(OrderShipped::class);

    // Assert an event was not dispatched...
    Event::assertNotDispatched(OrderFailedToShip::class);

    // Assert that no events were dispatched...
    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
{
    /**
     * Test order shipping.
     */
    public function test_orders_can_be_shipped(): void
    {
        Event::fake();

        // Perform order shipping...

        // Assert that an event was dispatched...
        Event::assertDispatched(OrderShipped::class);

        // Assert an event was dispatched twice...
        Event::assertDispatched(OrderShipped::class, 2);

        // Assert an event was dispatched once...
        Event::assertDispatchedOnce(OrderShipped::class);

        // Assert an event was not dispatched...
        Event::assertNotDispatched(OrderFailedToShip::class);

        // Assert that no events were dispatched...
        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);

    // Other events are dispatched as normal...
    $order->update([
        // ...
    ]);
});
php
/**
 * Test order process.
 */
public function test_orders_can_be_processed(): void
{
    Event::fake([
        OrderCreated::class,
    ]);

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

    Event::assertDispatched(OrderCreated::class);

    // Other events are dispatched as normal...
    $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;
    });

    // Events are dispatched as normal and observers will run...
    $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
{
    /**
     * Test order process.
     */
    public function test_orders_can_be_processed(): void
    {
        $order = Event::fakeFor(function () {
            $order = Order::factory()->create();

            Event::assertDispatched(OrderCreated::class);

            return $order;
        });

        // Events are dispatched as normal and observers will run...
        $order->update([
            // ...
        ]);
    }
}