事件系统

事件系统基于发布-订阅(Publish-Subscribe)模式,让模块之间以松耦合方式通信:监听方注册回调,触发方只管广播,双方互不依赖。框架内部的生命周期通知(应用初始化、路由加载、服务启动等)全部经由此系统分发,业务代码也可以定义自己的事件。

本文依据框架源码 src/Core/Event.phpsrc/Core/FrameworkEvent.phpsrc/Core/Server/ServerEventHook.php 编写。

核心 API

事件管理器为 Viswoole\Core\Event,通过门面访问:

php
use Viswoole\Core\Facade\Event;
方法签名说明
onon(string|UnitEnum $event, callable|string $handle, int $limit = 0): string|array注册监听器;闭包返回监听器 ID,类名注册返回方法名 ID 数组
emitemit(string|UnitEnum $event, array $arguments = []): void触发事件,按注册顺序调用所有监听器
offoff(string|UnitEnum $event, ?string $id = null): void移除监听器;$id 为 null 时移除该事件的全部监听器
offAlloffAll(): void清空所有监听器
getEventsgetEvents(): array获取所有已注册监听的事件名列表

注册监听器 on

php
use Viswoole\Core\Facade\Event;

// 闭包监听:返回监听器 ID,可用于精确移除
$id = Event::on('user_login', function (array $user) {
    Log::info("用户 {$user['id']} 已登录");
});

// 限制触发次数:仅响应前 3 次,之后自动移除(0 表示不限制)
Event::on('cache_miss', function (string $key) {
    Log::warning("缓存未命中: {$key}");
}, 3);

Event::emit('user_login', [['id' => 1]]);  // 参数以数组形式依次传入监听器
Event::off('user_login', $id);             // 移除单个监听器
Event::off('user_login');                  // 移除该事件的全部监听器

监听器由容器 invoke() 执行,因此监听器方法同样支持依赖注入参数。

事件名规则

  • 大小写不敏感:事件名注册、触发、移除时统一转为小写,Event::on('UserLogin', ...)Event::emit('userlogin', ...) 指向同一监听组;
  • on() 的事件名不能包含 .:包含时抛 InvalidArgumentException. 被保留给定向触发语法,见下文);
  • 支持枚举事件名:任意枚举均可作为事件名——字符串枚举使用枚举值,数值枚举与纯枚举使用枚举名;但枚举类不能作为监听器注册。

框架内置事件名统一由 FrameworkEvent 枚举定义(见下文生命周期事件)。

类名监听器与 event.id 定向触发

on() 传入类名时,框架反射扫描该类的全部公共方法(跳过抽象方法、构造与析构方法),以「方法名」为监听器 ID 批量注册:

php
class UserEvents
{
    public static function login(array $user): void { /* ... */ }

    public function register(array $user): void { /* ... */ }
}

// 注册整个类:返回 ['login', 'register']
Event::on('user', UserEvents::class);

// 定向触发:'user.login' 只调用 'user' 事件下 ID 为 'login' 的监听器
Event::emit('user.login', [['id' => 1]]);    // → UserEvents::login()
Event::emit('user.register', [['id' => 1]]); // → UserEvents::register()

// 移除 'user' 事件下的 'login' 监听器
Event::off('user', 'login');

定向触发语法 emit('事件名.监听器ID')

  • 只触发该事件下指定 ID 的监听器,其余监听器不受影响;
  • 监听器 ID 是注册时生成的:类名监听器为小写方法名,闭包监听器为自动生成的哈希;
  • 对闭包监听器同样适用:Event::emit('user_login.某个id') 可定向触发某一支。

次数限制

on() 的第三个参数 $limit 限制监听器最大触发次数:达到上限后框架自动将其移除;传 0 表示不限制;负数在注册时即抛异常。适合「只关心前 N 次」的场景(如启动期的预热告警)。

框架生命周期事件

框架内置事件聚合在枚举 Viswoole\Core\FrameworkEvent 中,作为事件名的单一事实来源(支持 IDE 补全,避免硬编码字符串)。监听时可直接传枚举:

