剖析 WordPress `wp_update_user()` 函数源码:用户数据更新与钩子触发的流程。

各位观众老爷,大家好!今天咱们来聊聊WordPress里一个“老朋友”——wp_update_user() 函数。这哥们儿,负责更新用户信息,看似简单,实则内部乾坤很大。不搞清楚它的来龙去脉,以后遇到用户数据相关的bug,那可就抓瞎了。

咱们这次就来扒一扒 wp_update_user() 的源码,看看它到底做了些什么,又埋了哪些“雷”。

一、wp_update_user() 的基本用法:简单粗暴的更新

首先,我们先来回顾一下 wp_update_user() 的基本用法。这个函数接受一个数组作为参数,数组里包含了要更新的用户数据。最关键的是,必须包含一个 ID 字段,用来指定要更新哪个用户。

<?php
$user_data = array(
    'ID'         => 1, // 用户ID,必须有!
    'first_name' => '张',
    'last_name'  => '三',
    'user_email' => '[email protected]'
);

$user_id = wp_update_user( $user_data );

if ( is_wp_error( $user_id ) ) {
    echo '更新失败:' . $user_id->get_error_message();
} else {
    echo '更新成功!用户ID:' . $user_id;
}
?>

上面的代码片段展示了如何更新用户的名字和邮箱。注意,如果更新成功,wp_update_user() 会返回用户ID;如果失败,会返回一个 WP_Error 对象,包含了错误信息。

二、源码剖析:一层一层扒开它的皮

现在,咱们进入正题,开始扒 wp_update_user() 的源码。为了方便理解,我把代码拆解成了几个关键步骤,并加上了详细的注释。

  1. 参数校验和准备工作
/**
 * Updates user data.
 *
 * @since 2.0.0
 *
 * @param array|object $userdata User's data. Accepts either a user object or an array of user data.
 *                              The following keys are valid:
 *                              'ID' - (int) Required. User ID.
 *                              'user_pass' - (string) The user's password.
 *                              'user_login' - (string) The user's login name.
 *                              'user_nicename' - (string) The user's nice name.
 *                              'user_url' - (string) The user's URL.
 *                              'user_email' - (string) The user's email address.
 *                              'display_name' - (string) The user's display name.
 *                              'nickname' - (string) The user's nickname.
 *                              'first_name' - (string) The user's first name.
 *                              'last_name' - (string) The user's last name.
 *                              'description' - (string) The user's description.
 *                              'rich_editing' - (string) Whether to enable the rich editor.
 *                              'syntax_highlighting' - (string) Whether to enable syntax highlighting.
 *                              'comment_shortcuts' - (string) Whether to enable comment shortcuts.
 *                              'admin_color' - (string) The user's admin color scheme.
 *                              'use_ssl' - (string) Whether to force SSL for the user's admin screens.
 *                              'show_admin_bar_front' - (string) Whether to show the admin bar on the front end.
 *                              'locale' - (string) The user's locale.
 *                              'wp_capabilities' - (array) The user's capabilities.
 *                              'wp_user_level' - (int) The user's user level.
 *                              'user_activation_key' - (string) The user's activation key.
 *                              'spam' - (string) Whether the user is marked as spam.
 *                              'deleted' - (string) Whether the user is marked as deleted.
 * @return int|WP_Error WP_Error on failure, User ID on success.
 */
