WordPress函数:register_deactivation_hook 注册插件停用函数

编辑文章

简介

register_deactivation_hook 用于在插件被停用时执行清理代码,如停止定时任务、移除临时数据、重置状态等,确保系统在插件停用后保持干净状态。

语法

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

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

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

用法

基础用法

在插件的主文件中,使用 register_deactivation_hook 注册一个函数,该函数将在插件被停用时执行。常用于清理插件运行时创建的临时数据。

<?php
/**
 * Plugin Name: 邮件通知插件
 * Description: 定期发送系统状态邮件通知。
 */

// 注册激活钩子 - 创建定时任务
register_activation_hook( __FILE__, 'email_notifier_activate' );
function email_notifier_activate() {
    // 创建每日发送邮件的定时任务
    if ( ! wp_next_scheduled( 'email_notifier_daily_report' ) ) {
        wp_schedule_event( time(), 'daily', 'email_notifier_daily_report' );
    }
}

// 注册停用钩子 - 清理定时任务
register_deactivation_hook( __FILE__, 'email_notifier_deactivate' );
function email_notifier_deactivate() {
    // 获取定时任务的下次执行时间
    $timestamp = wp_next_scheduled( 'email_notifier_daily_report' );

    // 如果定时任务存在,则清除它
    if ( $timestamp ) {
        wp_unschedule_event( $timestamp, 'email_notifier_daily_report' );
    }

    // 也可以使用 wp_clear_scheduled_hook 一次性清除该钩子的所有计划事件
    wp_clear_scheduled_hook( 'email_notifier_daily_report' );

    // 清理插件生成的临时缓存数据
    $cache_keys = array(
        'email_notifier_last_sent',
        'email_notifier_queue',
        'email_notifier_lock'
    );

    foreach ( $cache_keys as $key ) {
        wp_cache_delete( $key, 'email_notifier' );
    }
}

// 定时任务的实际处理函数
add_action( 'email_notifier_daily_report', 'send_daily_email_report' );
function send_daily_email_report() {
    // 发送邮件的逻辑
    // ...
}

进阶用法

处理多站点网络停用、状态清理、与其他插件的依赖关系解除,以及提供用户友好的反馈。

register_deactivation_hook( __FILE__, 'advanced_plugin_deactivation' );

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

    // 1. 清理多站点数据
    if ( is_multisite() && $network_wide ) {
        // 获取所有站点的 ID
        $site_ids = get_sites( array( 
            'fields' => 'ids',
            'number' => 0  // 获取所有站点
        ) );

        foreach ( $site_ids as $site_id ) {
            switch_to_blog( $site_id );
            cleanup_plugin_for_site();
            restore_current_blog();
        }
    } else {
        // 单站点或非网络停用
        cleanup_plugin_for_site();
    }

    // 2. 移除插件创建的角色和能力
    $roles_to_remove = array( 'my_plugin_manager', 'my_plugin_editor' );

    foreach ( $roles_to_remove as $role_name ) {
        if ( get_role( $role_name ) ) {
            remove_role( $role_name );
        }
    }

    // 3. 清理自定义数据库表(但保留数据,因为可能重新激活)
    // 注意:通常不在停用时删除表,除非明确用户要求
    global $wpdb;
    $temp_table_name = $wpdb->prefix . 'my_plugin_temp_data';

    // 只删除临时表,不删除主数据表
    $wpdb->query( "DROP TABLE IF EXISTS $temp_table_name" );

    // 4. 通知其他插件或主题本插件已停用
    do_action( 'my_plugin_deactivated' );

    // 5. 刷新固定链接规则(如果插件注册了自定义文章类型或分类法)
    // 这是必要的,因为停用后这些规则不再有效
    flush_rewrite_rules( false );

    // 6. 记录停用日志(用于调试)
    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
        error_log( 'My Plugin 已停用。清理完成。' );
    }
}

/**
 * 为单个站点执行插件清理
 */
function cleanup_plugin_for_site() {
    global $wpdb;

    // 清理站点特定选项
    $options_to_clean = array(
        'my_plugin_temp_settings',
        'my_plugin_cache_timestamp',
        'my_plugin_last_run'
    );

    foreach ( $options_to_clean as $option ) {
        delete_option( $option );
    }

    // 清理用户元数据(移除插件添加的临时元数据)
    // 注意:这里只删除临时数据,不删除用户的重要数据
    $wpdb->query(
        $wpdb->prepare(
            "DELETE FROM $wpdb->usermeta WHERE meta_key LIKE %s",
            '_my_plugin_temp_%'
        )
    );

    // 清理瞬态数据(transients)
    $transients_to_delete = array(
        'my_plugin_data_cache',
        'my_plugin_api_response'
    );

    foreach ( $transients_to_delete as $transient ) {
        delete_transient( $transient );
    }
}

