InnoCMS Hook 系统详解:Action、Filter 与 Blade 注入
任何想长期演进的 CMS 都必须解决一个问题:怎么让二次开发不破坏内核升级。WordPress 用 plugin + hook 解决了,Drupal 用 module + hook 解决了,InnoCMS 的答案也是 hook —— 但实现得更现代、更 Laravel-native。
本文把 InnoCMS 的 hook 系统拆成三种基本类型,每种都给最小可复现的代码片段,最后用一个实战案例把全流程串起来。
三种 Hook 的本质区别
很多新人会把这三种 hook 混着用,结果就是行为飘忽、调试困难。其实它们的语义完全不同:
- Action(
fire_hook_action/listen_hook_action)—— 触发副作用。比如发邮件、写日志、清缓存。返回值不重要,调用方不接收。 - Filter(
fire_hook_filter/listen_hook_filter)—— 转换数据。把一个值传进去,链上每个 listener 都有机会改它,最后一个 listener 的返回值就是最终结果。 - Blade(
listen_blade_insert/listen_blade_update)—— 往视图里插内容。前者在某一点插入 HTML,后者包裹某段 HTML。
判断标准很简单:要"做一件事"就用 Action,要"改一个值"就用 Filter,要"显示一段 HTML"就用 Blade。
Action:副作用触发器
典型场景是文章发布后触发推送通知、生成缓存、同步到搜索索引:
// 在插件的 Boot.php 里
listen_hook_action('article.created', function ($article) {
// 推送到企业微信
Http::post('https://qyapi.weixin.qq.com/...', [
'content' => "新文章:{$article->title}",
]);
});
内核在某篇文章创建后调用 fire_hook_action('article.created', $article),所有监听这个钩子的 closure 都会被执行。注意:listener 的返回值会被丢弃。如果你希望"修改文章对象",不要用 Action,要用 Filter。
Filter:数据转换链
Filter 是 InnoCMS 里最强大的扩展点。它把数据沿一条链传递,每个 listener 都能改:
// 给文章列表的每篇文章追加一个 external_url 字段
listen_hook_filter('article.list.item', function ($article) {
$article->external_url = "https://example.com/article-{$article->slug}";
return $article;
});
内核渲染列表时:$article = fire_hook_filter('article.list.item', $article)。链上多个 listener 按注册顺序依次执行,前一个的返回值是后一个的输入。
避坑:listener 必须有返回值,忘了 return 会把整个数据流截断成 null。这一点和 Laravel 自带的 Event 不同,Event 不需要返回。
Blade 注入:往视图里插 HTML
当插件需要在页面某处显示自己的内容时,不要改主题文件,用 Blade hook。InnoCMS 提供两种模式:
Insert 模式 —— 在主题模板的某个标记处插入 HTML:
// 主题模板里
@hookinsert('layouts.footer.top')
// 插件里
listen_blade_insert('layouts.footer.top', function () {
return view('YourPlugin::footer_banner');
});
Update 模式 —— 包裹主题里某段内容:
// 主题模板里
@hookupdate('article.body')
{!! $article->content !!}
@endhookupdate
// 插件里给内容加一层 wrapper
listen_blade_update('article.body', function ($html) {
return "<div class='content-wrapper'>{$html}</div>";
});
实战:给文章详情页加分享按钮
把三种 hook 串起来做一个真实需求 —— 文章页底部显示一组分享按钮,并且每次访问文章时统计一次分享点击数。
步骤 1:在主题的 articles/show.blade.php 里预留 hook 点:
<div class="aurora-article-detail__body">
{!! $article->content !!}
</div>
@hookinsert('article.share')
步骤 2:插件 Boot.php 里注册:
namespace Plugin\ArticleShare;
class Boot
{
public function init(): void
{
// 渲染分享按钮
listen_blade_insert('article.share', function () {
return view('ArticleShare::buttons');
});
// 过滤文章数据,附加 share_url
listen_hook_filter('article.show.data', function ($data) {
$data['share_url'] = urlencode($data['article']->url);
return $data;
});
// 监听分享点击(前端 POST 上来后触发)
listen_hook_action('article.share.clicked', function ($articleId) {
ShareLog::create(['article_id' => $articleId]);
});
}
}
三种 hook 各司其职:Blade 负责显示按钮,Filter 负责准备按钮需要的数据,Action 负责记录点击副作用。这种职责分离让单个插件的逻辑清晰,也让多个插件能并存而不互相打架。
常见的 hook 点
InnoCMS 内核已经预留了大量 hook 点,覆盖绝大部分二次开发场景:
component.sidebar.plugin.routes—— 过滤器,往管理后台侧栏加菜单项layouts.header.bottom—— Blade 插入点,加 header 扩展layouts.footer.top—— Blade 插入点,加 footer 扩展page.content.top/page.content.bottom—— Blade 插入点,包裹正文
找不到合适的 hook 点时,可以在主题模板里自己加 @hookinsert('YourPlugin.yourPoint'),再在插件里监听 —— 这就是"自己开闸门"的模式。
写在最后
Hook 系统是 InnoCMS 区别于"只能改源码"的低端 CMS 的核心。掌握三种 hook 的区别、知道内核在哪里开了闸门、能在自己写的代码里给后人留 hook 点 —— 一个二次开发的闭环就形成了。下一篇会拆 Plugin Manager 的加载流程,把插件从 config.json 到 Boot::init() 的全链路讲透。