function wp_update_user( $userdata ) {
    global $wpdb;

    // 首先,确保传入的是数组或者对象
    if ( ! is_array( $userdata ) && ! is_object( $userdata ) ) {
        return new WP_Error( 'invalid_user_data', __( 'Invalid user data.' ) );
    }

    // 如果传入的是对象,转换成数组
    if ( is_object( $userdata ) ) {
        $userdata = get_object_vars( $userdata );
    }

    // 必须要有用户ID
    if ( ! isset( $userdata['ID'] ) ) {
        return new WP_Error( 'no_user_id', __( 'You must include a user ID.' ) );
    }

    $user_id = (int) $userdata['ID']; // 强制转换成整数,防止SQL注入

    // 获取旧的用户数据
    $old_user = get_userdata( $user_id );

    if ( false === $old_user ) {
        return new WP_Error( 'invalid_user', __( 'Invalid user ID.' ) );
    }

    // 移除不应该直接更新的字段
    $illegal_names = array( 'user_login', 'user_pass', 'user_nicename' );
    foreach ( $illegal_names as $illegal_name ) {
        if ( isset( $userdata[ $illegal_name ] ) ) {
            unset( $userdata[ $illegal_name ] );
        }
    }

    // 过滤用户数据,允许修改,`pre_user_update` 钩子
    $userdata = apply_filters( 'pre_user_update', $userdata, $user_id );
}

这一步主要做了以下几件事:

  • 类型检查: 确保传入的是数组或对象。
  • ID验证: 检查 ID 字段是否存在,这是更新用户的关键。
  • 获取旧数据: 通过 get_userdata() 获取旧的用户数据,后续会用到。
  • 移除非法字段: 移除 user_loginuser_passuser_nicename 这些不应该直接更新的字段,如果需要修改这些字段,应该使用专门的函数。
  • pre_user_update 钩子: 这是第一个钩子,允许我们在更新用户数据之前,修改 userdata 数组。
  1. 数据清洗和准备
    // 数据清洗,使用 wp_unslash() 函数
    $userdata = wp_unslash( $userdata );

    // 准备更新的数据数组
    $data = array();
    $update = array();

    $update['ID'] = $user_id;

    // 定义允许更新的字段
    $updateable_keys = array(
        'user_url',
        'user_email',
        'display_name',
        'nickname',
        'first_name',
        'last_name',
        'description',
        'rich_editing',
        'syntax_highlighting',
        'comment_shortcuts',
        'admin_color',
        'use_ssl',
        'show_admin_bar_front',
        'locale',
    );

    // 循环遍历,将需要更新的字段添加到 $data 数组中
    foreach ( $updateable_keys as $field ) {
        if ( isset( $userdata[ $field ] ) ) {
            $data[ $field ] = $userdata[ $field ];
            $update[ $field ] = $userdata[ $field ];
        }
    }

    // 处理 user_email 的情况
    if ( isset( $userdata['user_email'] ) ) {
        $user_email = sanitize_email( $userdata['user_email'] );

        if ( ! is_email( $user_email ) ) {
            return new WP_Error( 'invalid_email', __( 'The email address is invalid.' ) );
        }

        if ( email_exists( $user_email, $user_id ) ) {
            return new WP_Error( 'email_exists', __( 'The email address is already in use.' ) );
        }

        $data['user_email'] = $user_email;
        $update['user_email'] = $user_email;
    }

这一步主要做了以下几件事:

  • 数据清洗: 使用 wp_unslash() 函数移除反斜杠,防止转义问题。
  • 准备更新数组: 创建 $data 数组,用于存储要更新的字段和值。
  • 定义可更新字段: $updateable_keys 数组定义了允许直接更新的字段。
  • 字段过滤: 循环遍历 $updateable_keys,将 $userdata 中存在的字段添加到 $data 数组中。
  • user_email 特殊处理:user_email 进行了特殊处理,包括验证邮箱格式、检查邮箱是否已存在。
  1. 更新用户元数据(User Meta)
    // 处理用户元数据
    $meta_update = array();

    $meta_keys = array(
        'nickname',
        'first_name',
        'last_name',
        'description'
    );

    foreach ( $meta_keys as $meta_key ) {
        if ( isset( $userdata[ $meta_key ] ) ) {
            $meta_update[ $meta_key ] = $userdata[ $meta_key ];
        }
    }

    foreach ( $meta_update as $meta_key => $meta_value ) {
        update_user_meta( $user_id, $meta_key, $meta_value );
    }

