`register_activation_hook` 里写 `.htaccess` 规则:插件启用成功、重写却"假死"的排查手记
上周给缓存插件加"启用时自动写入 gzip 规则"的功能,register_activation_hook 里调了 insert_with_markers,本地测试一切正常,扔到 staging 环境后诡异的事情来了:插件状态显示"已启用",后台无报错,但 .htaccess 纹丝不动,就像那行代码被吞了一样。
先贴最初的问题代码,精简后长这样:
register_activation_hook( __FILE__, function() {
$rules = "<IfModule mod_deflate.c>\nAddOutputFilterByType DEFLATE text/html\n</IfModule>";
insert_with_markers( get_home_path() . '.htaccess', 'MyPlugin', $rules );
} );
看起来人畜无害对吧?坑就坑在 get_home_path() 的返回值。本地是单站点、标准目录,它返回 /var/www/html/;但 staging 用了 Bedrock 结构,ABSPATH 指向 /web/wp/,而 .htaccess 实际躺在 /web/。于是 insert_with_markers 对着一个不存在的路径写文件,静默失败——不抛异常、不返 false,就是啥也没发生。
第一个补丁:别信 get_home_path(),自己找 .htaccess:
$htaccess = file_exists( ABSPATH . '.htaccess' )
? ABSPATH . '.htaccess'
: dirname( ABSPATH ) . '/.htaccess'; // Bedrock 兼容
结果 staging 还是不行。这次 .htaccess 路径对了,但文件权限是 644、属主是 deploy 用户,PHP-FPM 跑在 www-data 下,insert_with_markers 需要读-修改-写全链路,第一步 fopen 就跪了。更坑的是这个函数的错误处理:
// wp-admin/includes/misc.php 源码片段
if ( ! $f = fopen( $htaccess_file, 'r+' ) ) {
return false; // 没触发 error_log,没抛异常,调用方根本不知道
}
所以第二个教训:永远包装 insert_with_markers,自己做前置校验。我现在的启用钩子长这样:
register_activation_hook( __FILE__, function() {
$htaccess = my_plugin_locate_htaccess(); // 自定义路径解析
if ( ! $htaccess ) {
set_transient( 'my_plugin_htaccess_missing', 1, HOUR_IN_SECONDS );
return; // 不阻断启用,但记一笔
}
if ( ! is_writable( $htaccess ) ) {
set_transient( 'my_plugin_htaccess_unwritable', compact('htaccess'), HOUR_IN_SECONDS );
return;
}
$rules = my_plugin_generate_rules();
$result = insert_with_markers( $htaccess, 'MyPlugin', $rules );
if ( ! $result ) {
set_transient( 'my_plugin_htaccess_failed', file_get_contents( $htaccess ), HOUR_IN_SECONDS );
}
} );
然后在 admin_notices 里读这些 transient 给用户看,而不是让他们去猜为什么"启用了但没效果"。
还没完。解决完写入问题后,我又发现多站点子目录模式下的新坑:子站的 .htaccess 规则必须包在 RewriteRule 条件里,直接扔 mod_deflate 块会导致主站规则被子站覆盖冲突。insert_with_markers 本身不处理这种上下文,它只管机械地替换标记区间。所以现在我的规则生成器里多了站点类型分支:
if ( is_multisite() && defined( 'SUBDOMAIN_INSTALL' ) && ! SUBDOMAIN_INSTALL ) {
$rules = "<IfModule mod_rewrite.c>\nRewriteEngine On\nRewriteBase /\n" . $gzip_block . "\n</IfModule>";
}
最后汇总下这条踩坑链的排查顺序,给后来人省点时间:
1. 路径层:打印 ABSPATH、get_home_path()、实际 .htaccess 位置,Bedrock/Trellis 用户重点看;
2. 权限层:is_readable / is_writable 前置检查,别信函数返回值;
3. 标记层:确认 insert_with_markers 的 $marker 参数没和别的插件撞名,我遇到过两个缓存插件都用 WP_CACHE 当标记,后启用的把先启用的规则清空了;
4. 上下文层:多站点、子目录、反向代理(比如 Nginx 前置时 .htaccess 根本不被读取)都要单独处理。
最讽刺的是,这个"自动写入"功能最初是为了"减少用户手动配置麻烦"而设计的,结果我自己在兼容层写的代码比用户手动复制粘贴的工作量还多三倍。现在我在考虑要不要改成启用时只检测环境、生成规则文本、提示用户手动粘贴的方案——有些"自动化"的坑,跳进去才知道水多深。
有人遇到过 insert_with_markers 在 Windows IIS 环境下直接崩掉的情况吗?我暂时没环境复现,好奇它的 flock 锁在 IIS 上表现如何。