Меню

InnoCMS Hook 系统详解:Action、Filter 与 Blade 注入

ipanel-cli 2026-06-19 37
InnoCMS Hook 系统详解:Action、Filter 与 Blade 注入
Hook 是 InnoCMS 二次开发的核心抽象。本文拆解三种 hook 的本质区别 —— Action 触发副作用、Filter 转换数据、Blade 注入视图 —— 并用一个给文章页加分享按钮的实战案例,把从注册到渲染的完整链路讲透。

任何想长期演进的 CMS 都必须解决一个问题:怎么让二次开发不破坏内核升级。WordPress 用 plugin + hook 解决了,Drupal 用 module + hook 解决了,InnoCMS 的答案也是 hook —— 但实现得更现代、更 Laravel-native。

本文把 InnoCMS 的 hook 系统拆成三种基本类型,每种都给最小可复现的代码片段,最后用一个实战案例把全流程串起来。

三种 Hook 的本质区别

很多新人会把这三种 hook 混着用,结果就是行为飘忽、调试困难。其实它们的语义完全不同:

Action 与 Filter 两种 hook 的概念对比
  • Actionfire_hook_action / listen_hook_action)—— 触发副作用。比如发邮件、写日志、清缓存。返回值不重要,调用方不接收。
  • Filterfire_hook_filter / listen_hook_filter)—— 转换数据。把一个值传进去,链上每个 listener 都有机会改它,最后一个 listener 的返回值就是最终结果。
  • Bladelisten_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 提供两种模式:

Blade hook 注入点概念图

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.jsonBoot::init() 的全链路讲透。