插件卸载时 `uninstall.php` 与 `register_uninstall_hook` 二选一?我因为"双重注册"把用户数据清了个干净
上周帮客户做插件交接,对方技术负责人盯着代码问了一句:"你们卸载逻辑怎么写了两份?"我当时一愣,回去翻仓库才发现——uninstall.php 里删了一遍表,register_uninstall_hook 的回调里又删了一遍。更绝的是,两份逻辑还不完全一致,一个清选项、一个清表,用户点卸载时到底执行哪份,全看 WordPress 当天心情。
这篇帖子不是讲"怎么写卸载",是讲"怎么写不互相打架"。
一、WordPress 的卸载执行优先级,很多人记反了
官方文档说得很含糊,实测行为是这样的:
// 情况A:只有 uninstall.php,且文件头部注释合规
// → 直接走文件,不触发 register_uninstall_hook
// 情况B:没有 uninstall.php,或有但注释不对
// → 才会去查 register_uninstall_hook 注册的回调
// 情况C:两者都有,且 uninstall.php 注释正确
// → 只执行 uninstall.php,hook 回调被完全忽略
但"情况C"有个致命盲区:如果 uninstall.php 存在但语法报错,或者权限问题导致 is_uninstallable_plugin() 返回 false,系统会静默 fallback 到 hook 回调。两份逻辑都会被执行——只是不在同一次请求里,而是"这次失败,下次补刀"。
我踩的坑更隐蔽:uninstall.php 开头忘了写标准的插件信息注释,导致 WordPress 不认它是"合规的卸载文件",于是每次都走 hook。后来补上了注释,但 hook 没删,测试环境因为缓存问题没触发双重执行,线上环境换了服务器配置后突然"生效"了。
二、建表时的"隐形契约":charset 与 collation 别硬编码
这是另一个血案。早期版本为了"兼容",我直接把建表语句写死了:
$sql = "CREATE TABLE {$wpdb->prefix}my_plugin_logs (
id bigint(20) NOT NULL AUTO_INCREMENT,
content longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
...
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
客户数据库是 utf8mb4_0900_ai_ci(MySQL 8.0 默认),插件激活时 dbDelta 对比结构,发现 collation 不一致,每次页面加载都尝试"修正"表结构。后台慢得像蜗牛,SHOW PROCESSLIST 里全是 ALTER TABLE。
现在我的模板里固定这么写:
global $wpdb;
$charset_collate = $wpdb->get_charset_collate(); // 让 WordPress 去猜宿主环境
$sql = "CREATE TABLE {$wpdb->prefix}my_plugin_logs (
id bigint(20) NOT NULL AUTO_INCREMENT,
content longtext,
...
) $charset_collate;";
注意 content longtext 后面不再跟 CHARACTER SET ... COLLATE ...,让 $charset_collate 统一收口。如果某个字段确实需要特殊编码(比如存 emoji 的字段要 utf8mb4 而宿主是 utf8),单独声明并加注释说明原因。
三、升级脚本的"时间旅行"问题:版本号比较别用字符串
插件从 1.9 升级到 1.10,我的升级脚本没执行。调试半天发现:
// 错误示范
if ( $current_version < '1.10' ) { ... }
// PHP 字符串比较:'1.9' < '1.10' 返回 false
// 因为 '1.9' 的第二个字符 '9' > '1.10' 的第二个字符 '1'
现在全部改用 version_compare(),并且版本号入库时存的是"最后成功执行的升级版本",不是"当前插件版本":
$db_version = get_option( 'my_plugin_db_version', '0.0.0' );
$upgrades = [
'1.2.0' => 'upgrade_120_add_index',
'1.5.0' => 'upgrade_150_migrate_options',
'1.10.0' => 'upgrade_1100_split_tables',
];
foreach ( $upgrades as $ver => $callback ) {
if ( version_compare( $db_version, $ver, '

