分析 WordPress `get_option()` 和 `update_option()` 函数源码:如何处理单值与多值选项。

各位未来的WordPress大师们,晚上好!我是你们今晚的向导,代号“Option侦探”,很高兴能和大家一起破解WordPress选项系统的密码。今天,我们要聚焦两个关键函数:get_option()update_option(),看看它们是如何巧妙地处理单值和多值选项的。

准备好了吗?让我们开始这场代码探险!

第一幕:get_option() —— 选项侦查员

get_option(),顾名思义,负责从WordPress的选项数据库中检索数据。 它的核心任务是将存储的选项值取出来,然后还给我们,整个过程涉及到缓存读取和数据库查询。

首先,我们来看看get_option()的简化版源码(为了便于理解,我删除了部分不常用的参数和过滤器,只保留核心逻辑):

function get_option( $option, $default = false ) {
    global $wpdb, $wp_load_alloptions;

    // 1. 检查是否已经加载所有选项到缓存
    if ( ! isset( $wp_load_alloptions ) ) {
        wp_load_alloptions();
    }

    // 2. 检查缓存
    if ( isset( $wp_load_alloptions[ $option ] ) ) {
        return maybe_unserialize( $wp_load_alloptions[ $option ] );
    }

    // 3. 检查选项是否已加载到运行时缓存
    if ( isset( $GLOBALS['wp_options'][ $option ] ) ) {
        return $GLOBALS['wp_options'][ $option ];
    }

    // 4. 从数据库查询
    $row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $option ) );

    // 5. 处理结果
    if ( is_object( $row ) ) {
        $value = $row->option_value;
    } else {
        $value = $default; // 如果没找到,返回默认值
    }

    // 6. 反序列化
    $value = maybe_unserialize( $value );

    // 7. 添加到运行时缓存
    $GLOBALS['wp_options'][ $option ] = $value;

    return $value;
}

让我们逐行解读这段代码,就像侦探分析线索一样:

  1. 检查是否已加载所有选项: wp_load_alloptions() 会尝试将 wp_options 表中的所有选项加载到内存,以提高性能。 如果 wp_load_alloptions 未设置,则调用该函数。 这是一种缓存策略,避免频繁查询数据库。

  2. 检查缓存: WordPress 使用全局变量 $wp_load_alloptions$GLOBALS['wp_options'] 作为缓存。 首先,在 $wp_load_alloptions 中查找,如果找到,直接返回反序列化后的值。

  3. 检查运行时缓存: 如果 $wp_load_alloptions 没有找到,检查 $GLOBALS['wp_options']。 这是个运行时缓存,存储着当前请求周期内已经获取过的选项值。

  4. 从数据库查询: 如果缓存中没有找到,就只能硬着头皮去数据库里捞了。 使用 $wpdb->prepare() 来防止 SQL 注入,查询 wp_options 表中 option_name 等于 $option 的记录。

  5. 处理结果: 如果查询成功,将 option_value 赋值给 $value;否则,使用传入的 $default 值。

  6. 反序列化: 关键的一步! maybe_unserialize() 函数会判断 $value 是否需要反序列化。 如果 $value 是序列化后的字符串,maybe_unserialize() 会将其转换回 PHP 的数组或对象。 这正是 WordPress 能够存储复杂数据结构的关键。

  7. 添加到运行时缓存: 将查询到的值添加到 $GLOBALS['wp_options'],以便下次快速访问。

单值与多值的秘密

get_option() 本身并不区分单值和多值。它只是从数据库中取出 option_value 字段的值,然后通过 maybe_unserialize() 尝试将其反序列化。

  • 单值选项: 如果 option_value 存储的是一个简单的字符串、数字或布尔值,maybe_unserialize() 不会做任何处理,直接返回原始值。

  • 多值选项: 如果 option_value 存储的是一个序列化后的数组或对象,maybe_unserialize() 会将其反序列化为对应的 PHP 数据结构。 这意味着你可以用一个选项来存储整个配置数组,比如某个插件的所有设置。

例子:

// 存储一个简单的字符串(单值)
update_option( 'my_string_option', 'Hello, World!' );
$string_value = get_option( 'my_string_option' ); // $string_value = 'Hello, World!'

