WordPress函数:register_uninstall_hook:注册插件删除函数

编辑文章

简介

register_uninstall_hook 用于在插件被永久删除时执行彻底的数据清理代码,如删除数据库表、移除所有选项、清理用户元数据等,确保插件删除后不留任何痕迹。

语法

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

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

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

版本提示:此函数自 WordPress 2.7.0 起可用。

用法

基础用法

在插件的主文件中,使用 register_uninstall_hook 注册一个函数,该函数将在插件被永久删除时执行。用于彻底清理插件创建的所有数据。

<?php
/**
 * Plugin Name: 用户反馈插件
 * Description: 收集和管理用户反馈。
 */

// 注册卸载钩子
register_uninstall_hook( __FILE__, 'feedback_plugin_uninstall' );

/**
 * 插件删除时执行的清理函数
 */
function feedback_plugin_uninstall() {
    global $wpdb;

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

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

    // 3. 删除插件创建的用户元数据
    $wpdb->query(
        $wpdb->prepare(
            "DELETE FROM $wpdb->usermeta WHERE meta_key LIKE %s",
            'feedback_plugin_%'
        )
    );

    // 4. 删除插件创建的文章元数据
    $wpdb->query(
        $wpdb->prepare(
            "DELETE FROM $wpdb->postmeta WHERE meta_key LIKE %s",
            'feedback_plugin_%'
        )
    );

    // 5. 清理瞬态(transients)和缓存
    $wpdb->query(
        $wpdb->prepare(
            "DELETE FROM $wpdb->options WHERE option_name LIKE %s OR option_name LIKE %s",
            '_transient_feedback_plugin_%',
            '_transient_timeout_feedback_plugin_%'
        )
    );

    // 6. 删除插件上传的文件(如果有)
    $upload_dir = wp_upload_dir();
    $plugin_upload_dir = $upload_dir['basedir'] . '/feedback-plugin';

    if ( file_exists( $plugin_upload_dir ) ) {
        // 递归删除目录及其内容
        $files = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator( $plugin_upload_dir, RecursiveDirectoryIterator::SKIP_DOTS ),
            RecursiveIteratorIterator::CHILD_FIRST
        );

        foreach ( $files as $fileinfo ) {
            if ( $fileinfo->isDir() ) {
                rmdir( $fileinfo->getRealPath() );
            } else {
                unlink( $fileinfo->getRealPath() );
            }
        }

        rmdir( $plugin_upload_dir );
    }
}

进阶用法

处理多站点网络卸载、复杂数据关系的清理、提供用户确认机制,以及处理与其他插件的集成数据。

register_uninstall_hook( __FILE__, 'advanced_plugin_uninstall' );

function advanced_plugin_uninstall() {
    // 检查是否应该执行清理(在某些情况下,用户可能希望保留数据)
    // 注意:卸载钩子执行时,插件已经不在内存中,无法访问插件函数或类
    // 因此,任何配置检查必须在插件删除前通过其他方式进行

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

        foreach ( $site_ids as $site_id ) {
            switch_to_blog( $site_id );
            cleanup_plugin_data_for_site();
            restore_current_blog();
        }
    } else {
        // 单站点
        cleanup_plugin_data_for_site();
    }

    // 2. 清理网络级别的数据(仅适用于多站点)
    if ( is_multisite() ) {
        global $wpdb;

        // 删除网络选项
        $wpdb->query(
            $wpdb->prepare(
                "DELETE FROM $wpdb->sitemeta WHERE meta_key LIKE %s",
                'my_plugin_network_%'
            )
        );
    }

    // 3. 清理自定义角色和能力
    $roles_to_remove = array( 'my_plugin_admin', 'my_plugin_contributor' );

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

    // 4. 清理自定义数据库表(包括关联表)
    global $wpdb;

    $tables_to_drop = array(
        $wpdb->prefix . 'my_plugin_main_data',
        $wpdb->prefix . 'my_plugin_meta',
        $wpdb->prefix . 'my_plugin_relationships',
    );

    foreach ( $tables_to_drop as $table ) {
        $wpdb->query( "DROP TABLE IF EXISTS $table" );
    }

    // 5. 清理重写规则缓存
    delete_option( 'rewrite_rules' );

    // 6. 记录卸载事件(可选)
    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
        error_log( 'My Plugin 已从系统中完全删除。' );
    }
}

/**
 * 为单个站点执行数据清理
 */
