WordPress函数:register_activation_hook 创建插件激活函数

编辑文章

简介

register_activation_hook 用于在插件被激活时执行一次性初始化代码,如创建数据库表、设置默认选项、检查环境依赖等,确保插件功能能够正常启动。

语法

函数定义于 wp-includes/plugin.php。当插件通过 WordPress 后台激活时,它会注册一个回调函数,该函数在插件激活过程中被执行。

register_activation_hook( string $file, callable $callback )
参数 类型 必需 默认值 说明
$file 字符串 主插件文件的路径。通常使用 __FILE__ 魔术常量来引用当前文件。
$callback 可调用函数 插件激活时要执行的回调函数。该函数不接受参数。

返回值null。此函数不返回任何值,仅用于注册激活钩子。

用法

基础用法

在插件的主文件中,使用 register_activation_hook 注册一个函数,该函数将在插件首次激活时执行。常用于创建自定义数据库表和设置默认配置。

<?php
/**
 * Plugin Name: 用户活动追踪插件
 * Description: 记录用户的登录和操作活动。
 */

// 注册激活钩子
register_activation_hook( __FILE__, 'user_activity_tracker_activate' );

/**
 * 插件激活时执行的初始化函数
 */
function user_activity_tracker_activate() {
    global $wpdb;

    // 定义表名(带前缀)
    $table_name = $wpdb->prefix . 'user_activity_logs';
    $charset_collate = $wpdb->get_charset_collate();

    // 创建表的 SQL 语句
    $sql = "CREATE TABLE IF NOT EXISTS $table_name (
        id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        user_id bigint(20) UNSIGNED NOT NULL,
        activity_type varchar(50) NOT NULL,
        ip_address varchar(45),
        user_agent text,
        created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY user_id (user_id),
        KEY activity_type (activity_type)
    ) $charset_collate;";

    // 引入 WordPress 升级 API(包含 dbDelta 函数)
    require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
    dbDelta( $sql );

    // 检查表是否成功创建
    if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_name'" ) != $table_name ) {
        // 如果表创建失败,可以记录错误或触发警告
        error_log( '用户活动日志表创建失败。' );
    }

    // 设置插件默认选项
    $default_options = array(
        'log_duration' => 90, // 默认保留90天日志
        'track_login'  => true,
        'track_logout' => true,
    );

    // 如果选项不存在,则添加(避免覆盖用户可能已经设置的值)
    if ( false === get_option( 'user_activity_tracker_options' ) ) {
        add_option( 'user_activity_tracker_options', $default_options );
    }

    // 为多站点网络的每个站点执行激活(如果需要)
    if ( is_multisite() ) {
        // 注意:网络激活的处理方式不同,见进阶用法
    }
}

进阶用法

处理多站点网络激活、执行环境检查、数据迁移以及与其他插件的兼容性检查。

register_activation_hook( __FILE__, 'advanced_plugin_activation' );

function advanced_plugin_activation( $network_wide ) {
    // 参数 $network_wide 仅在 WordPress 4.6.0+ 中传递,表示是否网络范围激活

    // 1. 环境检查
    if ( version_compare( PHP_VERSION, '7.4.0', '<' ) ) {
        deactivate_plugins( plugin_basename( __FILE__ ) );
        wp_die( '本插件需要 PHP 7.4 或更高版本。您的服务器当前运行的是 ' . PHP_VERSION . '。请联系主机提供商升级 PHP。' );
    }

    if ( version_compare( get_bloginfo( 'version' ), '5.6', '<' ) ) {
        deactivate_plugins( plugin_basename( __FILE__ ) );
        wp_die( '本插件需要 WordPress 5.6 或更高版本。请先升级 WordPress。' );
    }

    // 2. 检查必要扩展
    if ( ! extension_loaded( 'json' ) ) {
        deactivate_plugins( plugin_basename( __FILE__ ) );
        wp_die( '本插件需要 JSON 扩展。请联系服务器管理员启用 PHP JSON 扩展。' );
    }

    // 3. 多站点处理
    if ( is_multisite() && $network_wide ) {
        // 获取所有站点的 ID
        $site_ids = get_sites( array( 'fields' => 'ids' ) );

        foreach ( $site_ids as $site_id ) {
            switch_to_blog( $site_id );
            setup_plugin_for_site(); // 为每个站点执行初始化
            restore_current_blog();
        }
    } else {
        // 单站点或非网络激活
        setup_plugin_for_site();
    }

    // 4. 刷新固定链接规则(如果插件注册了自定义文章类型)
    flush_rewrite_rules( false ); // 参数 false 表示不立即更新 .htaccess,仅更新数据库规则

    // 5. 设置定时任务
    if ( ! wp_next_scheduled( 'my_plugin_daily_maintenance' ) ) {
        wp_schedule_event( time(), 'daily', 'my_plugin_daily_maintenance' );
    }
}

