Search by

fiberphp / event

fiberphp

📢 FiberPHP 事件分发器 —— 精确匹配与前缀通配监听,支持 emit/dispatch 双模式、监听器优先级,协程安全。

Package info

gitee.com/fiberphp/event.git

Issues

pkg:composer/fiberphp/event

Statistics

Installs: 6

Dependents: 8

Suggesters: 0

dev-master 2026-09-10 13:19 UTC

This package is auto-updated.

Last update: 2026-09-10 13:30:48 UTC


README

FiberPHP 框架的强类型事件总线。零字符串事件名,事件即类,监听器按类精确匹配并自动覆盖父类/接口继承链。提供 emit(异常安全)与 dispatch(异常上抛)双触发模式、priority 优先级、halt 首响即停、监听器级 ShouldQueue 异步事务 commit 后才派发、Subscriber 订阅者模式、#[ListenTo] 属性自动注册。

目录

安装

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\EventFiberPHP\Event\Facade\Event 调用。

方法签名说明
onon(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 接口检测)
listenlisten(class-string<EventInterface> $eventClass, class-string $listenerClass, string $method = 'handle', int $priority = 0): int注册类形式监听器(约定入口)。监听器类经容器构造支持依赖注入,ShouldQueue / UniqueListener 按实例自动识别
offoff(class-string<EventInterface> $eventClass, int $id): int移除指定监听器,返回移除数
subscribesubscribe(SubscriberInterface $subscriber): void注册 Subscriber,一次注册多事件监听器
emitemit(EventInterface $event, bool $halt = false): mixed派发事件(异常捕获记日志)。halt=true 返回首个非 null 响应;否则返回全部响应数组
dispatchdispatch(EventInterface $event, bool $halt = false): mixed派发事件(异常直接上抛)。返回值同 emit
hasListenerhasListener(class-string<EventInterface> $eventClass): bool指定事件类是否有监听器
listlist(?string $eventClass = null): array返回已注册监听器;可按事件类过滤(含继承匹配)
deferdefer(EventInterface $event): void延迟派发(显式暂存,不受事务影响)
flushDeferredflushDeferred(?string $eventClass = null): voidflush 所有或指定类的 defer 暂存事件
clearDeferredclearDeferred(): void清空 defer 暂存(不派发)
deferredCountdeferredCount(): int当前 defer 暂存数量
beginTransactionbeginTransaction(): void标记进入事务(由 database Connection 自动调用)
commitcommit(): void标记事务提交并 flush 暂存事件(由 database Connection 自动调用)
rollbackrollback(): void标记事务回滚并丢弃暂存事件(由 database Connection 自动调用)
flushPendingDispatchflushPendingDispatch(): void手动 flush 暂存事件(队列 Consumer 内部事务后可用)
dispatchQueuedListenersdispatchQueuedListeners(EventInterface $event): void仅供 queue Consumer 调用——只执行 ShouldQueue 监听器,不嵌套推队列
startTracingstartTracing(): void开启派发追踪(debug 用,默认关闭零开销)
stopTracingstopTracing(): array关闭追踪并返回采集到的 traces
getTracesgetTraces(): array获取当前 traces(不关闭 tracing)
clearTracesclearTraces(): void清空 traces
isTracingisTracing(): booltracing 开关状态

监听器执行顺序:priority 降序 → 注册 ID 升序。

类继承匹配

事件名由类名唯一确定。派发时,调度器会按以下顺序查找监听器:

  1. 事件类自身(精确匹配)
  2. class_parents 返回的所有父类
  3. 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/ListenerEventProvider 自动扫描 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)
    }
}

属性参数:

参数类型默认说明
eventclass-string<EventInterface>必填事件类名
priorityint0优先级(越大越先执行)
asyncboolfalse预留:标记监听器需要异步(监听器类需同时实现 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 服务)

License

MIT