function cleanup_plugin_data_for_site() {
    global $wpdb;

    // 清理所有选项
    $option_patterns = array(
        'my_plugin_%',
        '_my_plugin_%',
        'my-plugin-%',
    );

    foreach ( $option_patterns as $pattern ) {
        $wpdb->query(
            $wpdb->prepare(
                "DELETE FROM $wpdb->options WHERE option_name LIKE %s",
                $pattern
            )
        );
    }

    // 清理所有自定义文章类型的内容
    $custom_post_types = array( 'my_plugin_item', 'my_plugin_review' );

    foreach ( $custom_post_types as $post_type ) {
        // 获取所有该类型的文章
        $posts = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT ID FROM $wpdb->posts WHERE post_type = %s",
                $post_type
            )
        );

        if ( ! empty( $posts ) ) {
            // 删除文章元数据
            $post_ids_placeholder = implode( ',', array_fill( 0, count( $posts ), '%d' ) );
            $wpdb->query(
                $wpdb->prepare(
                    "DELETE FROM $wpdb->postmeta WHERE post_id IN ($post_ids_placeholder)",
                    $posts
                )
            );

            // 删除文章
            $wpdb->query(
                $wpdb->prepare(
                    "DELETE FROM $wpdb->posts WHERE post_type = %s",
                    $post_type
                )
            );
        }
    }

    // 清理自定义分类法的数据
    $custom_taxonomies = array( 'my_plugin_category', 'my_plugin_tag' );

    foreach ( $custom_taxonomies as $taxonomy ) {
        // 获取所有该分类法的条目
        $terms = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT t.term_id FROM $wpdb->terms AS t 
                INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id 
                WHERE tt.taxonomy = %s",
                $taxonomy
            )
        );

        if ( ! empty( $terms ) ) {
            // 删除分类法关系
            $wpdb->query(
                $wpdb->prepare(
                    "DELETE FROM $wpdb->term_relationships WHERE term_taxonomy_id IN (
                        SELECT term_taxonomy_id FROM $wpdb->term_taxonomy WHERE taxonomy = %s
                    )",
                    $taxonomy
                )
            );

            // 删除分类法数据
            $wpdb->query(
                $wpdb->prepare(
                    "DELETE FROM $wpdb->term_taxonomy WHERE taxonomy = %s",
                    $taxonomy
                )
            );

            // 删除分类条目
            $term_ids_placeholder = implode( ',', array_fill( 0, count( $terms ), '%d' ) );
            $wpdb->query(
                $wpdb->prepare(
                    "DELETE FROM $wpdb->terms WHERE term_id IN ($term_ids_placeholder)",
                    $terms
                )
            );

            // 删除分类元数据
            $wpdb->query(
                $wpdb->prepare(
                    "DELETE FROM $wpdb->termmeta WHERE term_id IN ($term_ids_placeholder)",
                    $terms
                )
            );
        }
    }
}

易错点

  • 混淆卸载与停用钩子:这是最常见的错误。register_uninstall_hook 在插件被永久删除时执行,而 register_deactivation_hook 在插件被停用时执行。卸载钩子应该删除所有插件数据,停用钩子通常只清理临时数据。
  • 卸载钩子函数必须在插件外部可访问:卸载钩子执行时,插件文件已被删除或不再加载,因此回调函数必须定义为独立的全局函数,不能是类的方法或闭包函数。如果必须使用类方法,应使用静态方法。
  • 忘记处理多站点网络:在多站点环境中,卸载钩子默认只在当前站点执行。如果插件被网络范围删除,需要遍历所有站点进行清理。
  • 未清理所有数据痕迹:插件可能创建了各种类型的数据:选项、用户元数据、文章元数据、自定义表、上传文件、瞬态数据等。卸载钩子应该清理所有这些数据,避免在数据库中留下垃圾。
  • 在卸载过程中产生新数据:卸载钩子不应该创建任何新数据或触发其他操作(如发送邮件、调用外部API)。它应该只专注于删除现有数据。
  • 卸载钩子执行时机:卸载钩子只在通过 WordPress 后台的插件页面删除插件时触发。如果用户通过 FTP 直接删除插件文件,或者使用服务器文件管理器删除,卸载钩子将不会执行。
  • 未考虑数据保留需求:某些用户可能希望删除插件但保留数据。虽然 WordPress 没有内置的数据保留机制,但可以考虑在插件设置中添加选项,让用户选择卸载时是否保留数据。

最佳实践

使用静态类方法处理复杂卸载

