`TypeError` 与 `ArgumentCountError` 在 WordPress 钩子回调里的"错位指认":我如何用参数签名快照锁定真正的"肇事者"

插件开发 32 浏览 0 回复 返回上级

上周被个报错折腾到怀疑人生:前台白屏,日志里躺着 TypeError: Argument 1 passed to Zsens\Admin\Hooks\ContentFilter::appendMetaBadge() must be of the type string, null given。看起来是类型声明太严、传了 null,对吧?我顺着堆栈往回调里加 is_null 兜底,结果第二天同样位置炸出 ArgumentCountError: Too few arguments to function...

两个异常,同一个钩子,根源却不在我的回调里。记录一下这种"错位指认"的排查套路,省得大家跟我一样在回调内部打转。

一、WordPress 钩子的"参数截断"机制是盲区

核心问题:add_filter('the_content', [$this, 'appendMetaBadge'], 10, 2) 我注册了两个参数,但某个第三方插件或者主题也挂了这个钩子,且它的回调只返回一个值、没有透传第二个参数:

// 某个"好心"但手短的第三方回调
add_filter('the_content', function($content) {
    return $content . '<!-- injected -->';
}, 5); // 优先级 5,比我先执行

WordPress 的 apply_filters 不是"按注册参数数量分发",而是按调用时传入的参数数量走。如果前一个回调没把第二个参数带出来,后续回调拿到的就是 null——但 PHP 的类型声明会直接把 null 翻译成 TypeError,而不是更直观的"参数缺失"。

更坑的是,如果某个中间回调用 call_user_func_array 或者反射重新包装了参数,数量对不上时直接抛 ArgumentCountError,堆栈顶却指向你的回调,让你以为是自己的签名写错了。

二、我的快速定位:给钩子做"参数签名快照"

不再盲猜,写了个探针直接 dump 钩子挂载点的参数契约:

add_action('all', function($tag) {
    if ($tag !== 'the_content') return;
    
    global $wp_filter;
    $hooks = $wp_filter[$tag]->callbacks ?? [];
    
    $snapshot = [];
    foreach ($hooks as $priority => $callbacks) {
        foreach ($callbacks as $idx => $cb) {
            $ref = is_array($cb['function']) 
                ? new ReflectionMethod($cb['function'][0], $cb['function'][1])
                : new ReflectionFunction($cb['function']);
            
            $snapshot[$priority][] = [
                'accepted_args' => $cb['accepted_args'],
                'required_params' => $ref->getNumberOfRequiredParameters(),
                'total_params' => $ref->getNumberOfParameters(),
                'file' => $ref->getFileName(),
                'line' => $ref->getStartLine(),
            ];
        }
    }
    
    // 写入临时日志,或者 wp_die 直接看
    error_log('HOOK_SNAPSHOT['.$tag.']: '.json_encode($snapshot, JSON_PRETTY_PRINT));
});

跑一遍,立刻发现优先级 5 的匿名函数 accepted_args=1,而我的回调在 10 上等着收两个参数。中间没有"桥梁"把第二个参数 $post 透传下来。

三、两种修复策略,看场景选

策略 A:防御型回调(推荐公共插件用)

不假设上游一定按规矩传参,把类型声明降级为兼容模式:

public function appendMetaBadge($content, $post = null) {
    // 如果 $post 为 null,自己补查
    if (is_null($post)) {
        $post = get_post();
    }
    // 再做强类型校验,失败则短路返回
    if (!($post instanceof \WP_Post)) {
        return $content;
    }
    // ...
}

策略 B:拦截型修复(主题或可控环境用)

直接在那个"截断"的回调后面、自己之前,插一个透传桥:

add_filter('the_content', function($content, $post = null) {
    return $content; // 什么都不做,只负责把两个参数原样往后传
}, 6, 2); // 优先级 6,卡在"截断者"和我之间

但这个是权宜之计,如果截断者变优先级就失效。更稳的做法是用 remove_filter 把对方的钩子卸了重构,前提是你知道它是谁。

四、延伸到 Action 的"静默吞参"

Action 没有返回值,参数截断更隐蔽。do_action('save_post', $post_ID, $post, $update) 如果某个挂载的回调只声明了一个参数,PHP 不会报错——多余的参数直接被忽略。但如果你后续依赖第三个参数 $update 做"是否更新"的判断,拿到的是默认值 false,逻辑直接跑偏,连异常都不给你。

这种我归类为"静默吞参",比抛异常更难查。对策是在关键 action 的回调入口加 func_num_args() 断言:

public function onSavePost($postId, $post = null, $update = false) {
    if (func_num_args() < 3) {
        // 被吞参了,从全局或重新查询补全
        $update = !empty(get_post_meta($postId, '_edit_last', true));
    }
    // ...
}

五、总结:Exception 是果,参数契约是根

TypeErrorArgumentCountError 在钩子场景下经常是同根生的"指认错位"。看到堆栈顶是自己的回调,先别急着改内部逻辑,往上翻两层看看 WP_Hook::apply_filters 的调用帧,确认传入参数的数量和类型。我的"签名快照"脚本现在常驻在调试环境里,遇到异常先跑快照再定位,省了不少冤枉时间。

大家有没有遇到过更离谱的"参数漂移"场景?比如 array_walk 改引用导致钩子参数地址变化的?欢迎丢案例。

评论0
回复 · 0
还没有回复
微信客服 微信客服