易错点

  • 与卸载钩子混淆:最常见的概念错误。停用钩子在插件停用时执行,卸载钩子在插件删除时执行。停用时应保留用户数据,卸载时才删除所有数据。如果在停用钩子中删除用户数据,当用户重新激活插件时,所有设置将丢失。
  • 错误处理多站点:在多站点环境中,停用钩子默认只在当前站点执行。如果插件被网络范围停用,需要遍历所有站点进行清理。从 WordPress 4.6.0 开始,停用钩子回调函数会接收 $network_wide 参数。
  • 未清理所有定时任务:如果插件创建了多个定时任务,需要逐一清理。使用 wp_clear_scheduled_hook 可以清除特定钩子的所有计划事件,但要注意避免误删其他插件的同名定时任务。
  • 直接删除数据库表:在停用钩子中直接删除插件创建的主数据库表通常是错误的,因为用户可能只是暂时停用插件。这应该在卸载钩子中处理,除非有特殊原因。
  • 在停用钩子中执行长时间操作:停用钩子执行时间应该尽可能短,因为 WordPress 会等待它完成才继续停用流程。长时间操作可能导致超时或给用户带来不好的体验。
  • 忘记刷新固定链接规则:如果插件注册了自定义文章类型或分类法,停用时应该调用 flush_rewrite_rules() 来移除相关的重写规则,否则可能导致 404 错误。
  • 在停用钩子中调用未加载的函数:与激活钩子类似,停用钩子执行时,插件可能已经部分卸载。避免调用插件中其他可能已被卸载的函数,或使用函数存在性检查。

最佳实践

区分停用与卸载逻辑

明确区分哪些清理工作应该在停用时进行,哪些应该保留到卸载时。通常,停用钩子处理临时数据和运行时状态,卸载钩子处理永久数据。

// 停用钩子 - 清理运行时数据
register_deactivation_hook( __FILE__, 'my_plugin_deactivate' );
function my_plugin_deactivate() {
    // 停止定时任务
    wp_clear_scheduled_hook( 'my_plugin_cron_job' );

    // 清理临时缓存
    wp_cache_delete( 'my_plugin_temp_data', 'my_plugin' );

    // 移除临时文件(如果存在)
    $upload_dir = wp_upload_dir();
    $temp_file = $upload_dir['basedir'] . '/my_plugin_temp.json';
    if ( file_exists( $temp_file ) ) {
        unlink( $temp_file );
    }

    // 刷新重写规则
    flush_rewrite_rules( false );
}

// 卸载钩子 - 删除永久数据(在插件删除时执行)
register_uninstall_hook( __FILE__, 'my_plugin_uninstall' );
function my_plugin_uninstall() {
    global $wpdb;

    // 删除数据库表
    $table_name = $wpdb->prefix . 'my_plugin_data';
    $wpdb->query( "DROP TABLE IF EXISTS $table_name" );

    // 删除所有插件选项
    $wpdb->query(
        $wpdb->prepare(
            "DELETE FROM $wpdb->options WHERE option_name LIKE %s",
            'my_plugin_%'
        )
    );

    // 删除用户元数据
    $wpdb->query(
        $wpdb->prepare(
            "DELETE FROM $wpdb->usermeta WHERE meta_key LIKE %s",
            'my_plugin_%'
        )
    );
}

提供状态恢复机制

考虑用户可能意外停用插件,或者在测试时需要频繁启停插件。可以创建快照或导出当前设置,以便重新激活时能够恢复。

register_deactivation_hook( __FILE__, 'my_plugin_deactivate_with_backup' );

function my_plugin_deactivate_with_backup() {
    // 1. 导出当前设置到临时文件(可选)
    $settings = get_option( 'my_plugin_settings', array() );

    if ( ! empty( $settings ) ) {
        $upload_dir = wp_upload_dir();
        $backup_file = $upload_dir['basedir'] . '/my_plugin_settings_backup.json';

        // 将设置保存为 JSON 文件
        file_put_contents( 
            $backup_file, 
            wp_json_encode( $settings, JSON_PRETTY_PRINT )
        );

        // 记录备份文件路径(存储为瞬态,7天后过期)
        set_transient( 
            'my_plugin_settings_backup_path', 
            $backup_file, 
            7 * DAY_IN_SECONDS 
        );
    }

    // 2. 执行常规清理
    cleanup_plugin_temporary_data();

    // 3. 显示管理员通知(通过瞬态,在下次管理员访问时显示)
    set_transient( 'my_plugin_deactivation_notice', 
        '插件已停用。设置已备份,可在7天内重新激活时恢复。',
        60 // 60秒后过期,确保只显示一次
    );
}

// 在激活钩子中检查并恢复备份
register_activation_hook( __FILE__, 'my_plugin_activate_with_restore' );