/**
 * 为单个站点执行插件初始化
 */
function setup_plugin_for_site() {
    global $wpdb;

    // 创建表(同上例,但使用独立函数以便复用)
    $table_name = $wpdb->prefix . 'my_plugin_data';
    $charset_collate = $wpdb->get_charset_collate();

    $sql = "CREATE TABLE IF NOT EXISTS $table_name (
        id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        data_key varchar(100) NOT NULL,
        data_value longtext,
        site_id bigint(20) UNSIGNED NOT NULL DEFAULT 0,
        PRIMARY KEY (id),
        UNIQUE KEY data_key_site (data_key, site_id)
    ) $charset_collate;";

    require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
    dbDelta( $sql );

    // 设置当前站点版本号
    update_option( 'my_plugin_db_version', '1.0.0' );
}

易错点

  • 错误的主文件路径$file 参数必须是插件主文件的绝对路径。使用 __FILE__ 是最可靠的方法。如果错误地使用了其他文件路径,激活钩子可能永远不会触发。
  • 在激活钩子中直接输出内容:激活钩子执行期间,不应该使用 echoprint 直接输出 HTML 或文本。这可能会破坏 WordPress 后台的激活流程,导致白屏或显示错误。如果必须通知用户,应使用 wp_die() 并带上适当的消息。
  • 忘记处理多站点网络:如果插件支持多站点,激活钩子默认只在当前站点执行。如果用户选择“网络激活”,你需要遍历所有站点进行初始化。从 WordPress 4.6.0 开始,激活钩子回调函数会接收 $network_wide 参数,指示是否是网络范围激活。
  • 重复执行初始化逻辑:激活钩子只应在插件首次激活时运行。如果初始化代码没有检查是否已经初始化(例如,检查表是否存在或选项是否已设置),可能会导致重复创建表或覆盖用户设置。务必使用 IF NOT EXISTS 或先进行检查。
  • 在激活钩子中调用未定义的函数:激活钩子执行时,插件的其他部分可能尚未加载。如果激活函数调用了插件内定义的另一个函数,必须确保该函数在激活钩子之前已经定义,或者使用条件判断函数是否存在。
  • 未处理激活失败的情况:如果初始化失败(如创建表失败),插件可能处于半激活状态。应该检查关键操作的结果,并在失败时触发错误或回滚,防止插件处于不稳定状态。

最佳实践

幂等性设计

确保激活钩子中的操作是幂等的,即无论执行多少次,结果都相同。这对于多站点环境、插件重新激活或故障恢复至关重要。

function my_plugin_activate() {
    global $wpdb;

    $table_name = $wpdb->prefix . 'my_plugin_table';

    // 检查表是否存在,而不是盲目创建
    if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_name'" ) != $table_name ) {
        // 表不存在,创建它
        $charset_collate = $wpdb->get_charset_collate();
        $sql = "CREATE TABLE $table_name (...) $charset_collate;";
        require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
        dbDelta( $sql );
    }

    // 使用 update_option 而不是 add_option,因为它会检查是否存在
    $defaults = array( 'option1' => 'value1' );
    $current = get_option( 'my_plugin_options', array() );

    // 只添加缺失的默认值,不覆盖现有设置
    $updated = wp_parse_args( $current, $defaults );
    update_option( 'my_plugin_options', $updated );
}

