Zsens Admin 插件钩子"时序迷宫":我用 `all` 钩子做"时间切片"才抓到那个在 `muplugins_loaded` 和 `plugins_loaded` 之间偷跑的"内鬼"

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

上周被个邪门问题折磨了一下午:我的插件在 A 站正常,在 B 站某个回调死活不触发。`add_action` 执行了,`has_action` 返回真值,优先级没撞车,命名空间没冲突——但就是不进断点。

最后靠往 `wp-includes/plugin.php` 里临时塞 `error_log` 才破案:B 站跑了个 MU 插件,在 `muplugins_loaded` 里提前触发了某个我依赖的钩子,而我的插件还在 `plugins_loaded` 里排队。时序差了一个身位,钩子已经跑完一轮了,我的挂载才姗姗来迟。

这事后我搞了个"生命周期探针"的轻量版,专门用来抓这种"时序幽灵"。核心思路是用 `all` 钩子做全局监听,把每个钩子的触发时机、参数指纹、调用栈切片记下来。不是生产环境用的,是本地调试时快速定位"谁抢了先":

```php // 塞在 MU 插件或当前插件最前面,wp-config 里 define('ZSENS_PROBE', true); if (defined('ZSENS_PROBE') && ZSENS_PROBE) { $GLOBALS['_zsens_probe_log'] = []; $GLOBALS['_zsens_probe_start'] = microtime(true); add_action('all', function ($tag) { $now = microtime(true) - $GLOBALS['_zsens_probe_start']; $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 8); $caller = ''; // 往上爬,跳过 wp-includes/plugin.php 本身 foreach ($backtrace as $frame) { if (isset($frame['file']) && false === strpos($frame['file'], 'plugin.php')) { $caller = basename($frame['file']) . ':' . ($frame['line'] ?? '?'); break; } } $GLOBALS['_zsens_probe_log'][] = [ 'time_ms' => round($now * 1000, 2), 'hook' => $tag, 'caller' => $caller, 'priority' => $GLOBALS['wp_filter'][$tag]->current_priority ?? 'n/a', ]; }, PHP_INT_MIN); // 抢在最前面 } ```

然后在 `shutdown` 或者你怀疑的钩子末尾输出:

```php add_action('shutdown', function () { if (!defined('ZSENS_PROBE') || !ZSENS_PROBE) return; $target = $_GET['probe_target'] ?? 'init'; // 关注某个钩子 $entries = array_filter($GLOBALS['_zsens_probe_log'], function ($e) use ($target) { return $e['hook'] === $target; }); // 按时间排序,看同一钩子被触发了几次、分别是谁干的 usort($entries, fn($a, $b) => $a['time_ms'] $b['time_ms']); error_log("=== PROBE: {$target} triggered " . count($entries) . " time(s) ==="); foreach ($entries as $e) { error_log(sprintf( "[%.2f ms] priority=%s caller=%s", $e['time_ms'], $e['priority'], $e['caller'] )); } }, PHP_INT_MAX); ```

这玩意帮我抓到过几个典型场景:

1. "早产"钩子

MU 插件在 `muplugins_loaded` 里直接 `do_action('init')`——别笑,真见过。我的探针输出里 `init` 在 0.5ms 就亮了,而正常应该在 50ms 之后。这种时候你的 `plugins_loaded` 挂载再快也追不上。

2. "多胞胎"钩子

同一个钩子被触发了两次,但来自不同文件。一次是核心正常流程,一次是某个插件里手贱的 `do_action('init')`。你的回调被执行了两次,第二次可能参数已经被前面的钩子改得面目全非。

3. 优先级"伪碰撞"

你以为优先级 10 和 11 是安全的?探针显示两个回调都在 10 上,因为其中一个用了字符串 `'10'` 被 PHP 弱类型转成 10,另一个也是 10,执行顺序变成注册先后——而注册顺序又受 `plugins_loaded` 时序影响。

现在我的调试流程变成:遇到"挂不上"先不急着查 `has_action`,而是 `?probe_target=那个钩子` 跑一遍,看时间轴上它到底出现了几次、每次谁发起的。比 Xdebug 轻量,比 `var_dump` 靠谱。

有个细节:`all` 钩子本身也会被 `do_action('all')` 触发,所以上面代码里我没在探针内部再记 `all`,不然递归爆炸。另外 `PHP_INT_MIN` 和 `PHP_INT_MAX` 是故意用的极端值,确保探针不被其他优先级干扰。

你们平时怎么抓这种"时序幽灵"的?我试过 `QM_Collectors` 但太重,自己写又怕漏。这版探针算是个折中,有更好思路的求拍。

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