对于复杂的插件,可以将卸载逻辑封装在静态类方法中,以提高代码组织和可维护性。

<?php
/**
 * Plugin Name: 高级数据管理插件
 */

// 定义主插件类
class Advanced_Data_Manager {

    // 激活钩子
    public static function activate() {
        // 创建表等初始化逻辑
    }

    // 卸载钩子 - 必须是静态方法
    public static function uninstall() {
        // 清理逻辑可以调用其他静态方法
        self::delete_all_options();
        self::drop_custom_tables();
        self::remove_uploaded_files();
        self::cleanup_multisite_data();
    }

    private static function delete_all_options() {
        global $wpdb;

        $patterns = array(
            'adv_data_manager_%',
            '_adv_data_manager_%',
        );

        foreach ( $patterns as $pattern ) {
            $wpdb->query(
                $wpdb->prepare(
                    "DELETE FROM $wpdb->options WHERE option_name LIKE %s",
                    $pattern
                )
            );
        }
    }

    private static function drop_custom_tables() {
        global $wpdb;

        $tables = array(
            $wpdb->prefix . 'adv_data_records',
            $wpdb->prefix . 'adv_data_meta',
        );

        foreach ( $tables as $table ) {
            $wpdb->query( "DROP TABLE IF EXISTS $table" );
        }
    }

    private static function remove_uploaded_files() {
        $upload_dir = wp_upload_dir();
        $plugin_dir = $upload_dir['basedir'] . '/adv-data-manager';

        if ( file_exists( $plugin_dir ) ) {
            self::rrmdir( $plugin_dir );
        }
    }

    private static function rrmdir( $dir ) {
        if ( ! file_exists( $dir ) ) {
            return;
        }

        $files = array_diff( scandir( $dir ), array( '.', '..' ) );

        foreach ( $files as $file ) {
            $path = $dir . '/' . $file;

            if ( is_dir( $path ) ) {
                self::rrmdir( $path );
            } else {
                unlink( $path );
            }
        }

        rmdir( $dir );
    }

    private static function cleanup_multisite_data() {
        if ( ! is_multisite() ) {
            return;
        }

        global $wpdb;

        // 清理网络选项
        $wpdb->query(
            $wpdb->prepare(
                "DELETE FROM $wpdb->sitemeta WHERE meta_key LIKE %s",
                'adv_data_manager_%'
            )
        );
    }
}

// 注册激活钩子
register_activation_hook( __FILE__, array( 'Advanced_Data_Manager', 'activate' ) );

// 注册卸载钩子 - 使用静态方法
register_uninstall_hook( __FILE__, array( 'Advanced_Data_Manager', 'uninstall' ) );

提供数据保留选项

在插件设置中提供选项,让用户选择卸载时是否保留数据。这需要在卸载钩子中读取一个特殊的选项来决定是否执行清理。

// 在插件设置页面添加选项
function my_plugin_add_settings() {
    add_settings_field(
        'my_plugin_data_retention',
        '卸载时保留数据',
        'my_plugin_data_retention_callback',
        'my_plugin_settings',
        'my_plugin_general_section'
    );

    register_setting( 'my_plugin_settings', 'my_plugin_data_retention' );
}
add_action( 'admin_init', 'my_plugin_add_settings' );

function my_plugin_data_retention_callback() {
    $value = get_option( 'my_plugin_data_retention', 'no' );
    ?>
    <label>
        <input type="checkbox" name="my_plugin_data_retention" value="yes" <?php checked( $value, 'yes' ); ?>>
        卸载插件时保留所有数据(可以稍后重新安装恢复)
    </label>
    <p class="description">如果勾选,删除插件时将不会删除任何数据。请谨慎使用此选项。</p>
    <?php
}

// 修改卸载钩子以检查数据保留选项
register_uninstall_hook( __FILE__, 'my_plugin_smart_uninstall' );

