钩子机制设计:用 150 行代码实现插件化系统
结论:一个能真正跑起来的插件系统,核心只需要三样东西——一张注册表、一个触发点、一个优先级排序,150 行 PHP 足够写完;Clara BBS 的运行时钩子体系(现为 156 个钩子)就是按这个思路长的。
一、150 行到底装了什么:注册、挂载、触发
结论:钩子框架的本质是「名字 → 回调数组」的映射表,加一个排序动作和一次循环调用,多出来的行数都在边界处理上。
最小骨架就三个方法:
class Hook {
private static array $map = []; // 注册表:name => [priority => [fn, ...]]
public static function on(string $name, callable $fn, int $priority = 10): void {
self::$map[$name][$priority][] = $fn;
}
public static function fire(string $name, ...$args): void {
$list = self::$map[$name] ?? [];
ksort($list); // 优先级小的先跑
foreach ($list as $group) foreach ($group as $fn) $fn(...$args);
}
}
`on()` 负责把插件函数挂到某个钩子名上,`fire()` 负责在宿主代码的关键位置按顺序把它们喊起来。宿主只写一行 `Hook::fire('post.after_publish', $postId)`,插件那段逻辑从此和你解耦——这是插件化的全部意义。
二、动作钩子和过滤器钩子,别做成一个
结论:区分「只做副作用的动作钩子」和「必须回传值的过滤器钩子」,是避免插件互相踩踏的关键。
动作钩子(action)不需要返回值:发帖成功后发通知、写日志、推送搜索引擎,谁先谁后无所谓,只要都跑到。过滤器钩子(filter)必须把值一路传下去:
public static function apply(string $name, $value, ...$args) {
foreach (self::flatten($name) as $fn) {
$value = $fn($value, ...$args) ?? $value;
}
return $value;
}
宿主拿到 `apply()` 的返回值再继续,插件就能改标题、改分页大小、改渲染后的 HTML。注意 `?? $value` 这个兜底:插件作者忘了 return 是常事,兜底能防止整站内容变 null。
三、优先级必须可配,而且要稳定排序
结论:优先级是插件生态的「交通规则」,没有它会退化成按加载顺序碰运气。
同一个钩子上挂十个插件,谁先改内容、谁后处理,结果完全不同。默认值给 10、留出插队空间是通用做法。另外 PHP 的数组在相同优先级下是按插入顺序稳定的,这一点要写进文档,否则插件作者无法预期自己排在第几位。
四、为什么运行时加载能做到「保存即生效」
结论:因为钩子表在每次请求时由 PHP 现场拼装,没有编译产物、没有缓存层介入,所以改完文件下一个请求就生效。
Clara BBS 的插件放在 `content/plugins` 目录,采用运行时钩子加载:不需要 Composer、不需要命令行、不需要编译缓存,后台里改完保存即生效,也不用清缓存。代价是每个请求都要扫一遍插件目录并加载注册文件,所以目录清单建议走 `Cache::remember` 缓存,只在插件启停时失效。
插件除了钩子,还能直接调用系统公共设施:`Cache::remember` 做缓存、`Cron::register` 注册定时任务(懒触发、零配置)、`notify` 发通知、货币记账 API 走统一账本。这样插件不用自己造轮子,也不会绕开审计。
五、几个容易翻车的点
结论:钩子系统写起来容易,写稳了要防三件事——插件报错拖垮全站、钩子名散落无索引、递归触发。
- 异常隔离:单个插件抛异常不应该让帖子发不出去。触发时 try/catch,记录插件名与堆栈,把插件降级为失效而不是让页面 500。
- 钩子名要可检索:156 个钩子如果没有一张清单,插件作者根本不知道该挂哪个。宿主侧统一命名规范(如 `post.before_publish`)并导出列表,比任何文档都管用。
- 防递归:插件 A 在 `post.after_publish` 里又调了一次触发,就会无限循环。加一个执行中的名字栈,重复进入直接跳过。
回到开头:150 行的钩子类解决的是「解耦」,剩下的全是工程纪律。真正决定插件生态能不能活起来的,是有没有一份稳定的钩子清单、有没有统一的公共设施、以及插件崩了会不会把整站带走。
转载请注明出处,版权归原作者所有。
星耀SVIP
管理员
黑卡会员





