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;

更新缓存后打开任意页面,查看网页源码搜索 &lt;hook&gt;,系统会把所有钩子的实际位置标出来。这是唯一可靠的查法。

常见错误速查

现象 原因 处理
钩子完全没反应 缺全局类 plugin_标识 补上并让它存在
全局方法能跑,脚本方法不跑 CURSCRIPT 拼错 核对脚本名,X3.4 起 plugin.php 写 plugin
$value 改了没效果 忘了 return $value 每个改参数的钩子都返回
手机版无效果 只写了 plugin_ 类 补 mobileplugin_ 类
换了模板后钩子消失 模板里没有 {hook/xxx} 在模板对应位置加标签

小结

两个类撑起整个机制:plugin_标识 是必须存在的全局嵌入点类,plugin_标识_CURSCRIPT 是脚本嵌入点类,继承前者。方法名 = 钩子名,改 $value 必须返回。

钩子具体怎么用,看 2.4 的三个实战。