php
use Viswoole\Core\Facade\Event;
use Viswoole\Core\FrameworkEvent;

Event::on(FrameworkEvent::ServerStarted, function (\Viswoole\Core\Server $server) {
    // 服务启动完成
});
// 字符串形式等价(事件名归一化为小写):Event::on('serverstarted', ...)

全部内置事件(以源码枚举为准):

事件枚举事件名触发时机回调参数
AppInitializedappinitializedApp 构造完成、依赖绑定与服务注册执行完毕后
AppDestroyingappdestroyingApp 对象析构时,供服务清理资源
RouterInitializingrouterinitializing路由初始化开始前、加载配置路由与注解路由之前
RouterInitializedrouterinitialized配置路由、注解路由加载并解析完成后
ServerCreatingservercreating加载服务配置之前,此时 Swoole Server 尚未创建Server $server
ServerCreatedservercreatedSwoole Server 创建完成并绑定到容器之后、服务启动之前Server $server
ServerStartingserverstarting调用 start() 之后、Swoole 事件循环启动之前Server $server
ServerStartedserverstartedSwoole onStart 回调中,主进程事件循环已启动Server $server
ServerShuttingDownservershuttingdownSwoole onBeforeShutdown 回调中,供执行清理工作Server $server

监听注册时机

AppInitialized 在 App 构造结束时触发,此时配置已加载。想在框架启动的最早期介入,可使用 config/listens.php(下文)或 Swoole 事件挂钩。

监听注册方式

方式一:config/listens.php

config/listens.php 是事件监听注册入口。它作为配置文件随配置目录一起加载(PHP 配置文件会被 include 执行),在文件顶层直接调用 Event::on() 即可:

php
// config/listens.php
use Viswoole\Core\Facade\Event;

Event::on('AppInitialized', function () {
    echo_log('应用初始化完成');
}, 1); // 第三个参数限制仅触发一次

适合注册框架生命周期事件的监听。

方式二:服务提供者

在服务提供者的 boot() 中注册,适合随业务模块分发的事件(boot 阶段所有服务已注册完毕):

php
use Viswoole\Core\Service\Provider;

final class OrderEventProvider extends Provider
{
    public function register(): void {}

    public function boot(): void
    {
        $this->app->event->on('order', \App\Listener\OrderListener::class);
    }
}

Swoole 事件挂钩:ServerEventHook

框架事件与 Swoole 事件是两层系统。Swoole 服务端事件(workerStartworkerStoponTask 等)通过 Viswoole\Core\Server\ServerEventHook 注册,框架将其统一转发给 Swoole Server:

php
use Viswoole\Core\Server\ServerEventHook;

// 注册单个事件(同一事件可注册多个回调,按注册顺序执行)
ServerEventHook::addEvent('workerStart', function (\Swoole\Server $server, int $workerId) {
    // 每个 Worker 进程启动时执行一次
});

// 批量注册
ServerEventHook::addEvents([
    'workerStart' => function (\Swoole\Server $server, int $workerId) { /* ... */ },
    'workerStop'  => function (\Swoole\Server $server, int $workerId) { /* ... */ },
]);
方法签名说明
addEventaddEvent(string $event, callable $callback): void注册单个 Swoole 事件处理器,事件名不区分大小写
addEventsaddEvents(array $events): void批量注册,键为事件名、值为回调
getEventHooksgetEventHooks(): array获取全部事件回调映射,供 Server 注册时使用

addEvent 必须在服务启动前调用

ServerEventHook 的事件表在 Swoole Server 创建并绑定回调时被消费。所有挂钩注册应发生在 ServerStarting 事件之前(如 config/listens.php 或服务提供者中),启动后注册不会再生效。workerStart 是「每个 Worker 进程执行一次」逻辑(如连接池填充)的正确挂钩位置,主进程的 start 事件中创建的资源无法跨进程共享。

框架自身已在 ServerEventHook 中内置了 startshutdownbeforeShutdown 三个事件的处理(输出启动信息、监听 SIGINT 安全关闭、触发 ServerShuttingDown 事件),自定义回调会与之共存。

下一步