2.3 页面嵌入与钩子编程
用两个类把自己的代码插进系统脚本的任意执行点。
钩子是什么
前两节的插件都在做自己的页面。这一节反过来:不新建页面,而是在系统跑到某个位置时,把你的代码叫起来。
系统在模板里预留的调用点写作 {hook/钩子名},比如 {hook/global_usernav_extra2}。当页面渲染到这个位置,Discuz! 会去查找有没有插件声明了对应的方法,有就调用。整个机制靠类名和方法名的约定实现,不靠注册表——这也是为什么类名一个字符都不能错。
类命名规则
四个部分拼出类名,每一段都有明确含义:
| 部分 | 含义 |
|---|---|
plugin_ |
普通版脚本类前缀 |
mobileplugin_ |
手机版脚本类前缀 |
identifier |
插件唯一标识符 |
CURSCRIPT |
嵌入点所在脚本名(forum.php → forum;X3.4 起 plugin.php → plugin) |
CURSCRIPT 这一段是分水岭:写上它,方法只在该脚本里生效;不写,方法全局生效。举例说明,插件标识符是 hello:
| 想覆盖的范围 | 类名 |
|---|---|
| 全站所有脚本 | plugin_hello |
只在 forum.php |
plugin_hello_forum |
只在 plugin.php |
plugin_hello_plugin |
只在手机版 forum.php |
mobileplugin_hello_forum |
两个类:全局的与脚本的
骨架分两层。全局嵌入点类必须存在,否则这个插件的任何钩子都不会被调用:
<?php
// source/plugin/hello/hello.class.php
class plugin_hello {
// 全局钩子:这个脚本里没有专属类时用的方法
function global_usernav_extra2() {}
// 所有模块执行前
function common() {}
// 帖子内容解析时(discuzcode() 执行时)
function discuzcode($value) {}
// 主题删除前后,会被调用 2 次
function deletethread($value) {}
}
class plugin_hello_forum extends plugin_hello {
// forum.php 的模块执行前
function forum() {}
// 模块执行完毕、模板输出前
function forum_output($value) {}
// showmessage() 执行时
function forum_message($value) {}
}
两个关键点:第二个类必须 extends 第一个类,否则继承不到全局方法;forum_output 这类方法名由「CURSCRIPT 名 + _output」构成,写在子类里。
逐个方法讲语义
调用时机决定了你该往哪个方法里塞代码:
| 方法 | 调用时机 | 参数 | 典型用途 |
|---|---|---|---|
common() |
所有模块执行前(全局类) | 无 | 统一初始化、注册全局变量 |
global_HookId() |
模块执行完、模板输出前(全局类) | 无 | 输出前的最后处理 |
HookId() |
所有模块执行前(脚本类) | 无 | 页面级前置逻辑 |
HookId_output($value) |
模块执行完、模板输出前(脚本类) | 见下 | 改模板、改变量 |
HookId_message($value) |
showmessage() 执行时 |
param |
改写提示文案 |
discuzcode($value) |
帖子内容解析时 | param、caller |
改写帖子内容 |
deletethread($value) |
主题删除前后,调用 2 次 | param、step |
删除前拦截或删除后统计 |
HookId_output 收到的 $value 是一个数组,有三个键:
function forum_output($value) {
// template:要输出的模板,改它可以整个换掉输出目标
// message:showmessage 的内容
// values:当前可用的模板变量,可以往里塞新变量
$value['values']['myextra'] = 'hello';
return $value;
}
必须 return $value。只改不返回等于没改,这是初学者最常见的失败原因。
手机版要单独写一遍
手机版模板走 mobileplugin_ 前缀,类名和普通版并列存在于同一个 .class.php 文件里:
<?php
class mobileplugin_hello_forum extends plugin_hello_forum {
function forum() {}
function forum_output($value) {
return $value;
}
}
写成继承脚本类是省事的选择,继承全局类也行。手机版的模板在 touch 目录下,钩子位置由模板决定——如果手机版模板里没放 {hook/xxx},你的方法再对也不会被调。
钩子名的来源
方法名可以拆成四段:类别_脚本名_作用_标识。
global_usernav_extra2 属于 global(全局),作用是用户导航栏额外项;forum_output 是 forum 脚本的输出钩子。前面两段决定在哪调用,后面两段决定干什么。
不确定某个钩子叫什么时,别猜。config/config_global.php 里把开发者模式打开:
$_config['plugindeveloper'] = 2;
更新缓存后打开任意页面,查看网页源码搜索 <hook>,系统会把所有钩子的实际位置标出来。这是唯一可靠的查法。
常见错误速查
| 现象 | 原因 | 处理 |
|---|---|---|
| 钩子完全没反应 | 缺全局类 plugin_标识 |
补上并让它存在 |
| 全局方法能跑,脚本方法不跑 | CURSCRIPT 拼错 | 核对脚本名,X3.4 起 plugin.php 写 plugin |
$value 改了没效果 |
忘了 return $value |
每个改参数的钩子都返回 |
| 手机版无效果 | 只写了 plugin_ 类 |
补 mobileplugin_ 类 |
| 换了模板后钩子消失 | 模板里没有 {hook/xxx} |
在模板对应位置加标签 |
小结
两个类撑起整个机制:plugin_标识 是必须存在的全局嵌入点类,plugin_标识_CURSCRIPT 是脚本嵌入点类,继承前者。方法名 = 钩子名,改 $value 必须返回。
钩子具体怎么用,看 2.4 的三个实战。