function my_plugin_activate_with_restore() {
    // 检查是否有备份文件
    $backup_file = get_transient( 'my_plugin_settings_backup_path' );

    if ( $backup_file && file_exists( $backup_file ) ) {
        // 读取备份文件
        $backup_data = file_get_contents( $backup_file );
        $settings = json_decode( $backup_data, true );

        if ( $settings && is_array( $settings ) ) {
            // 恢复设置
            update_option( 'my_plugin_settings', $settings );

            // 删除备份文件
            unlink( $backup_file );
            delete_transient( 'my_plugin_settings_backup_path' );
        }
    }

    // 执行常规激活逻辑
    setup_plugin();
}

优雅处理依赖关系

如果插件与其他插件或主题有依赖关系,在停用时应该检查并通知用户可能的后续影响。

register_deactivation_hook( __FILE__, 'my_plugin_deactivate_with_dependency_check' );

function my_plugin_deactivate_with_dependency_check() {
    // 检查是否有其他插件依赖本插件
    $dependent_plugins = array();

    // 假设我们维护一个列表,或通过某种方式检测
    if ( function_exists( 'some_dependent_plugin_function' ) ) {
        $dependent_plugins[] = '依赖插件 A';
    }

    // 或者通过选项检测
    $integration_settings = get_option( 'other_plugin_my_plugin_integration', false );
    if ( $integration_settings ) {
        $dependent_plugins[] = '依赖插件 B';
    }

    // 如果有依赖的插件,显示警告
    if ( ! empty( $dependent_plugins ) ) {
        // 将警告保存到管理员通知
        set_transient( 'my_plugin_deactivation_warning', 
            sprintf( 
                '以下插件可能依赖 My Plugin:%s。停用 My Plugin 可能导致它们功能异常。',
                implode( ', ', $dependent_plugins )
            ),
            300 // 5分钟后过期
        );
    }

    // 清理与其他插件/主题的集成数据
    cleanup_integration_data();

    // 执行常规清理
    perform_standard_cleanup();
}

// 在管理员界面显示警告
add_action( 'admin_notices', 'show_my_plugin_deactivation_warning' );

function show_my_plugin_deactivation_warning() {
    $warning = get_transient( 'my_plugin_deactivation_warning' );

    if ( $warning ) {
        echo '<div class="notice notice-warning is-dismissible">';
        echo '<p>' . esc_html( $warning ) . '</p>';
        echo '</div>';

        // 显示后删除瞬态
        delete_transient( 'my_plugin_deactivation_warning' );
    }
}

性能优化与错误处理

确保停用过程快速、可靠,即使部分清理失败也不会阻止整个停用流程。

register_deactivation_hook( __FILE__, 'my_plugin_safe_deactivation' );

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

    try {
        // 1. 清理定时任务(带有错误处理)
        $timestamp = wp_next_scheduled( 'my_plugin_daily_task' );
        if ( $timestamp ) {
            $result = wp_unschedule_event( $timestamp, 'my_plugin_daily_task' );
            if ( ! $result ) {
                $errors[] = '无法清除定时任务。';
            }
        }

        // 2. 清理数据库临时表(使用安全查询)
        global $wpdb;
        $temp_table = $wpdb->prefix . 'my_plugin_temp';

        // 先检查表是否存在
        $table_exists = $wpdb->get_var( 
            $wpdb->prepare( 
                "SHOW TABLES LIKE %s", 
                $temp_table 
            ) 
        );

        if ( $table_exists ) {
            $result = $wpdb->query( "DROP TABLE IF EXISTS $temp_table" );
            if ( false === $result ) {
                $errors[] = '无法删除临时表。';
            }
        }

        // 3. 清理文件(带有安全检查)
        $upload_dir = wp_upload_dir();
        $log_file = $upload_dir['basedir'] . '/my_plugin_debug.log';

        if ( file_exists( $log_file ) && is_writable( $log_file ) ) {
            if ( ! unlink( $log_file ) ) {
                $errors[] = '无法删除日志文件。';
            }
        }

    } catch ( Exception $e ) {
        $errors[] = '停用过程中发生异常:' . $e->getMessage();
    }

    // 如果有错误,记录但不阻止停用
    if ( ! empty( $errors ) && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
        error_log( 'My Plugin 停用时发生错误:' . implode( ' ', $errors ) );

        // 也可以将错误保存到选项,供后续查看
        update_option( 
            'my_plugin_deactivation_errors', 
            array(
                'time' => current_time( 'mysql' ),
                'errors' => $errors
            )
        );
    }

    // 无论是否有错误,都继续执行必要的清理
    flush_rewrite_rules( false );

    // 最后清理错误记录选项本身(避免累积)
    delete_option( 'my_plugin_deactivation_errors' );
}