这一步专门处理用户元数据。虽然 nicknamefirst_namelast_namedescription 也在 $updateable_keys 中,但是它们的值实际上是存储在 wp_usermeta 表中的。所以这里需要单独调用 update_user_meta() 函数来更新这些字段。

  1. 更新用户角色和权限
    // 处理用户角色
    if ( isset( $userdata['role'] ) ) {
        $role = $userdata['role'];

        // 如果当前用户没有编辑用户的权限,并且尝试修改自己的角色,则返回错误
        if ( ! current_user_can( 'edit_users' ) && $user_id == get_current_user_id() ) {
            return new WP_Error( 'edit_users', __( 'Sorry, you are not allowed to edit users.' ) );
        }

        // 删除旧的角色
        $old_user->remove_role( $old_user->roles[0] );

        // 添加新的角色
        $old_user->add_role( $role );
    }

    // 处理用户权限
    if ( isset( $userdata['wp_capabilities'] ) && is_array( $userdata['wp_capabilities'] ) ) {
        $capabilities = $userdata['wp_capabilities'];

        // 更新用户权限
        foreach ( $capabilities as $cap => $grant ) {
            $old_user->add_cap( $cap, $grant );
        }
    }

这一步处理用户角色和权限。

  • role 处理: 如果传入了 role 字段,会先移除旧的角色,然后添加新的角色。
  • wp_capabilities 处理: 如果传入了 wp_capabilities 字段,会循环遍历,更新用户的权限。
  1. 执行数据库更新
    // 如果有需要更新的数据,则执行数据库更新
    if ( ! empty( $data ) ) {
        $where = array( 'ID' => $user_id );
        $result = $wpdb->update( $wpdb->users, $data, $where );

        if ( false === $result ) {
            return new WP_Error( 'db_update_error', __( 'Could not update user in database.' ), $wpdb->last_error );
        }
    }

这一步真正执行数据库更新操作。使用 $wpdb->update() 函数更新 wp_users 表中的数据。

  1. 触发 Action 钩子
    // 触发 action 钩子
    do_action( 'profile_update', $user_id, $old_user->data );
    do_action( 'edit_user_profile_update', $user_id );
    do_action( 'personal_options_update', $user_id );

    wp_cache_delete( $user_id, 'users' );
    wp_cache_delete( 'user_email_' . $data['user_email'], 'useremail' );

    return $user_id;
}

最后,触发一系列的 action 钩子,并清理缓存。

  • profile_update 钩子: 在用户资料更新后触发,传递用户ID和旧的用户数据。
  • edit_user_profile_update 钩子: 在编辑用户资料页面更新后触发,传递用户ID。
  • personal_options_update 钩子: 在个人选项更新后触发,传递用户ID。
  • 清理缓存: 使用 wp_cache_delete() 函数清理用户缓存,确保下次获取到的是最新的数据。

三、wp_update_user() 流程总结

为了更清晰地理解 wp_update_user() 的流程,我用表格来总结一下:

步骤 描述 涉及的函数/钩子
1 参数校验和准备工作: 验证输入参数,获取旧的用户数据,移除非法字段,触发 pre_user_update 钩子。 is_array(), is_object(), get_object_vars(), get_userdata(), apply_filters( 'pre_user_update' )
2 数据清洗和准备: 清洗数据,定义可更新字段,过滤需要更新的字段,对 user_email 进行特殊处理。 wp_unslash(), sanitize_email(), is_email(), email_exists()
3 更新用户元数据: 更新存储在 wp_usermeta 表中的用户元数据,例如 nicknamefirst_namelast_namedescription update_user_meta()
4 更新用户角色和权限: 更新用户的角色和权限。 remove_role(), add_role(), add_cap()
5 执行数据库更新: 使用 $wpdb->update() 函数更新 wp_users 表中的数据。 $wpdb->update()
6 触发 Action 钩子和清理缓存: 触发一系列的 action 钩子,例如 profile_updateedit_user_profile_updatepersonal_options_update,并清理用户缓存。 do_action(), wp_cache_delete()