// 存储一个数组(多值)
$my_array = array(
    'key1' => 'value1',
    'key2' => 'value2',
);
update_option( 'my_array_option', $my_array );
$array_value = get_option( 'my_array_option' ); // $array_value = array( 'key1' => 'value1', 'key2' => 'value2' )

第二幕:update_option() —— 选项数据管理员

update_option() 负责更新或添加 wp_options 表中的选项。 它的核心任务是将数据序列化后,存入数据库。

同样,我们先看一个简化版的源码:

function update_option( $option, $value, $autoload = null ) {
    global $wpdb;

    // 1. 验证选项名
    if ( ! is_string( $option ) ) {
        return false;
    }

    // 2. 过滤新值
    $value = apply_filters( 'pre_update_option_' . $option, $value, $option );
    $old_value = get_option( $option );

    // 3. 过滤新值和旧值
    if ( $value === $old_value ) {
        return false; // 如果新值和旧值相同,则不更新
    }

    $value = apply_filters( 'option_' . $option, $value, $option, $old_value );

    // 4. 序列化
    $value = maybe_serialize( $value );

    // 5. 更新或插入数据库
    $now = time();

    $row = $wpdb->get_row( $wpdb->prepare( "SELECT option_id FROM $wpdb->options WHERE option_name = %s", $option ) );
    if ( is_object( $row ) ) {
        // 更新
        $option_id = $row->option_id;
        $result = $wpdb->update(
            $wpdb->options,
            array( 'option_value' => $value ),
            array( 'option_name' => $option )
        );
        if ( $result ) {
            do_action( 'updated_option', $option, $old_value, $value );
        }
    } else {
        // 插入
        $result = $wpdb->insert(
            $wpdb->options,
            array(
                'option_name'  => $option,
                'option_value' => $value,
                'autoload'     => ( 'no' === $autoload ) ? 'no' : 'yes',
            )
        );
        if ( $result ) {
            do_action( 'added_option', $option, $value );
        }
    }

    // 6. 清除缓存
    wp_cache_delete( 'alloptions', 'options' ); // 清除所有选项的缓存
    wp_cache_delete( $option, 'options' ); // 清除单个选项的缓存

    return true;
}

让我们分解一下这个函数:

  1. 验证选项名: 确保选项名是字符串,否则直接返回 false

  2. 过滤新值: 通过 apply_filters() 应用一系列过滤器,允许插件和主题修改要保存的值。 pre_update_option_{$option} 在真正更新数据库之前过滤,option_{$option} 允许你访问新旧值,并做更精细的控制。

  3. 过滤新值和旧值: 如果新值和旧值相同,为了避免不必要的数据库操作,直接返回 false

  4. 序列化: 关键的一步! maybe_serialize() 函数会判断 $value 是否需要序列化。 如果 $value 是一个数组或对象,maybe_serialize() 会将其转换为序列化后的字符串。 这确保了复杂的数据结构可以安全地存储到数据库中。

  5. 更新或插入数据库: 先查询数据库中是否存在该选项。 如果存在,则更新 option_value 字段;如果不存在,则插入一条新记录。 autoload 参数决定了该选项是否在 WordPress 启动时自动加载到内存中。

  6. 清除缓存: 更新或插入完成后,需要清除缓存,以确保下次调用 get_option() 时能获取到最新的值。 wp_cache_delete() 函数用于清除缓存。

单值与多值的存储

update_option() 的关键在于 maybe_serialize() 函数。

  • 单值选项: 如果 $value 是一个简单的字符串、数字或布尔值,maybe_serialize() 不会做任何处理,直接将原始值存储到数据库中。

  • 多值选项: 如果 $value 是一个数组或对象,maybe_serialize() 会将其序列化为一个字符串,然后再存储到数据库中。

例子:

// 存储一个简单的字符串(单值)
update_option( 'my_string_option', 'Hello, World!' ); // 数据库中 'option_value' 的值为 'Hello, World!'

// 存储一个数组(多值)
$my_array = array(
    'key1' => 'value1',
    'key2' => 'value2',
);
update_option( 'my_array_option', $my_array ); // 数据库中 'option_value' 的值为 'a:2:{s:4:"key1";s:6:"value1";s:4:"key2";s:6:"value2";}' (序列化后的字符串)