版本控制与升级路径

在选项中存储插件的数据库版本,便于后续升级时检测并执行数据迁移。

function my_plugin_activate() {
    $current_db_version = get_option( 'my_plugin_db_version', '0' );

    // 初次安装
    if ( '0' === $current_db_version ) {
        create_initial_tables();
        update_option( 'my_plugin_db_version', '1.0.0' );
    }

    // 注意:实际的升级逻辑应该放在一个单独的升级函数中,
    // 并在每次插件加载时检查版本号,而不是仅在激活时。
    // 这可以确保用户通过直接上传文件更新插件时也能执行升级。
}

// 更好的做法:在插件主文件中添加版本检查
function my_plugin_check_upgrade() {
    $current_version = get_option( 'my_plugin_db_version', '0' );

    if ( version_compare( $current_version, '1.1.0', '<' ) ) {
        // 执行从 1.0.0 到 1.1.0 的升级
        upgrade_to_1_1_0();
        update_option( 'my_plugin_db_version', '1.1.0' );
    }

    if ( version_compare( $current_version, '1.2.0', '<' ) ) {
        upgrade_to_1_2_0();
        update_option( 'my_plugin_db_version', '1.2.0' );
    }
}
add_action( 'plugins_loaded', 'my_plugin_check_upgrade' );

环境检查与优雅降级

在激活前进行全面的环境检查,如果不满足要求,则禁用插件并提供清晰的错误信息,而不是让插件在损坏状态下运行。

function my_plugin_activation_check() {
    $errors = array();

    // 检查 PHP 版本
    if ( version_compare( PHP_VERSION, '7.4.0', '<' ) ) {
        $errors[] = '需要 PHP 7.4 或更高版本。';
    }

    // 检查 WordPress 版本
    if ( version_compare( get_bloginfo( 'version' ), '5.6', '<' ) ) {
        $errors[] = '需要 WordPress 5.6 或更高版本。';
    }

    // 检查必要插件
    if ( ! is_plugin_active( 'woocommerce/woocommerce.php' ) ) {
        $errors[] = '需要 WooCommerce 插件。';
    }

    // 检查函数或类是否存在
    if ( ! function_exists( 'some_required_function' ) ) {
        $errors[] = '缺少必要的函数。';
    }

    if ( ! empty( $errors ) ) {
        // 停用插件
        deactivate_plugins( plugin_basename( __FILE__ ) );

        // 显示错误信息
        $error_message = '<h3>插件无法激活</h3><ul>';
        foreach ( $errors as $error ) {
            $error_message .= '<li>' . esc_html( $error ) . '</li>';
        }
        $error_message .= '</ul>';

        wp_die( $error_message, '激活错误', array( 'back_link' => true ) );
    }
}

register_activation_hook( __FILE__, 'my_plugin_activation_check' );

与卸载钩子配合

为每个在激活钩子中创建的资源(数据库表、选项、定时任务)在卸载钩子中提供清理方案。

// 激活钩子
register_activation_hook( __FILE__, 'my_plugin_activate' );
function my_plugin_activate() {
    // 创建表、设置选项、添加定时任务...
    wp_schedule_event( time(), 'daily', 'my_plugin_cron' );
}

// 卸载钩子
register_uninstall_hook( __FILE__, 'my_plugin_uninstall' );
function my_plugin_uninstall() {
    global $wpdb;

    // 删除自定义表
    $table_name = $wpdb->prefix . 'my_plugin_table';
    $wpdb->query( "DROP TABLE IF EXISTS $table_name" );

    // 删除插件选项
    delete_option( 'my_plugin_options' );
    delete_option( 'my_plugin_db_version' );

    // 清理定时任务
    $timestamp = wp_next_scheduled( 'my_plugin_cron' );
    if ( $timestamp ) {
        wp_unschedule_event( $timestamp, 'my_plugin_cron' );
    }
}