插件安装后"成功"提示亮绿灯,但数据库表却纹丝不动:我追踪 `register_activation_hook` 被"静默跳过"的完整取证链
上周给内部 CRM 插件加自动安装逻辑,本地测了十遍都稳,丢到客户测试环境却翻车了——后台弹了"插件已启用"的绿框,可 `wp_myplugin_contacts` 表压根没建。更诡异的是,error_log 干净得像没跑过激活钩子。
我第一反应是检查钩子注册位置。WordPress 官方文档说得很死:register_activation_hook(__FILE__, 'myplugin_activate') 必须放在主插件文件顶层,不能包在 if (is_admin()) 或者类初始化里。但代码明明在主文件第一屏,排除。
第二刀砍向文件路径。客户环境用了符号链接做版本管理,__FILE__ 指向的是 /var/www/releases/20240903/myplugin.php,而 WordPress 插件列表里识别的是 /var/www/current/wp-content/plugins/myplugin/myplugin.php。这两个路径在 plugin_basename 眼里根本不是同一个东西,钩子自然挂不到正确的插件标识上。
验证方法:在激活函数里强行写死 error_log(plugin_basename(__FILE__)),对比后台插件页 URL 里的 plugin=myplugin/myplugin.php。如果 basename 对不上,钩子就是挂给了一个"幽灵插件"。
修复方案不是硬编码路径,而是用 plugin_basename(__FILE__) 做统一归一化,或者干脆在 mu-plugin 里做引导加载。但这里有个更隐蔽的坑:如果你用了 Composer 的 vendor/autoload.php 提前加载类,而激活逻辑封装在类静态方法里,__FILE__ 如果写在类文件里而不是主入口文件,路径偏差会更难察觉。
表没建只是第一层。我补了路径修复后,发现多站点环境下子站激活时主站表建了、子站没建——register_activation_hook 只触发在当前请求的站点上下文,不会自动遍历 wp_blogs。如果插件需要每个站点独立表,得在 wpmu_new_blog 里补注册,或者手动切 switch_to_blog 循环建表。
最后贴个我现在用的"防御式激活"骨架,把路径校验、多站点切换、异常回滚包在一起:
function myplugin_activate($network_wide) {
$expected = plugin_basename(MYPLUGIN_MAIN_FILE); // 主文件里 define 的常量
$actual = plugin_basename(__FILE__);
if ($expected !== $actual) {
deactivate_plugins($actual);
wp_die('路径不匹配,钩子未正确注册。请联系运维检查符号链接配置。');
}
if ($network_wide && is_multisite()) {
foreach (get_sites(['fields' => 'ids']) as $blog_id) {
switch_to_blog($blog_id);
myplugin_run_schema();
restore_current_blog();
}
} else {
myplugin_run_schema();
}
}
这个 case 给我的教训是:WordPress 的"成功"提示只代表 activate_plugin() 函数没抛异常,不代表你的钩子真的跑了。下次遇到激活逻辑"疑似失效",先打三个点:钩子注册路径、__FILE__ 解析结果、当前站点上下文。比盯着数据库权限查半天管用得多。