function my_plugin_smart_uninstall() {
    // 注意:卸载钩子执行时,插件选项可能已经被删除
    // 因此,我们需要在卸载前检查一个特殊的瞬态或选项

    // 方法1:检查一个特殊文件(如果存在则保留数据)
    $retention_file = WP_CONTENT_DIR . '/uploads/my_plugin_retain_data.flag';

    if ( file_exists( $retention_file ) ) {
        // 用户希望保留数据,跳过清理
        // 可以记录日志或什么都不做
        error_log( 'My Plugin: 数据保留选项已启用,跳过数据清理。' );
        return;
    }

    // 方法2:在停用钩子中设置一个瞬态(更可靠)
    // 在停用钩子中:
    // $retain_data = get_option( 'my_plugin_data_retention', 'no' );
    // if ( 'yes' === $retain_data ) {
    //     set_transient( 'my_plugin_retain_data_on_uninstall', 'yes', 300 ); // 5分钟有效期
    // }

    // 然后在卸载钩子中检查:
    // if ( 'yes' === get_transient( 'my_plugin_retain_data_on_uninstall' ) ) {
    //     delete_transient( 'my_plugin_retain_data_on_uninstall' );
    //     return;
    // }

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

安全的数据清理

确保数据清理操作安全、完整,避免SQL注入风险,并正确处理可能的数据关联。

function perform_complete_cleanup() {
    global $wpdb;

    // 使用事务确保数据一致性(如果数据库引擎支持)
    $wpdb->query( 'START TRANSACTION' );

    try {
        // 1. 删除自定义文章类型的内容
        $post_types = array( 'my_cpt' );

        foreach ( $post_types as $post_type ) {
            // 获取所有文章ID
            $post_ids = $wpdb->get_col(
                $wpdb->prepare(
                    "SELECT ID FROM $wpdb->posts WHERE post_type = %s",
                    $post_type
                )
            );

            if ( ! empty( $post_ids ) ) {
                // 批量删除文章元数据(使用IN子句)
                $placeholders = implode( ',', array_fill( 0, count( $post_ids ), '%d' ) );

                // 文章元数据
                $wpdb->query(
                    $wpdb->prepare(
                        "DELETE FROM $wpdb->postmeta WHERE post_id IN ($placeholders)",
                        $post_ids
                    )
                );

                // 文章关系(如分类法关系)
                $wpdb->query(
                    $wpdb->prepare(
                        "DELETE FROM $wpdb->term_relationships WHERE object_id IN ($placeholders)",
                        $post_ids
                    )
                );

                // 文章本身
                $wpdb->query(
                    $wpdb->prepare(
                        "DELETE FROM $wpdb->posts WHERE ID IN ($placeholders)",
                        $post_ids
                    )
                );
            }
        }

        // 2. 删除自定义表(使用预处理语句避免SQL注入)
        $tables = array(
            $wpdb->prefix . 'my_plugin_data',
            $wpdb->prefix . 'my_plugin_logs',
        );

        foreach ( $tables as $table ) {
            // 检查表是否存在
            $table_exists = $wpdb->get_var( 
                $wpdb->prepare( 
                    "SHOW TABLES LIKE %s", 
                    $table 
                ) 
            );

            if ( $table_exists ) {
                $wpdb->query( "DROP TABLE IF EXISTS " . esc_sql( $table ) );
            }
        }

        // 3. 删除所有插件相关的选项
        $option_patterns = array( 'my_plugin_%', '_my_plugin_%' );

        foreach ( $option_patterns as $pattern ) {
            $wpdb->query(
                $wpdb->prepare(
                    "DELETE FROM $wpdb->options WHERE option_name LIKE %s",
                    $pattern
                )
            );
        }

        // 提交事务
        $wpdb->query( 'COMMIT' );

        // 记录成功
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            error_log( 'My Plugin 数据清理完成。' );
        }

    } catch ( Exception $e ) {
        // 回滚事务
        $wpdb->query( 'ROLLBACK' );

        // 记录错误
        error_log( 'My Plugin 数据清理失败: ' . $e->getMessage() );

        // 抛出异常,让WordPress知道卸载失败
        throw $e;
    }
}

与卸载脚本配合

对于特别复杂的插件,可以考虑提供独立的卸载脚本,在卸载钩子中调用。

register_uninstall_hook( __FILE__, 'my_plugin_uninstall_handler' );

function my_plugin_uninstall_handler() {
    // 包含卸载脚本
    $uninstall_script = plugin_dir_path( __FILE__ ) . 'uninstall.php';

    if ( file_exists( $uninstall_script ) ) {
        require_once $uninstall_script;

        // 假设卸载脚本中定义了以下函数
        if ( function_exists( 'my_plugin_run_uninstall' ) ) {
            my_plugin_run_uninstall();
        }
    } else {
        // 回退到基本清理
        basic_cleanup();
    }
}

// uninstall.php 文件内容示例:
// <?php
// defined( 'WP_UNINSTALL_PLUGIN' ) || exit;
//
// function my_plugin_run_uninstall() {
//     // 完整的清理逻辑
//     // ...
// }