`TypeError` 与 `ArgumentCountError` 在 WordPress 钩子回调里的"错位指认":我如何用参数签名快照锁定真正的"肇事者"
上周被个报错折腾到怀疑人生:前台白屏,日志里躺着 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 是果,参数契约是根
TypeError 和 ArgumentCountError 在钩子场景下经常是同根生的"指认错位"。看到堆栈顶是自己的回调,先别急着改内部逻辑,往上翻两层看看 WP_Hook::apply_filters 的调用帧,确认传入参数的数量和类型。我的"签名快照"脚本现在常驻在调试环境里,遇到异常先跑快照再定位,省了不少冤枉时间。
大家有没有遇到过更离谱的"参数漂移"场景?比如 array_walk 改引用导致钩子参数地址变化的?欢迎丢案例。