fiberphp / event
📢 FiberPHP 事件分发器 —— 精确匹配与前缀通配监听,支持 emit/dispatch 双模式、监听器优先级,协程安全。
Requires
- php: >=8.3
- fiberphp/container: dev-master
- fiberphp/contract: dev-master
- fiberphp/discovery: dev-master
- psr/log: ^3.0
Requires (Dev)
- phpunit/phpunit: ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-10 13:30:48 UTC
README
FiberPHP 框架的强类型事件总线。零字符串事件名,事件即类,监听器按类精确匹配并自动覆盖父类/接口继承链。提供 emit(异常安全)与 dispatch(异常上抛)双触发模式、priority 优先级、halt 首响即停、监听器级 ShouldQueue 异步、事务 commit 后才派发、Subscriber 订阅者模式、#[ListenTo] 属性自动注册。
目录
- 安装
- 快速入门
- 核心接口
- Event 静态门面
- 类继承匹配
- 监听器级异步(ShouldQueue)
- 事务提交后派发(afterCommit)
- Deferred 延迟派发
- 幂等监听器(UniqueListener)
- Subscriber 订阅者模式
- 属性自动注册(#[ListenTo])
- GenericEvent 通用容器事件
- 派发追踪(TraceableDispatcher)
- 配置驱动注册
- 目录结构
安装
composer require fiberphp/event
安装后 EventProvider 随包发现自动注册。Worker boot 阶段注入日志器,并按约定扫描应用 app/Listener 目录注册监听器(#[ListenTo] 方法与 SubscriberInterface 订阅者),生产环境可编译缓存跳过反射,见生产环境缓存。
快速入门
1. 创建事件类
继承 AbstractEvent(推荐)或实现 EventInterface:
<?php
declare(strict_types=1);
namespace App\Event;
use FiberPHP\Event\AbstractEvent;
final class UserRegisteredEvent extends AbstractEvent
{
public function userId(): int
{
return $this->payload['user_id'];
}
public function email(): string
{
return $this->payload['email'];
}
}
2. 注册监听器
use App\Event\UserRegisteredEvent;
use FiberPHP\Event\Event;
// 基本注册 —— priority 越大越先执行(默认 0)
$id = Event::on(UserRegisteredEvent::class, function (array $payload, UserRegisteredEvent $event) {
echo "用户注册: {$event->userId()}\n";
}, priority: 10);
// 显式指定异步(强制 ShouldQueue,跳过接口自动检测)
// 同一个事件的不同监听器可以有不同策略:有的同步有的异步
$id = Event::on(UserRegisteredEvent::class, [EmailListener::class, 'handle'], queue: 'email', delay: 60);
// 移除监听器
Event::off(UserRegisteredEvent::class, $id);
3. 派发事件
use App\Event\UserRegisteredEvent;
use FiberPHP\Event\Event;
$event = new UserRegisteredEvent([
'user_id' => 123,
'email' => 'foo@example.com',
]);
// emit:捕获异常、不中断其他监听器,返回全部响应数组
$responses = Event::emit($event);
// halt=true:首个非 null 响应即停
$result = Event::emit($event, halt: true);
// dispatch:异常直接上抛,中断后续监听器
try {
Event::dispatch($event);
} catch (\Throwable $e) {
// 处理异常
}
4. 返回 false 中断传播
监听器显式返回 false 等价于调用 $event->stopPropagation():
Event::on(UserRegisteredEvent::class, function (array $payload, UserRegisteredEvent $event) {
$event->stopPropagation(); // 方式一:对象方法
});
Event::on(UserRegisteredEvent::class, fn () => false); // 方式二:返回 false
核心接口
EventInterface
所有事件对象必须实现此接口:
interface EventInterface
{
/** 事件载荷(监听器接收的数据数组) */
public function payload(): array;
/** 是否已停止传播 */
public function isStopped(): bool;
/** 停止传播,后续监听器不再执行 */
public function stopPropagation(): void;
}
AbstractEvent(推荐继承)
内置基类,提供 payload() / isStopped() / stopPropagation() 的 final 实现:
abstract class AbstractEvent implements EventInterface
{
protected array $payload = [];
protected bool $stopped = false;
public function __construct(array $payload = [])
{
$this->payload = $payload;
}
final public function payload(): array;
final public function isStopped(): bool;
final public function stopPropagation(): void;
}
ShouldQueue
监听器类实现此接口后,emit() 会自动将该监听器分离为异步任务推送到队列执行。事件本身保持纯载荷,不承担异步配置——这意味着同一事件的不同监听器可以有不同的同步策略:
interface ShouldQueue
{
/** 目标队列名 */
public function getQueue(): string;
/** 延迟秒数(0 = 立即) */
public function getDelay(): int;
/** queue 连接名(对应 config/queue.php 的 connections.*) */
public function getConnection(): string;
}
SubscriberInterface
一次注册多事件监听器的订阅者契约,详见 Subscriber 章节。
Event 静态门面
所有方法通过 FiberPHP\Event\Event 或 FiberPHP\Event\Facade\Event 调用。
| 方法 | 签名 | 说明 |
|---|---|---|
on | on(class-string<EventInterface> $eventClass, callable $listener, int $priority = 0, ?string $queue = null, int $delay = 0, string $connection = 'default'): int | 注册监听器(闭包/ callable)。[ListenerClass::class, 'handle'] 形式会经容器解析为实例;$queue 非 null 时强制异步(跳过 ShouldQueue 接口检测) |
listen | listen(class-string<EventInterface> $eventClass, class-string $listenerClass, string $method = 'handle', int $priority = 0): int | 注册类形式监听器(约定入口)。监听器类经容器构造支持依赖注入,ShouldQueue / UniqueListener 按实例自动识别 |
off | off(class-string<EventInterface> $eventClass, int $id): int | 移除指定监听器,返回移除数 |
subscribe | subscribe(SubscriberInterface $subscriber): void | 注册 Subscriber,一次注册多事件监听器 |
emit | emit(EventInterface $event, bool $halt = false): mixed | 派发事件(异常捕获记日志)。halt=true 返回首个非 null 响应;否则返回全部响应数组 |
dispatch | dispatch(EventInterface $event, bool $halt = false): mixed | 派发事件(异常直接上抛)。返回值同 emit |
hasListener | hasListener(class-string<EventInterface> $eventClass): bool | 指定事件类是否有监听器 |
list | list(?string $eventClass = null): array | 返回已注册监听器;可按事件类过滤(含继承匹配) |
defer | defer(EventInterface $event): void | 延迟派发(显式暂存,不受事务影响) |
flushDeferred | flushDeferred(?string $eventClass = null): void | flush 所有或指定类的 defer 暂存事件 |
clearDeferred | clearDeferred(): void | 清空 defer 暂存(不派发) |
deferredCount | deferredCount(): int | 当前 defer 暂存数量 |
beginTransaction | beginTransaction(): void | 标记进入事务(由 database Connection 自动调用) |
commit | commit(): void | 标记事务提交并 flush 暂存事件(由 database Connection 自动调用) |
rollback | rollback(): void | 标记事务回滚并丢弃暂存事件(由 database Connection 自动调用) |
flushPendingDispatch | flushPendingDispatch(): void | 手动 flush 暂存事件(队列 Consumer 内部事务后可用) |
dispatchQueuedListeners | dispatchQueuedListeners(EventInterface $event): void | 仅供 queue Consumer 调用——只执行 ShouldQueue 监听器,不嵌套推队列 |
startTracing | startTracing(): void | 开启派发追踪(debug 用,默认关闭零开销) |
stopTracing | stopTracing(): array | 关闭追踪并返回采集到的 traces |
getTraces | getTraces(): array | 获取当前 traces(不关闭 tracing) |
clearTraces | clearTraces(): void | 清空 traces |
isTracing | isTracing(): bool | tracing 开关状态 |
监听器执行顺序:priority 降序 → 注册 ID 升序。
类继承匹配
事件名由类名唯一确定。派发时,调度器会按以下顺序查找监听器:
- 事件类自身(精确匹配)
class_parents返回的所有父类class_implements返回的所有接口
这意味着:监听父类或接口的监听器,会被子类事件自动触发。可以利用这一点实现分组监听。
// 定义接口作为分组标记
interface UserEventInterface extends EventInterface {}
// 具体事件类都实现它
final class UserLoginEvent extends AbstractEvent implements UserEventInterface {}
final class UserLogoutEvent extends AbstractEvent implements UserEventInterface {}
// 监听接口 —— 所有实现类事件都会触发
Event::on(UserEventInterface::class, function (array $payload, EventInterface $event) {
logger()->info('用户事件: ' . $event::class, $payload);
});
Event::emit(new UserLoginEvent(['uid' => 1])); // ✅ 触发
Event::emit(new UserLogoutEvent(['uid' => 1])); // ✅ 触发
父类监听器同理:
abstract class DomainEvent extends AbstractEvent {}
final class OrderPaidEvent extends DomainEvent { /* ... */ }
Event::on(DomainEvent::class, fn (array $p, EventInterface $e) => /* 记录领域事件日志 */);
Event::emit(new OrderPaidEvent([...])); // ✅ 触发
去重说明:若同一条监听器同时被父类和子类链路上注册了,调度器会按 ID 去重,保证同一次派发中每条监听器只执行一次。
监听器级异步(ShouldQueue)
事件本身保持纯载荷,监听器类实现 ShouldQueue 接口后,emit() 会自动将该监听器分离为异步任务推送到队列执行。未实现此接口的监听器保持同步执行。
// 同一个 UserRegisteredEvent:
// EmailListener 异步(慢操作,不阻塞注册流程)
// LogListener 同步(必须立刻记录,事务回滚也能看到日志)
class EmailListener implements ShouldQueue
{
public function getQueue(): string { return 'email'; }
public function getDelay(): int { return 60; }
public function getConnection(): string { return 'redis'; }
public function handle(array $payload, EventInterface $event): void
{
// 发邮件(60 秒延迟执行)
}
}
Event::on(UserRegisteredEvent::class, [EmailListener::class, 'handle']);
Event::on(UserRegisteredEvent::class, [LogListener::class, 'handle']);
// emit 时自动分离:EmailListener 走队列,LogListener 同步执行
Event::emit(new UserRegisteredEvent(['user_id' => 1]));
设计取舍:
- 事件端异步 = 同一事件全异步或全同步,简单但粒度粗
- 监听器端异步 = 同一事件不同监听器可以有不同策略,更灵活——这是 FiberPHP 选择的方案
- 闭包/函数形式的监听器无法实现接口,永远同步执行
队列端:fiberphp/queue 包的 EventDispatchMiddleware 自动识别 {event_class, payload} 格式消息,重建 Event 对象并调用 Event::dispatchQueuedListeners() 只执行异步监听器,无需手动配置。
事务提交后派发(afterCommit)
emit() / dispatch() 会自动感知数据库事务:
- 事务内:事件暂存,不派发
- 事务 commit:所有暂存事件统一 flush
- 事务 rollback:暂存事件直接丢弃,不派发
database 包的 Connection::startTrans() / commit() / rollback() 自动调用 Event::beginTransaction() / commit() / rollback(),无需手动干预。
use FiberPHP\ORM\Model;
use FiberPHP\Event\Event;
// 典型场景:Model::save() 在事务内触发事件
$order = Order::create(['total' => 100]);
// OrderModelEvent 在事务内被 emit → 暂存
// 事务 commit → 事件 flush → EmailListener 收到通知
// 事务 rollback → 事件丢弃 → EmailListener 收不到(不会通知一个不存在的订单)
嵌套事务:只有最外层 commit/rollback 才通知 Event。内层保存点回滚不影响暂存队列。
Deferred 延迟派发
批量操作场景下,先暂存事件等全部攒完再统一派发,避免逐条 emit 的循环开销:
// 导入 1000 条商品数据
foreach ($importData as $row) {
$product->save($row);
Event::defer(new AfterInsertEvent(['id' => $product->id]));
}
// 统一 flush(全部 emit 模式,异常吞掉继续)
Event::flushDeferred();
// 也可以只 flush 某类事件
Event::flushDeferred(AfterInsertEvent::class);
和事务暂存的区别:
| defer | 事务暂存(afterCommit) | |
|---|---|---|
| 触发方式 | Event::defer($event) 显式调用 | emit 自动感知 db()->transaction() |
| 事务内行为 | 直接暂存,不受事务影响 | 随事务 commit/rollback flush/drop |
| flush 时机 | Event::flushDeferred() | 自动在 commit 后 |
| 清除 | Event::clearDeferred() | 自动在 rollback 后 |
幂等监听器(UniqueListener)
监听器实现 UniqueListener 接口后,emit 派发时自动用 fiberphp/cache 做 SET NX 幂等检查——同一事件同一 payload 在 TTL 内只执行一次。
use FiberPHP\Event\Contract\UniqueListener;
use FiberPHP\Event\EventInterface;
class EmailListener implements UniqueListener
{
public function handle(array $payload, EventInterface $event): void
{
// 发送邮件...
}
public function uniqueId(array $payload, EventInterface $event): string
{
return 'email:' . $payload['user_id'] . ':' . $payload['template'];
}
public function uniqueTtl(): int
{
return 3600; // 1 小时内相同 ID 不重复执行
}
}
// listen() 自动识别 ShouldQueue + UniqueListener;
// 需要强制指定异步队列/延迟时改用 on(..., queue: 'email', delay: 60)
Event::listen(UserRegisteredEvent::class, EmailListener::class);
fiberphp/cache未安装时自动跳过幂等守卫,不阻断派发- 队列重试场景特别有用——Consumer 收到重复消息时自动跳过
Subscriber 订阅者模式
一个类集中注册多个事件监听器,适合领域内聚场景。订阅者放在 app/Listener 目录即被自动扫描;处理方法为实例方法,订阅者类经容器构造、支持依赖注入:
use FiberPHP\Event\Contract\SubscriberInterface;
final class UserSubscriber implements SubscriberInterface
{
public static function subscribe(string $dispatcher): void
{
// $dispatcher 即 Event 类名;listen() 把监听器类经容器解析为实例
$dispatcher::listen(UserRegisteredEvent::class, self::class, 'onRegistered', priority: 10);
$dispatcher::listen(UserLoginEvent::class, self::class, 'onLogin');
$dispatcher::listen(UserLogoutEvent::class, self::class, 'onLogout');
}
public function onRegistered(array $payload, EventInterface $event): void
{
// 发欢迎邮件(可通过构造函数注入依赖)
}
public function onLogin(array $payload, EventInterface $event): void
{
// 记录登录 IP
}
public function onLogout(array $payload, EventInterface $event): void
{
// 清理会话
}
}
框架应用中无需手动注册(app/Listener 目录自动扫描);纯库场景可手动 Event::subscribe(new UserSubscriber())。
与属性注册的区别:
| SubscriberInterface | #[ListenTo] 属性 | |
|---|---|---|
| 风格 | 代码驱动,类内聚,可加 if/switch 条件分支 | 声明式,方法上标记 |
| 优先级 | $dispatcher::listen($event, self::class, $method, 10) | #[ListenTo($event, priority: 10)] |
| 注册 | EventProvider 自动扫描 app/Listener | EventProvider 自动扫描 app/Listener |
属性自动注册(#[ListenTo])
监听器类放在应用 app/Listener 目录、处理方法上标 #[ListenTo] 属性,EventProvider boot 时由 AttributeScanner 扫描目录自动注册(监听器类经容器构造,支持构造器依赖注入):
use FiberPHP\Event\Attribute\ListenTo;
use FiberPHP\Event\Contract\EventInterface;
final class OrderListener
{
#[ListenTo(OrderPaidEvent::class, priority: 100)]
public function handlePaid(array $payload, EventInterface $event): void
{
// 更新库存
}
#[ListenTo(OrderPaidEvent::class)]
public function sendReceipt(array $payload, EventInterface $event): void
{
// 发收据(默认优先级 0)
}
// 一个方法监听多个事件
#[ListenTo(OrderPaidEvent::class, priority: 50)]
#[ListenTo(OrderShippedEvent::class, priority: 50)]
public function notifyAdmin(array $payload, EventInterface $event): void
{
// 通知管理员(重复属性:IS_REPEATABLE)
}
}
属性参数:
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
event | class-string<EventInterface> | 必填 | 事件类名 |
priority | int | 0 | 优先级(越大越先执行) |
async | bool | false | 预留:标记监听器需要异步(监听器类需同时实现 ShouldQueue) |
属性注册需要
fiberphp/discovery包扫描。在 framework 应用中由 boot 阶段自动处理;纯库包可手动扫描调用Event::on()。
GenericEvent 通用容器事件
当没有必要为某个场景定义专属 Event 类时,使用 GenericEvent 提供可读写的 payload:
use FiberPHP\Event\GenericEvent;
use FiberPHP\Event\Event;
$event = new GenericEvent('kernel.response', [
'response' => new Response('original'),
]);
Event::dispatch($event);
// 监听器可以修改 payload
Event::on(GenericEvent::class, function (array $payload, GenericEvent $event) {
$event['response'] = new Response('modified'); // ArrayAccess 读写
$event->offsetSet('status', 200);
});
GenericEvent 同时实现 EventInterface 和 \ArrayAccess,监听器之间可以通过 payload 传递数据。
派发追踪(TraceableDispatcher)
debug 时开启 tracing,完整还原每次事件派发链路:执行了哪些监听器、各自耗时、有没有报错、有没有被 stop/async 跳过。
Event::startTracing();
// 正常派发
Event::emit(new UserLoginEvent(['uid' => 1]));
Event::emit(new OrderPaidEvent(['order_id' => 123]));
$traces = Event::stopTracing();
// 每条 trace 结构:
// [
// 'event_class' => UserLoginEvent::class,
// 'halt' => false,
// 'throw_exception' => false,
// 'total_duration_ms' => 0.86,
// 'listener_count' => 4, // 注册的监听器总数
// 'async_count' => 1, // 异步分离数量
// 'listeners' => [
// ['id'=>1, 'priority'=>20, 'listener'=>'Closure', 'should_queue'=>false, 'duration_ms'=>0.001, 'result'=>'ok'],
// ['id'=>2, 'priority'=>10, 'listener'=>'EmailListener::handle', 'should_queue'=>false, 'duration_ms'=>0.85, 'result'=>'error', 'error'=>'db down'],
// ['id'=>3, 'priority'=>5, 'listener'=>'LogListener::handle', 'should_queue'=>false, 'duration_ms'=>0, 'result'=>'stopped'],
// ['id'=>4, 'priority'=>0, 'listener'=>'SmsListener::handle', 'should_queue'=>true, 'duration_ms'=>0, 'result'=>'queued'],
// ],
// ]
监听器 result 枚举:ok(正常执行)| stopped(返回 false → stopPropagation)| halted(halt=true 模式首个非 null 响应)| error(emit 模式吞异常)| queued(ShouldQueue 异步分离)。
生产环境开销:默认关闭,开启与否在 dispatchInternal 里只有一个 static::$tracing ? 三元判断——关闭时 tracing 相关变量初始化和循环内 trace 写入全部跳过。
目录结构
packages/event/
├── README.md
├── composer.json
├── config/
│ └── event.php # 默认配置(随包自动合并,仅 options.enable 开关)
└── src/
├── AbstractEvent.php # 事件基类(payload/stopPropagation 默认实现)
├── Event.php # 核心调度器(静态 API + 事务钩子,listen() 类监听器入口)
├── EventProvider.php # 服务提供者(boot 扫描 app/Listener,挂载 optimize 缓存事件)
├── GenericEvent.php # 通用可读写容器事件
├── Install.php # 包安装声明(Provider 注册 + 配置模板发布)
├── Attribute/
│ ├── ListenTo.php # 监听器自动注册属性
│ └── AttributeScanner.php # app/Listener 扫描器(#[ListenTo]/Subscriber,含编译缓存)
├── Contract/
│ ├── EventInterface.php # 事件对象契约
│ ├── ShouldQueue.php # 异步监听器契约
│ └── SubscriberInterface.php # 订阅者契约
├── Event/
│ ├── OptimizeCacheEvent.php # 内置优化缓存事件
│ └── OptimizeClearEvent.php # 内置优化清理事件
└── Facade/
└── Event.php # Facade 代理(容器 event 服务)