第三幕:maybe_serialize()maybe_unserialize() 的幕后工作

这两个函数是处理单值和多值选项的关键。 它们负责在 PHP 数据结构和序列化后的字符串之间进行转换。

function maybe_serialize( $data ) {
    if ( is_array( $data ) || is_object( $data ) ) {
        return serialize( $data );
    }

    // Double serialization is required for backward compatibility.
    if ( is_serialized( $data ) ) {
        return serialize( $data );
    }

    return $data;
}

function maybe_unserialize( $original ) {
    if ( is_serialized( $original ) ) { // don't attempt to unserialize data that wasn't serialized going in
        return @unserialize( $original );
    }

    return $original;
}

function is_serialized( $data, $strict = true ) {
    // If it isn't a string, it isn't serialized.
    if ( ! is_string( $data ) ) {
        return false;
    }
    $data = trim( $data );
    if ( 'N;' == $data ) {
        return true;
    }
    if ( strlen( $data ) < 4 ) {
        return false;
    }
    if ( ':' !== $data[1] ) {
        return false;
    }
    if ( $strict ) {
        $result = preg_match( '/^((s|i|b|d):[0-9]+:|a:[0-9]+:{)/', $data );
    } else {
        $result = preg_match( '/^(s|i|b|d|a):[0-9]+:/', $data );
    }
    if ( $result ) {
        return true;
    }
    return false;
}
  • maybe_serialize() 如果输入的数据是数组或对象,则使用 serialize() 函数将其序列化。 如果已经是序列化的字符串,则再次序列化(为了兼容性)。 否则,直接返回原始数据。

  • maybe_unserialize() 使用 is_serialized() 函数判断输入的数据是否是序列化后的字符串。 如果是,则使用 unserialize() 函数将其反序列化。 否则,直接返回原始数据。

  • is_serialized() 用于检测一个字符串是否是序列化后的数据。它通过检查字符串的格式来判断。

表格总结

为了更清晰地理解,我们用一个表格来总结 get_option()update_option() 如何处理单值和多值选项:

函数 输入数据类型 maybe_serialize() 行为 存储到数据库中的值 maybe_unserialize() 行为 返回的数据类型
update_option() 字符串、数字、布尔值 不处理 原始值 N/A N/A
update_option() 数组、对象 序列化 序列化后的字符串 N/A N/A
get_option() 字符串、数字、布尔值(从数据库读取) 不处理 原始值 不处理 字符串、数字、布尔值
get_option() 序列化后的字符串(从数据库读取) N/A 序列化后的字符串 反序列化 数组、对象

最佳实践与注意事项

  1. 谨慎使用 autoload autoload 参数决定了选项是否在 WordPress 启动时自动加载。 如果一个选项不经常使用,不要将其设置为 autoload,以减少内存消耗。

  2. 使用选项 Transients API 来缓存复杂数据: 对于计算量大的数据,或者需要频繁更新的数据,使用 Transients API 可以更有效地缓存数据,并设置过期时间。

  3. 避免存储过大的数据: wp_options 表不适合存储大量数据。 如果需要存储大量数据,可以考虑创建自定义表。

  4. 注意数据类型: 在存储数据时,要明确数据的类型,以便在获取数据时进行正确的处理。

  5. 防止SQL注入: 始终使用 $wpdb->prepare() 来构建查询,这可以帮助你防止 SQL 注入攻击。

  6. 善用过滤器: WordPress 提供了一系列的过滤器,允许你在选项被保存或检索之前对其进行修改。 这可以帮助你实现各种自定义功能,例如自动转换数据类型或添加安全检查。

总结陈词

get_option()update_option() 是 WordPress 选项系统的核心函数。 它们通过 maybe_serialize()maybe_unserialize() 函数,巧妙地处理了单值和多值选项的存储和检索。 理解这些函数的原理,可以帮助我们更好地使用 WordPress 的选项系统,开发出更高效、更灵活的插件和主题。

希望今天的讲座对大家有所帮助。 如果有任何问题,欢迎提问!

祝大家在 WordPress 的世界里玩得开心!

发表回复

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