2.4 钩子实战:帖子解析、删除拦截与消息改写

三个能直接抄走的例子,以及怎么查到自己要的钩子。

三个案例都放在 source/plugin/hello/hello.class.php 里,前提是 2.3 讲的全局嵌入点类已经存在。

案例一:帖子内容解析时插入内容

discuzcode() 是渲染帖子正文的函数——你看到的正文里,UBB 标签、表情、图片最终都经过它。要在解析时动手脚,就实现同名方法。

设计意图:不改数据库,只在输出时加工。关掉插件内容立刻恢复原样,出问题没有脏数据。

<?php
class plugin_hello {
    // 帖子内容解析时调用
    function discuzcode($value) {
        global $_G, $discuz;

        // $_G['discuzcodemessage'] 是当前待解析的字串
        $message = $_G['discuzcodemessage'];

        // $value 含 param 与 caller,据此区分调用场景
        if ($value['caller'] !== 'post') {
            return $value;
        }

        // 只对含指定标记的帖子生效,避免污染全站
        if (strpos($message, '[hello]') === false) {
            return $value;
        }

        $message = str_replace(
            '[hello]',
            '&lt;div class="hello-box"&gt;你好,这是一条插件插入的内容。&lt;/div&gt;',
            $message
        );

        // 改完写回,否则解析器拿到的还是原串
        $_G['discuzcodemessage'] = $message;

        return $value;
    }
}

关键三点:用 $_G['discuzcodemessage'] 取待解析串、用 $value['caller'] 区分场景、改完必须写回同一个键。这个方法返回 $value 而非字符串,改的是全局里的字串。

案例二:删除前后做拦截与统计

deletethread() 和 deletepost() 是删除类钩子,有个特别之处:一次操作会被调用 2 次,$value['step'] 区分阶段。

step 时机 用途
check 删除前 校验权限、记录待删清单
delete 删除后 写日志、清理关联数据

待处理的 ID 数组放在 $_G['deletethreadtids'](主题)和 $_G['deletepostids'](帖子)里。

<?php
class plugin_hello {
    // 主题删除前后各调一次
    function deletethread($value) {
        global $_G;

        $tids = isset($_G['deletethreadtids']) ? $_G['deletethreadtids'] : array();
        if (!$tids) {
            return $value;
        }

        if ($value['step'] == 'check') {
            // 删除前:记录清单
            foreach ($tids as $tid) {
                C::t('myplugin_log')->insert(array(
                    'type' => 'delete_thread',
                    'targetid' => $tid,
                    'operator' => $_G['uid'],
                    'dateline' => TIMESTAMP,
                ));
            }
        } else {
            // 删除后:清理自己的关联数据
            DB::delete('myplugin_thread_setting', DB::field('tid', array($tids)));
        }

        return $value;
    }
}

三个要点:先判 step 再动手,否则同样逻辑会跑两遍;TID 数组用 DB::field() 转成 SQL 片段,别自己拼引号;关联表清理放 delete 阶段,跟系统操作保持一致。deletepost() 写法一样,只是数组换成 $_G['deletepostids']。

真要做「拦截」而不只是记录,在 check 阶段判断权限并调 showmessage() 中断。但这是主动拦截,会在删除流程中间抛提示,逻辑要写得保守。

案例三:改写提示与输出

HookId_message 拦 showmessage(),HookId_output 在模板输出前动手。前者改文案,后者改变量。

<?php
class plugin_hello_forum extends plugin_hello {
    // showmessage() 执行时
    function forum_message($value) {
        $message = $value['param']['message'];

        // 把系统提示替换成自己的文案
        $message = str_replace('操作成功', '已按站点规则处理完成', $message);

        $value['param']['message'] = $message;

        // 改完必须返回
        return $value;
    }

    // 模块执行完、模板输出前
    function forum_output($value) {
        // values 是可用模板变量,往里塞新变量
        $value['values']['hello_tip'] = '本页面由 hello 插件增强';
        return $value;
    }
}

模板侧直接用就行:

{template common/header}
&lt;p&gt;{$hello_tip}&lt;/p&gt;
{template common/footer}

forum_message 适合做全站提示文案的品牌化——把「操作成功」换成你自己的措辞。$value['param']['message'] 改完一定要 return,否则提示还是原文。forum_output 更常用:values 里加的变量,模板可以像用系统变量一样用。

怎么查全部预定义钩子

不要背钩子名,也不要靠猜。官方给的路径是在 config/config_global.php 文件末尾加:

$_config['plugindeveloper'] = 2;

改完更新缓存,打开任意页面,查看网页源码搜索 &lt;hook&gt;,所有钩子的实际位置都会标出来。plugindeveloper 为 1 时只显示开发者入口,为 2 时才输出钩子标记。查完记得改回 1 或 0,2 会让页面源码变脏。

钩子命名规律

名字一般拆成四段:类别_脚本名_作用_标识。前两段决定在哪调用,_output 结尾表示输出前,_message 结尾表示 showmessage() 时。规律好记,但具体某个钩子到底存不存在,仍以源码搜索为准。

常用钩子速查

方法 时机 参数
common() 所有模块执行前 无
HookId() 所有模块执行前(脚本类) 无
HookId_output($value) 模块执行完、模板输出前 template、message、values
global_HookId() 同上(全局类) 无
HookId_message($value) showmessage() 执行时 param
discuzcode($value) 帖子内容解析时 param、caller
deletethread($value) 主题删除前后,2 次 param、step
deletepost($value) 帖子删除前后,2 次 param、step
ad_adId($value) 对应广告位 params、content
avatar($value) 头像调用时 —

ad_adId 里的 adId 是广告位脚本 ID,对应函数名如 ad_headerbanner()。头像钩子 avatar() 是 X2.5 新增的。

小结

三个案例对应三种改法:discuzcode() 在渲染时加工内容(不改数据),deletethread() 在删除前后分段处理(step 判阶段),HookId_message() 改文案、HookId_output() 改变量。

三条通用纪律:改完必须 return $value、分阶段的钩子先判 step、钩子名不确定就开 plugindeveloper = 2 查源码。

第三篇开始讲插件的工程化:XML 清单、安装脚本与数据表。