首页 / 开发文档 / 插件钩子与扩展点

插件钩子与扩展点

action 与 filter 的区别、内核实际会发哪些事件、标签与后台页怎么挂进去。

两类扩展:action 与 filter

类型语义用法
action通知一件事发生了,回调不返回值Hook::on('content.saved', fn)
filter把一个值交给你改,回调必须返回新值Hook::filterOn('某个标签', fn)
// 订阅事件
Hook::on('plugin.activated', function ($name) {
    if ($name !== 'my_plugin') { return; }   // 事件是广播的,自己判一下是谁
});

// 改一个值(filter 必须 return,忘了 return 就等于把值清空了)
Hook::filterOn('content.query.opts', function ($opts, $ctx) {
    $opts['num'] = 20;
    return $opts;
});

内核当前会发的事件

只有下面这些,都是实例事实而不是规划:

事件参数什么时候
plugin.activated插件名插件启用成功后
plugin.deactivated插件名插件停用后
plugin.uninstalled插件名插件卸载后
theme.installed主题名主题装好后
theme.switched主题名切换到某主题后
theme.removed主题名主题删除后
admin.route.<路由>路由参数数组访问插件声明的后台页时
内核目前只发 action,还没有 filter 埋点。filter 现在主要给插件之间互相用: A 插件 Hook::emit('my_plugin.price', $v),B 插件用 Hook::filter('my_plugin.price', $v) 去改。 自己发事件时请加插件名前缀,避免和别人的事件名撞车。

扩展点一:模板标签

// 单标签:{naicha:myplugin_now}          → 返回值**自动 HTML 转义**
Template::registerTag('myplugin_now', function (Template $t, array $a) {
    return date('Y-m-d H:i');
});

// 确实要输出 HTML 时,在模板里显式写 html="1":{naicha:myplugin_html html="1"}
// 与变量标签 {detail.content html="1"} 是同一套约定。


// 块标签:{naicha:myplugin_box}…{/naicha:myplugin_box}
// 回调**自己负责输出**,没有返回值这一说
Template::registerTag('myplugin_box', function (Template $t, array $a, callable $body) {
    echo '<div class="box">';
    $body();                 // 调用一次 = 把中间那段内容输出一次
    echo '</div>';
}, true);

标签名只允许小写字母、数字、下划线,字母开头,2–30 位。 模板里写了没人实现的标签不会报错,只是输出空 —— 不会因为插件没启用就把整页搞崩。

扩展点二:后台菜单与页面

// plugin.json
"admin": {
  "menu": [
    { "route": "myplugin", "label": "我的插件", "perm": "plugin.myplugin.manage" }
  ]
}
  • 声明之后,侧栏会自动出现这一项(按 perm 过滤:没这个权限的人看不见)。
  • 同一个 route 的请求会被交给 Hook::on('admin.route.myplugin', ...) 的回调。
  • 权限键要在插件里 Perm::register() 注册,否则它不会出现在角色权限矩阵里,也就没人能勾。

扩展点三:数据表

// install.php:点「启用」时跑一次,必须幂等
Db::exec("CREATE TABLE IF NOT EXISTS plug_myplugin_item (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT DEFAULT '',
    created_at TEXT DEFAULT ''
)");
本机数据库可能是 SQLite 3.7.17(2013 年版),三条限制请记牢:
· 没有 JSON1:json_extract / json_array_length 一律不可用;
· 没有 UPSERT:INSERT … ON CONFLICT 是语法错误(要 3.24+); 「有则更新」请写成先 DELETE 再 INSERT;
· 唯一性用 CREATE UNIQUE INDEX + INSERT OR IGNORE。

访问数据库的正确姿势

// 一律走 Db 层,占位符绑定,不要自己拼 SQL
$rows = Db::all('SELECT * FROM plug_myplugin_item WHERE id > :i', ['i' => 0]);
$one  = Db::one('SELECT * FROM contents WHERE id=:i', ['i' => 1]);
Db::insert('plug_myplugin_item', ['title' => '新记录']);
Db::update('plug_myplugin_item', ['title' => '改了'], 'id=:id', ['id' => 1]);
Db::exec('DELETE FROM plug_myplugin_item WHERE id=:id', ['id' => 1]);

日志

要留痕就用 Data::log('动作', '细节'),它会出现在后台「操作日志」里; 调试信息用 error_log(),不会污染页面。

没找到答案? 可以在 联系我们 留言说明使用场景,我们会补充进文档。