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() {
// // 完整的清理逻辑
// // ...
// }