钩子机制设计:用 150 行代码实现插件化系统

CLARA轻量论坛系统
CLARA轻量论坛系统 星耀SVIP管理员 黑卡会员
发布于 2026-09-16 09:23 ·3 浏览 ·0 回复

结论:一个能真正跑起来的插件系统,核心只需要三样东西——一张注册表、一个触发点、一个优先级排序,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 行的钩子类解决的是「解耦」,剩下的全是工程纪律。真正决定插件生态能不能活起来的,是有没有一份稳定的钩子清单、有没有统一的公共设施、以及插件崩了会不会把整站带走。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-397.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~