四、重点关注的钩子:pre_user_updateprofile_update

wp_update_user() 函数中,有两个钩子尤其值得关注:

  • pre_user_update 这个钩子允许我们在更新用户数据之前,修改 $userdata 数组。这给了我们很大的灵活性,可以自定义一些验证规则,或者添加一些额外的字段。
<?php
add_filter( 'pre_user_update', 'my_pre_user_update', 10, 2 );

function my_pre_user_update( $userdata, $user_id ) {
    // 检查用户是否尝试修改自己的邮箱
    if ( isset( $userdata['user_email'] ) && $user_id == get_current_user_id() ) {
        // 验证新的邮箱是否符合自定义的规则
        if ( ! my_validate_email( $userdata['user_email'] ) ) {
            // 如果不符合规则,则返回错误
            add_filter( 'wp_kses_allowed_html', 'wpse_kses_allowed_html', 10, 2 );
            function wpse_kses_allowed_html( $tags, $context ) {
                $tags['p']['class'] = true;
                $tags['a']['class'] = true;
                return $tags;
            }
            $error = new WP_Error( 'invalid_email', __('<strong>ERROR</strong>: The email address is invalid according to our custom rules.', 'textdomain') );
            return $error;
        }
    }

    return $userdata;
}

// 自定义邮箱验证规则
function my_validate_email( $email ) {
    // 这里可以添加你自己的验证逻辑
    // 例如,检查邮箱是否包含特定的域名
    if ( strpos( $email, '@example.com' ) !== false ) {
        return true;
    } else {
        return false;
    }
}
?>
  • profile_update 这个钩子在用户资料更新后触发,可以用来执行一些后续操作,例如发送邮件通知、记录日志等。
<?php
add_action( 'profile_update', 'my_profile_update', 10, 2 );

function my_profile_update( $user_id, $old_user_data ) {
    // 获取新的用户数据
    $new_user_data = get_userdata( $user_id );

    // 比较新的和旧的用户数据,找出修改的字段
    $changed_fields = array();
    foreach ( (array) $new_user_data->data as $key => $value ) {
        if ( $value != $old_user_data->$key ) {
            $changed_fields[ $key ] = array(
                'old' => $old_user_data->$key,
                'new' => $value
            );
        }
    }

    // 如果有修改的字段,则发送邮件通知
    if ( ! empty( $changed_fields ) ) {
        $message = "用户ID: " . $user_id . "n";
        $message .= "以下字段被修改:n";
        foreach ( $changed_fields as $field => $values ) {
            $message .= "  " . $field . ": " . $values['old'] . " -> " . $values['new'] . "n";
        }

        wp_mail( '[email protected]', '用户资料更新通知', $message );
    }
}
?>

五、注意事项和常见问题

  • 必须包含 ID 字段: 这是 wp_update_user() 的前提条件,否则会报错。
  • 不要直接更新 user_loginuser_passuser_nicename 这些字段应该使用专门的函数来修改,例如 wp_update_user_login()wp_set_password()
  • 注意数据清洗和验证: 特别是 user_email 字段,要进行格式验证和重复性检查,防止出现脏数据。
  • 合理利用钩子: pre_user_updateprofile_update 钩子可以帮助我们实现自定义的逻辑,增强 wp_update_user() 的功能。
  • 角色权限问题: 更新用户角色时,需要确保当前用户有足够的权限。
  • 缓存问题: 修改用户信息后,注意清理缓存,避免显示旧的数据。

六、总结

wp_update_user() 函数是 WordPress 中更新用户数据的重要工具。通过深入了解它的源码和流程,我们可以更好地掌握用户数据更新的机制,避免踩坑,并利用钩子机制扩展其功能。希望今天的分享能帮助大家更好地理解和使用这个“老朋友”。

今天的讲座就到这里,谢谢大家! 如果有问题,欢迎提问。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注