> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 包的事件监听器注册与缓存

> 解读 Laravel 13 的事件注册与 event:cache 的实现，讲解包的显式注册、与自动发现的重复注册、依赖解析以及更新时的验证方法。

想要为包提供事件驱动的扩展点时，需要将**事件类的提供**与**监听器的注册**分开设计。如果采用即使用户不把 `vendor` 内的目录加入自动发现也能正常工作的结构，就可以减少安装、更新和创建缓存时的前提条件。

本页会确认 Laravel Framework `v13.36.0` 的实现，将[事件](/zh-CN/events)的基本操作应用到包的长期维护中。Provider 的加载请参阅[包自动发现](/zh-CN/advanced/package-discovery)，测试环境请参阅[包的测试](/zh-CN/advanced/package-testing)。

## 显式注册包的监听器

包默认执行的处理，可以在普通 `Illuminate\Support\ServiceProvider` 的 `boot()` 中通过 `Event::listen()` 注册。Laravel 默认的监听器自动发现位置是应用的 `app/Listeners`，并不会自动扫描到包内的 `src/Listeners`。

作为示例，我们来创建一个将配送完成事件记录到日志的包。事件只是一个持有通知所需值的类。

```php theme={null}
<?php

namespace Acme\Courier\Events;

final class ShipmentDelivered
{
    public function __construct(public readonly string $shipmentId)
    {
    }
}
```

监听器在构造函数中接收 Logger，并在 `handle()` 中处理事件。

```php theme={null}
<?php

namespace Acme\Courier\Listeners;

use Acme\Courier\Events\ShipmentDelivered;
use Psr\Log\LoggerInterface;

final class LogDeliveredShipment
{
    public function __construct(private readonly LoggerInterface $logger)
    {
    }

    public function handle(ShipmentDelivered $event): void
    {
        $this->logger->info('Shipment delivered', [
            'shipment_id' => $event->shipmentId,
        ]);
    }
}
```

在 Provider 中注册事件和监听器的类名。

```php theme={null}
<?php

namespace Acme\Courier;

use Acme\Courier\Events\ShipmentDelivered;
use Acme\Courier\Listeners\LogDeliveredShipment;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Event::listen(
            ShipmentDelivered::class,
            LogDeliveredShipment::class,
        );
    }
}
```

此结构的前提是 Provider 本身会被加载。请准备 Composer 的 `extra.laravel.providers`，或由用户进行显式注册。如果在 HTTP 处理和队列 Worker 中都需要该事件，就不要用 `runningInConsole()` 限制注册。

<Warning>
  请不要仅仅为了注册监听器而让该 Provider 实现 `DeferrableProvider`。如果延迟对象的服务没有被解析，监听器也不会被注册。重量级服务的绑定可根据需要拆分到另一个[延迟 Provider](/zh-CN/advanced/deferred-provider)中。
</Warning>

## 注册与实例创建发生在不同时机

`Dispatcher::listen()` 会将针对普通事件名的监听器添加到内部数组中。像上面那样传入类名时，此时并不会创建监听器的实例。

对于同步监听器，在事件触发后，`createClassCallable()` 会使用容器的 `make()` 解析类。因此可以利用构造函数的依赖注入。

```mermaid theme={null}
flowchart TD
    A["Provider 的 boot()"] --> B["通过 Event::listen() 保存类名"]
    B --> C["触发 ShipmentDelivered"]
    C --> D["Dispatcher 选择监听器"]
    D --> E["由容器解析同步监听器"]
    E --> F["将事件传给 handle()"]
```

这条路径并不会对 `handle()` 执行容器的 `call()`。应设计为**将任意服务注入到构造函数中，`handle()` 只接收事件**。请不要将其视为与 [Artisan 命令向 `handle()` 的依赖注入](/zh-CN/advanced/package-commands)相同的机制。

这里说明的是同步监听器。实现 `ShouldQueue` 时会走队列专用的另一条路径，因此还需要就事务、重试和依赖解析确认[事件的队列处理](/zh-CN/events)和[队列](/zh-CN/queues)。

## event:cache 不会阻止显式注册

`event:cache` 并不是将事件 Dispatcher 中已注册的所有监听器整体序列化的命令。

`EventCacheCommand::getEvents()` 会列举 `Illuminate\Foundation\Support\Providers\EventServiceProvider` 及其派生 Provider，并保存各 Provider 的自动发现结果和 `listens()` 的映射。写在普通 `ServiceProvider::boot()` 中的 `Event::listen()` 不在该收集范围内。

另一方面，即使在存在缓存的启动中，普通 Provider 的 `boot()` 也会被执行。**不包含在缓存中，与存在缓存时不会被注册，是两回事。**

| 注册方式 | `event:cache` 保存的内容 | 存在缓存时的启动 |
| - | - | - |
| 在普通 Provider 的 `boot()` 中使用 `Event::listen()` | 该注册本身不在收集范围内 | 在 `boot()` 中再次注册 |
| Foundation `EventServiceProvider` 的 `$listen` | 按 Provider 类名保存映射 | `getEvents()` 读取缓存 |
| 成为自动发现对象的监听器 | 将发现结果保存到映射中 | 使用缓存而非重新扫描目录 |

