插件钩子与扩展点
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:
· 没有 UPSERT:
· 唯一性用
· 没有 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(),不会污染页面。