钩子注册后"石沉大海":我用 `all` 伪钩子和 `doing_filter` 搭了一套"生命周期探照灯"
上周给插件加了个 `save_post` 回调,逻辑写得挺顺,但测试时死活不触发。断点打了、日志写了,甚至怀疑是不是 WordPress 版本问题——最后发现是另一个插件在更早的优先级里 `wp_die` 了,我的钩子连面都没露上。
这种"钩子注册了但世界不知道"的坑踩多了,我攒了一套调试组合拳,专门用来在生命周期里"打手电"。
第一招:`all` 钩子当"总线监听器"
WordPress 有个隐藏彩蛋钩子叫 `all`,字面意思:所有钩子触发时都会走它。用法很贼:
add_action( 'all', function( $tag ) {
static $depth = 0;
$depth++;
error_log( str_repeat( ' ', $depth - 1 ) . "→ {$tag}" );
} );
但别直接往生产环境扔,会炸日志。我通常包个条件:只在特定请求、或者 `WP_DEBUG_LOG` 开启且带个秘密参数时启用。比如 `?hook_debug=1` 才亮灯。
第二招:`doing_filter` / `doing_action` 做"当前位置 GPS"
有时候不是钩子没挂,是挂的时机错了——你想在 `plugins_loaded` 之前注册 `wp_enqueue_scripts`,那必然哑火。排查时我会在可疑位置插这段:
if ( doing_action( 'init' ) ) {
error_log( '当前正在 init,优先级=' . current_priority() );
}
`current_priority()` 是 5.3+ 才有的,老版本得用 `has_filter` 反查或者自己记数。关键是确认:你的钩子注册时,目标钩子是不是已经跑完了?
第三招:给回调套个"透明包装器"
直接改业务代码塞日志太脏,我写了个小工具函数:
function hook_spy( callable $callback, string $label = '' ) {
return function( ...$args ) use ( $callback, $label ) {
$tag = current_filter();
error_log( "[SPY:{$label}] {$tag} 触发,参数数=" . count( $args ) );
$result = $callback( ...$args );
error_log( "[SPY:{$label}] {$tag} 返回,类型=" . gettype( $result ) );
return $result;
};
}
// 用法:把原来的回调包一层
add_action( 'save_post', hook_spy( 'my_real_callback', '订单同步' ), 10, 3 );
这样不用动原函数,就能看到进没进、返没返、参数对不对。特别适合排查那种"进了钩子但条件分支提前 return"的隐形 bug。
第四招:可视化"钩子家谱"
有时候问题不在单个钩子,在整条链的优先级打架。我借了个思路,在 `shutdown` 时 dump 出所有已执行的钩子序列:
add_action( 'shutdown', function() {
global $wp_filter;
$executed = []; // 实际需要用 all 钩子收集,这里简写思路
// 或者用 debug-bar 的 Hooks & Actions 面板,但那个是"已注册",不是"已执行"
// 我的做法是:all 监听器把执行过的 tag 按顺序写进静态数组
} );
真正跑起来后,你会发现很多"以为会执行"的钩子其实被跳过了——比如 `admin_init` 在某些前端 Ajax 请求里根本不存在,或者 REST 请求走的是 `rest_api_init` 另一条分支。
一个实战案例:`wp_insert_post` vs `save_post` 的"鬼打墙"
我插件里需要在文章发布时推送到外部 API。一开始挂 `save_post`,结果更新一次文章推了三次——因为自动保存、修订版本都会触发。
用探照灯一看,`save_post` 的调用栈里 `DOING_AUTOSAVE` 和 `wp_is_post_revision` 根本没过滤干净。后来改成这样:
add_action( 'wp_insert_post', function( $post_id, $post, $update ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
if ( wp_is_post_revision( $post_id ) ) return;
if ( $post->post_status !== 'publish' ) return;
if ( ! $update ) return; // 新建还是更新,看业务需求
// 真正的推送逻辑...
}, 10, 3 );
但 `wp_insert_post` 本身又在 `save_post` 之后触发,优先级 10 时数据已经落库,比 `save_post` 更适合做"后置确认"。
最后:别在 `__construct` 里直接 `add_action`
这条是血泪教训。我以前习惯类一实例化就挂钩子,结果在某些场景(比如单元测试里 new 了一下)副作用满天飞。现在统一改成显式的 `register()` 方法,由入口文件控制时机:
$sync = new Order_Sync();
// 不是这里:$sync 一 new 就挂了一堆钩子
// 而是这里:我明确知道当前上下文需要它
if ( is_admin() ) {
$sync->register_admin_hooks();
} else {
$sync->register_frontend_hooks();
}
钩子挂在哪里、什么时候挂、挂之前世界处于什么状态——这三件事想清楚了,能少踩一半"注册了但不执行"的坑。剩下的那一半,用上面的探照灯慢慢照。
你们平时怎么追踪钩子执行?有没有更轻量的调试手段?


