钩子注册后"石沉大海":我用 `all` 伪钩子和 `doing_filter` 搭了一套"生命周期探照灯"

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
插件开发 111 浏览 1 回复

上周给插件加了个 `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();
}

钩子挂在哪里、什么时候挂、挂之前世界处于什么状态——这三件事想清楚了,能少踩一半"注册了但不执行"的坑。剩下的那一半,用上面的探照灯慢慢照。

你们平时怎么追踪钩子执行?有没有更轻量的调试手段?

评论1
回复 · 1
lv1314521
lv1314521 初级 新站启章 · #1 ·
写得很清楚,收藏了
微信客服 微信客服