`register_activation_hook` 里直接 `wp_insert_post` 建演示页:为什么你的插件一启用就白屏或丢数据,以及 `is_plugin_active` 检测为何救不了你
上周帮朋友排查一个插件,启用瞬间直接 500,错误日志里躺着 Call to undefined function wp_insert_post()。我愣了下——这函数不是 WordPress 核心吗?怎么可能未定义?
问题出在 register_activation_hook 的执行时机。很多人以为插件启用时 WordPress 已经全副武装,实际上钩子回调跑在 plugins_loaded 之前,不少常用 API 根本没加载。更坑的是,有些函数"半可用":不报错,但行为诡异,比如 wp_insert_post 在某些环境下能跑,换台服务器就炸。
❌ 错误写法:自信满满地"裸奔"
// my-plugin.php
register_activation_hook( __FILE__, 'my_plugin_activate' );
function my_plugin_activate() {
// 灾难现场:pluggable.php 可能还没加载
wp_insert_post( array(
'post_title' => '插件演示页',
'post_content' => '[my_plugin_demo]',
'post_status' => 'publish',
'post_type' => 'page',
) );
// 更隐蔽的坑:直接操作表,绕过核心钩子
global $wpdb;
$wpdb->query( "INSERT INTO {$wpdb->prefix}my_plugin_logs VALUES ..." );
// 假设当前用户就是管理员,不做任何校验
update_option( 'my_plugin_owner', get_current_user_id() );
}
这段代码的问题层层叠加:
wp_insert_post依赖的 taxonomy、post 缓存机制未初始化,可能插入脏数据- 直接 SQL 绕过
wpdb->insert,字符集和pre_insert钩子全丢 - CLI 启用插件时
get_current_user_id()返回 0,owner 变成"幽灵用户" - 没有
is_multisite()判断,在子站网络启用时可能写到错误站点
✅ 正确写法:给激活钩子穿上"防弹衣"
// my-plugin.php
register_activation_hook( __FILE__, 'my_plugin_activate_safe' );
function my_plugin_activate_safe() {
// 第一层:确保我们在能干活的环境
if ( ! current_user_can( 'activate_plugins' ) ) {
return; // CLI 或异常调用直接退
}
// 第二层:显式加载必要文件(防御性编程)
require_once ABSPATH . 'wp-admin/includes/post.php';
require_once ABSPATH . 'wp-admin/includes/plugin.php';
// 第三层:用 capability 检测替代 is_plugin_active
// 后者在激活钩子内部不可靠——插件状态处于"切换中"
$needs_network = is_multisite() && is_network_admin();
// 第四层:所有写入操作加锁,防止重复执行
$lock_key = 'my_plugin_activating_' . get_current_blog_id();
if ( get_transient( $lock_key ) ) {
return; // 已经在跑了,防并发
}
set_transient( $lock_key, 1, 30 );
// 第五层:用 wp_insert_post 但先检查是否已存在
$existing = get_page_by_path( 'plugin-demo', OBJECT, 'page' );
if ( ! $existing ) {
$page_id = wp_insert_post( array(
'post_title' => '插件演示页',
'post_name' => 'plugin-demo',
'post_content' => '[my_plugin_demo]',
'post_status' => 'publish',
'post_type' => 'page',
'post_author' => get_current_user_id() ?: 1, // 兜底到管理员
), true ); // WP_Error 模式
if ( is_wp_error( $page_id ) ) {
error_log( 'My Plugin: 演示页创建失败 - ' . $page_id->get_error_message() );
// 记录到选项而非直接抛异常,避免阻断启用流程
update_option( 'my_plugin_activation_errors', $page_id->get_error_messages() );
} else {
update_option( 'my_plugin_demo_page_id', $page_id );
}
}
// 数据库操作走正规军
my_plugin_setup_tables();
delete_transient( $lock_key );
}
function my_plugin_setup_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$table_name = $wpdb->prefix . 'my_plugin_logs';
// 必须用 dbDelta,且放在单独文件避免重复定义
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
event_type varchar(50) NOT NULL,
user_id bigint(20) unsigned DEFAULT 0,
context longtext,
PRIMARY KEY (id),
KEY idx_event_time (event_type, created_at)
) {$charset_collate};";
dbDelta( $sql );
}
关键差异对照表
| 场景 | 错误写法后果 | 正确写法处理 |
|---|---|---|
| 通过 WP-CLI 启用 | get_current_user_id() = 0,owner 记录异常 | capability 检测 + user_id 兜底到 1 |
| 多站点网络启用 | 数据写到主站,子站找不到 | is_network_admin() 判断 + 博客 ID 隔离锁 |
| 快速双击启用按钮 | 重复插入演示页 | transient 锁 + get_page_by_path 预检 |
| 插件依赖未激活 | 静默失败,用户无从知晓 | 激活前 current_user_can 校验,错误入选项 |
| 字符集不匹配 | emoji 或中文乱码 | $wpdb->get_charset_collate() 自动适配 |
一个更隐蔽的陷阱:is_plugin_active 的自指悖论
有人想在激活钩子里检测"另一个插件是否活跃"再决定行为:
// 千万别这么干!
if ( is_plugin_active( 'some-dep/some-dep.php' ) ) {
// 逻辑...
}
问题很微妙:is_plugin_active 读的是数据库选项 active_plugins,而当前插件自己的启用过程正处于事务中间态——这个选项可能还没更新,或者网络激活时用的是 site_option 而非 option。更安全的做法是检测目标插件的类或函数是否存在,或者直接检测其注册的动作钩子。
// 靠谱的依赖检测
$has_dependency = class_exists( 'Some_Dep_Main' )
|| has_action( 'some_dep_loaded' );
最后
激活钩子不是普通的"初始化入口",它是 WordPress 生命周期里最脆弱的缝隙之一。我的原则是:能延后到 init 或 admin_init 做的事,绝不塞进激活钩子;必须在这里做的,按"最小写入 + 全程防御"处理。你有过在激活阶段踩的奇坑吗?比如 flush_rewrite_rules 时机不对导致 404 全站爆炸之类的——欢迎丢出来一起复盘。