也可以在继承 Foundation `EventServiceProvider` 的包中使用 `$listen`。但是，在所确认的实现中，`shouldDiscoverEvents()` 的默认值是针对该基类本身的判断。**仅仅继承，并不会让该包的监听器目录被自动发现。**

此外，该 Provider 的 `$subscribe` 和 `$observers` 是在注册 `getEvents()` 结果的循环之外另行处理的。请不要只看 `$listen` 的缓存，就认为这些注册也会被省略。

<Tip>
  `Event::subscribe(SomeSubscriber::class)` 会在注册时通过容器创建订阅者并调用 `subscribe()`。它与按类名进行的 `Event::listen()` 不具有相同的延迟性。请避免在订阅者的构造函数或 `subscribe()` 中执行外部通信等副作用。
</Tip>

## 避免与自动发现的重复注册

如果用户通过 `withEvents(discover: [...])` 添加了包的监听器目录，而包的 Provider 也显式注册了同一个监听器，那么一个事件可能会导致处理执行多次。

`Dispatcher::listen()` 对普通事件执行的是向数组追加，在整个 Dispatcher 范围内不会自动对相同类名去重。Foundation `EventServiceProvider` 在自身映射内执行的 `array_unique()`，也不是用来合并其他 Provider 显式注册的机制。

请将注册的责任确定在一方。

* **由包注册默认监听器**时，不要求用户为该目录添加自动发现。
* **由用户选择所需监听器**时，包提供事件和监听器类，默认不注册相同的监听器。
* 从自动发现迁移到显式注册时，也要将用户端的发现配置和旧的事件缓存作为更新对象加以说明。

业务处理的幂等性也很重要，但不要在保留重复注册的情况下仅靠幂等性来掩盖问题，而应整理注册路径。

## 验证更新前后的行为

要检查注册状态，请在引入了包的验证用应用中执行 `event:list`。确认在无缓存和有缓存两种情况下目标监听器都存在。

```bash theme={null}
php artisan event:clear
php artisan event:list
php artisan event:cache
php artisan event:list
```

这是在验证用应用中的步骤。仅凭 `event:list` 的显示，无法保证监听器能完成处理或只执行一次。还要添加使用实际事件触发的测试。

作为示例，在 [Testbench 的基础 TestCase](/zh-CN/advanced/package-testing) 中注册 `CourierServiceProvider` 后，验证 Logger 只被调用一次。

```php theme={null}
<?php

namespace Acme\Courier\Tests\Feature;

use Acme\Courier\Events\ShipmentDelivered;
use Acme\Courier\Tests\TestCase;
use Illuminate\Support\Facades\Event;
use Psr\Log\LoggerInterface;

class PackageEventsTest extends TestCase
{
    public function test_delivery_is_logged_once(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $logger->expects($this->once())
            ->method('info')
            ->with('Shipment delivered', ['shipment_id' => 'shipment-123']);

        $this->app->instance(LoggerInterface::class, $logger);

        Event::dispatch(new ShipmentDelivered('shipment-123'));
    }
}
```

`Event::fake()` 便于确认事件是否被触发，但通常会阻止真实监听器的执行。本测试要验证注册和处理，因此不使用它。与自动发现的组合以及存在缓存时的启动，也要在另外的验证用应用或新的应用实例中确认。

<Warning>
  即使在启动后执行 `event:cache`，该进程内已注册的 Dispatcher 也不会根据缓存重新构建。请在新的启动中确认有缓存时的行为。要将更新后的监听器反映到长时间运行的 Worker 中，还需要按照常规部署流程进行重启。
</Warning>

## 长期维护检查清单

* 将事件类名、公共属性和监听器的注册方针作为面向用户的约定写入文档。
* 更改用户所订阅事件的名称或负载时，按照[版本兼容性管理](/zh-CN/advanced/package-versioning)说明变更。
* 对于涉及自动发现或 `$listen` 变更的更新，将重新创建 `event:cache` 纳入部署流程。
* 区分包的默认监听器与用户自己的监听器，不让同一个默认监听器被重复注册。
* 针对所支持的每个 PHP、Laravel 版本，持续进行监听器只执行一次的测试。

## 参考的一手资料

* [Laravel 官方文档：事件注册、自动发现、缓存、订阅者](https://github.com/laravel/docs/blob/73e61270287e4aaa1cecd77cd66aa6ef4f499828/events.md)
* [Dispatcher: listen、subscribe、createClassCallable](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Events/Dispatcher.php)
* [Foundation EventServiceProvider: register、getEvents、shouldDiscoverEvents](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php)
* [EventCacheCommand: 收集缓存对象](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Foundation/Console/EventCacheCommand.php)


## Related topics

- [进阶主题](/zh-CN/advanced/index.md)
- [包的路由注册与缓存](/zh-CN/advanced/package-routes.md)
- [事件与监听器](/zh-CN/events.md)
- [请求生命周期](/zh-CN/lifecycle.md)
- [延迟 Service Provider](/zh-CN/advanced/deferred-provider.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.