Skip to main content
想要为包提供事件驱动的扩展点时,需要将事件类的提供与监听器的注册分开设计。如果采用即使用户不把 vendor 内的目录加入自动发现也能正常工作的结构,就可以减少安装、更新和创建缓存时的前提条件。 本页会确认 Laravel Framework v13.36.0 的实现,将事件的基本操作应用到包的长期维护中。Provider 的加载请参阅包自动发现,测试环境请参阅包的测试。

显式注册包的监听器

包默认执行的处理,可以在普通 Illuminate\Support\ServiceProvider 的 boot() 中通过 Event::listen() 注册。Laravel 默认的监听器自动发现位置是应用的 app/Listeners,并不会自动扫描到包内的 src/Listeners。 作为示例,我们来创建一个将配送完成事件记录到日志的包。事件只是一个持有通知所需值的类。
监听器在构造函数中接收 Logger,并在 handle() 中处理事件。
在 Provider 中注册事件和监听器的类名。
此结构的前提是 Provider 本身会被加载。请准备 Composer 的 extra.laravel.providers,或由用户进行显式注册。如果在 HTTP 处理和队列 Worker 中都需要该事件,就不要用 runningInConsole() 限制注册。
请不要仅仅为了注册监听器而让该 Provider 实现 DeferrableProvider。如果延迟对象的服务没有被解析,监听器也不会被注册。重量级服务的绑定可根据需要拆分到另一个延迟 Provider中。

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

Dispatcher::listen() 会将针对普通事件名的监听器添加到内部数组中。像上面那样传入类名时,此时并不会创建监听器的实例。 对于同步监听器,在事件触发后,createClassCallable() 会使用容器的 make() 解析类。因此可以利用构造函数的依赖注入。 这条路径并不会对 handle() 执行容器的 call()。应设计为将任意服务注入到构造函数中,handle() 只接收事件。请不要将其视为与 Artisan 命令向 handle() 的依赖注入相同的机制。 这里说明的是同步监听器。实现 ShouldQueue 时会走队列专用的另一条路径,因此还需要就事务、重试和依赖解析确认事件的队列处理和队列。

event:cache 不会阻止显式注册

event:cache 并不是将事件 Dispatcher 中已注册的所有监听器整体序列化的命令。 EventCacheCommand::getEvents() 会列举 Illuminate\Foundation\Support\Providers\EventServiceProvider 及其派生 Provider,并保存各 Provider 的自动发现结果和 listens() 的映射。写在普通 ServiceProvider::boot() 中的 Event::listen() 不在该收集范围内。 另一方面,即使在存在缓存的启动中,普通 Provider 的 boot() 也会被执行。不包含在缓存中,与存在缓存时不会被注册,是两回事。 也可以在继承 Foundation EventServiceProvider 的包中使用 $listen。但是,在所确认的实现中,shouldDiscoverEvents() 的默认值是针对该基类本身的判断。仅仅继承,并不会让该包的监听器目录被自动发现。 此外,该 Provider 的 $subscribe 和 $observers 是在注册 getEvents() 结果的循环之外另行处理的。请不要只看 $listen 的缓存,就认为这些注册也会被省略。
Event::subscribe(SomeSubscriber::class) 会在注册时通过容器创建订阅者并调用 subscribe()。它与按类名进行的 Event::listen() 不具有相同的延迟性。请避免在订阅者的构造函数或 subscribe() 中执行外部通信等副作用。

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

如果用户通过 withEvents(discover: [...]) 添加了包的监听器目录,而包的 Provider 也显式注册了同一个监听器,那么一个事件可能会导致处理执行多次。 Dispatcher::listen() 对普通事件执行的是向数组追加,在整个 Dispatcher 范围内不会自动对相同类名去重。Foundation EventServiceProvider 在自身映射内执行的 array_unique(),也不是用来合并其他 Provider 显式注册的机制。 请将注册的责任确定在一方。
  • 由包注册默认监听器时,不要求用户为该目录添加自动发现。
  • 由用户选择所需监听器时,包提供事件和监听器类,默认不注册相同的监听器。
  • 从自动发现迁移到显式注册时,也要将用户端的发现配置和旧的事件缓存作为更新对象加以说明。
业务处理的幂等性也很重要,但不要在保留重复注册的情况下仅靠幂等性来掩盖问题,而应整理注册路径。

验证更新前后的行为

要检查注册状态,请在引入了包的验证用应用中执行 event:list。确认在无缓存和有缓存两种情况下目标监听器都存在。
这是在验证用应用中的步骤。仅凭 event:list 的显示,无法保证监听器能完成处理或只执行一次。还要添加使用实际事件触发的测试。 作为示例,在 Testbench 的基础 TestCase 中注册 CourierServiceProvider 后,验证 Logger 只被调用一次。
Event::fake() 便于确认事件是否被触发,但通常会阻止真实监听器的执行。本测试要验证注册和处理,因此不使用它。与自动发现的组合以及存在缓存时的启动,也要在另外的验证用应用或新的应用实例中确认。
即使在启动后执行 event:cache,该进程内已注册的 Dispatcher 也不会根据缓存重新构建。请在新的启动中确认有缓存时的行为。要将更新后的监听器反映到长时间运行的 Worker 中,还需要按照常规部署流程进行重启。

长期维护检查清单

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

参考的一手资料

最后修改于 2026年10月13日