剖析 WordPress `get_site_option()` 和 `update_site_option()` 函数的源码:如何在网络级别存储和检索选项。

各位朋友,大家好!今天咱们来聊聊WordPress里两个神神秘秘的函数:get_site_option()update_site_option()。 它们专门负责在网络级别(也就是多站点环境)存储和读取选项。 啥是网络级别?简单来说,就是整个WordPress站点群共享的一些设置。

咱们的目标是:扒开这两个函数的源码,看看它们是怎么在数据库里藏猫腻,又是怎么把那些设置给揪出来的。

一、热身:单站点和多站点的区别

在深入源码之前,先稍微复习一下单站点和多站点之间的区别。单站点嘛,就是一个网站对应一个数据库。多站点,就是一个WordPress安装,能跑多个网站,它们共享一部分数据库,但也有各自独立的部分。

这就引出了一个问题:有些设置,是所有站点都一样的(比如SMTP服务器),有些设置是每个站点独有的(比如网站名称)。get_option()update_option() 这对好基友,负责处理单站点的选项。而今天要讲的 get_site_option()update_site_option(),就是专门为多站点设计的,管理那些网络级别的选项。

二、get_site_option():寻宝猎人

好,咱们先从 get_site_option() 开始。想象一下,你要在一座大房子里找一件宝贝,你知道这宝贝不在每个房间里,而是在一个特殊的储藏室里。get_site_option() 就扮演了这个寻宝猎人的角色。

先来看看它的基本用法:

$my_network_option = get_site_option( 'my_network_setting', 'default_value' );

if ( $my_network_option ) {
  echo "Network setting is: " . $my_network_option;
} else {
  echo "Network setting not found, using default value.";
}

这段代码的意思是:尝试获取名为 my_network_setting 的网络选项。 如果找到了,就显示它的值。 否则,就使用默认值 default_value

现在,让我们深入到 get_site_option() 的源码里,看看它是怎么找到宝贝的。

function get_site_option( $option, $default = false ) {
    global $wpdb, $blog_id, $_wp_switched_stack;

    // 处理非字符串的选项名称
    if ( ! is_scalar( $option ) ) {
        return false;
    }

    // 尝试从缓存中获取
    $value = wp_cache_get( $option, 'site-options' );

    if ( false !== $value ) {
        /**
         * Filters the value of an existing site option.
         *
         * The dynamic portion of the hook name, `$option`, refers to the site option name.
         *
         * @since 2.9.0
         *
         * @param mixed $value The value of the site option.
         */
        return apply_filters( "pre_site_option_{$option}", $value );
    }

    // 获取网络ID
    $network_id = get_current_network_id();

    // 构造查询语句
    $sql = $wpdb->prepare( "SELECT option_value FROM {$wpdb->sitemeta} WHERE site_id = %d AND meta_key = %s", $network_id, $option );

    // 执行查询
    $value = $wpdb->get_var( $sql );

    // 如果没有找到,就返回默认值
    if ( is_null( $value ) ) {
        return $default;
    }

    // 反序列化数据
    $value = maybe_unserialize( $value );

    // 将结果存入缓存
    wp_cache_set( $option, $value, 'site-options' );

    /** This filter is documented in wp-includes/option.php */
    return apply_filters( "pre_site_option_{$option}", $value );
}

来,咱们一行一行地解读:

  1. global $wpdb, $blog_id, $_wp_switched_stack;: 这行代码声明了几个全局变量。 $wpdb 是WordPress的数据库对象, $blog_id 是当前站点的ID(在多站点环境下), $_wp_switched_stack 用于记录站点切换的状态(后面会提到)。

  2. if ( ! is_scalar( $option ) ) { return false; }: 检查选项名称是否是标量值(比如字符串、数字)。如果不是,就直接返回 false。这是一种安全措施,防止出现意外错误。

  3. $value = wp_cache_get( $option, 'site-options' );: 首先,尝试从缓存中获取选项值。wp_cache_get() 是 WordPress 的缓存函数,它可以提高性能,避免每次都查询数据库。site-options 是缓存组的名称,用于将网络选项与其他选项区分开。

  4. if ( false !== $value ) { ... }: 如果缓存中找到了选项值,就用 apply_filters() 应用一个过滤器,然后直接返回。这个过滤器允许开发者在选项值返回之前修改它。 注意 pre_site_option_{$option} 这种动态的过滤器命名方式,非常灵活。

  5. $network_id = get_current_network_id();: 如果缓存中没有找到,就调用 get_current_network_id() 函数获取当前的网络ID。 这个函数通常返回主站点的 ID(也就是网络管理员站点)。在不同的多站点配置中,这个ID可能会变化,但通常是1。

  6. $sql = $wpdb->prepare( "SELECT option_value FROM {$wpdb->sitemeta} WHERE site_id = %d AND meta_key = %s", $network_id, $option );: 构造 SQL 查询语句。注意这里使用了 $wpdb->prepare() 函数,它可以防止 SQL 注入攻击。 查询的目标是 $wpdb->sitemeta 表,这个表专门用于存储网络级别的元数据。site_id 列存储网络ID, meta_key 列存储选项名称,option_value 列存储选项值。

  7. $value = $wpdb->get_var( $sql );: 执行 SQL 查询,并将结果保存在 $value 变量中。 $wpdb->get_var() 函数只返回查询结果的第一行第一列的值。

  8. if ( is_null( $value ) ) { return $default; }: 如果查询结果是 null,说明数据库中没有找到对应的选项,就返回默认值。

  9. $value = maybe_unserialize( $value );: 如果找到了选项值,就使用 maybe_unserialize() 函数尝试反序列化它。 这是因为有些选项值可能是一个数组或对象,需要先序列化成字符串才能存储在数据库中。

  10. wp_cache_set( $option, $value, 'site-options' );: 将选项值存入缓存,以便下次更快地获取。

  11. return apply_filters( "pre_site_option_{$option}", $value );: 再次应用过滤器,然后返回选项值。

重点总结:

  • get_site_option() 首先尝试从缓存中获取选项值。
  • 如果缓存中没有找到,就查询 wp_sitemeta 表。
  • 查询时使用网络ID和选项名称作为条件。
  • 找到选项值后,先反序列化,然后存入缓存。
  • 使用 pre_site_option_{$option} 过滤器允许开发者修改选项值。

三、update_site_option():藏宝大师

现在,让我们来看看 update_site_option() 函数。它就像一个藏宝大师,负责把我们的宝贝藏到那个特殊的储藏室里。

先来看看它的基本用法:

$result = update_site_option( 'my_network_setting', 'new_value' );

if ( $result ) {
  echo "Network setting updated successfully.";
} else {
  echo "Failed to update network setting.";
}

这段代码的意思是:将名为 my_network_setting 的网络选项的值更新为 new_value。如果更新成功,就返回 true,否则返回 false

接下来,让我们深入到 update_site_option() 的源码里,看看它是怎么藏宝贝的。

function update_site_option( $option, $value ) {
    global $wpdb;

    // 处理非字符串的选项名称
    if ( ! is_scalar( $option ) ) {
        return false;
    }

    // 获取网络ID
    $network_id = get_current_network_id();

    // 序列化数据
    $value = sanitize_option( $option, $value );
    $value = maybe_serialize( $value );

    // 构造查询语句,尝试查找是否存在
    $old_value = get_site_option( $option );

    // 如果新旧值一样,直接返回
    if ( $value === $old_value ) {
        return false;
    }

    /**
     * Fires before a specific site option is updated.
     *
     * The dynamic portion of the hook name, `$option`, refers to the site option name.
     *
     * @since 2.9.0
     *
     * @param string $option Name of the site option to update.
     * @param mixed  $value  New value of the site option.
     */
    do_action( "update_site_option_{$option}", $option, $value );

    // 构造更新/插入语句
    $sql = $wpdb->prepare( "SELECT meta_id FROM {$wpdb->sitemeta} WHERE site_id = %d AND meta_key = %s", $network_id, $option );
    $meta_id = $wpdb->get_var( $sql );

    if ( $meta_id ) {
        // 更新操作
        $result = $wpdb->update(
            $wpdb->sitemeta,
            array( 'meta_value' => $value ),
            array( 'meta_id' => $meta_id ),
            array( '%s' ),
            array( '%d' )
        );
    } else {
        // 插入操作
        $result = $wpdb->insert(
            $wpdb->sitemeta,
            array(
                'site_id'    => $network_id,
                'meta_key'   => $option,
                'meta_value' => $value,
            ),
            array( '%d', '%s', '%s' )
        );
    }

    // 清除缓存
    wp_cache_delete( $option, 'site-options' );

    /**
     * Fires after a specific site option is updated.
     *
     * The dynamic portion of the hook name, `$option`, refers to the site option name.
     *
     * @since 2.9.0
     *
     * @param string $option     Name of the site option to update.
     * @param mixed  $old_value  The old value of the site option.
     * @param mixed  $value      The new value of the site option.
     */
    do_action( "updated_site_option_{$option}", $option, $old_value, $value );

    return (bool) $result;
}

咱们也来一行一行地解读:

  1. global $wpdb;: 声明了全局变量 $wpdb,也就是WordPress的数据库对象。

  2. if ( ! is_scalar( $option ) ) { return false; }: 检查选项名称是否是标量值。如果不是,就直接返回 false

  3. $network_id = get_current_network_id();: 获取当前的网络ID。

  4. $value = sanitize_option( $option, $value );: 使用 sanitize_option() 函数对选项值进行清理。 这个函数会根据选项名称,应用不同的过滤规则,防止恶意数据进入数据库。

  5. $value = maybe_serialize( $value );: 使用 maybe_serialize() 函数尝试序列化选项值。如果选项值是一个数组或对象,就需要先序列化成字符串才能存储在数据库中。

  6. $old_value = get_site_option( $option );: 获取选项的旧值。 这样做是为了判断新值和旧值是否相同。

  7. if ( $value === $old_value ) { return false; }: 如果新值和旧值相同,就直接返回 false。 避免不必要的数据库操作。 注意这里是严格比较 ===,类型和值都要相等。

  8. do_action( "update_site_option_{$option}", $option, $value );: 在更新选项之前,触发一个动作(action)。 允许开发者在更新选项之前执行一些自定义操作。

  9. $sql = $wpdb->prepare( "SELECT meta_id FROM {$wpdb->sitemeta} WHERE site_id = %d AND meta_key = %s", $network_id, $option );: 构造 SQL 查询语句,用于查找数据库中是否已经存在对应的选项。

  10. $meta_id = $wpdb->get_var( $sql );: 执行 SQL 查询,并将结果保存在 $meta_id 变量中。 $meta_idwp_sitemeta 表中主键ID,如果存在,就说明需要更新选项,否则需要插入新选项。

  11. if ( $meta_id ) { ... } else { ... }: 根据 $meta_id 的值,判断是更新选项还是插入新选项。

    • 更新操作: 使用 $wpdb->update() 函数更新 wp_sitemeta 表中的 meta_value 列。
    • 插入操作: 使用 $wpdb->insert() 函数向 wp_sitemeta 表中插入一条新记录。
  12. wp_cache_delete( $option, 'site-options' );: 清除缓存中对应的选项。 这样可以确保下次获取选项时,能够获取到最新的值。

  13. do_action( "updated_site_option_{$option}", $option, $old_value, $value );: 在更新选项之后,触发一个动作。 允许开发者在更新选项之后执行一些自定义操作。

  14. return (bool) $result;: 返回更新/插入操作的结果。

重点总结:

  • update_site_option() 首先对选项值进行清理和序列化。
  • 然后,判断新值和旧值是否相同。如果相同,就直接返回。
  • 接着,查询 wp_sitemeta 表,判断是更新选项还是插入新选项。
  • 更新或插入操作完成后,清除缓存。
  • 使用 update_site_option_{$option}updated_site_option_{$option} 动作允许开发者在更新选项前后执行自定义操作。

四、wp_sitemeta 表:藏宝阁

说了这么多,咱们得看看宝贝们到底藏在哪儿了。 所有网络级别的选项,都存储在 wp_sitemeta 表里。 这个表的结构大致如下:

列名 数据类型 说明
meta_id bigint(20) 主键,自增长
site_id bigint(20) 网络ID,通常是主站点的ID(1)
meta_key varchar(255) 选项名称
meta_value longtext 选项值,可以是字符串、数字、数组或对象的序列化字符串

五、一些细节和注意事项

  • 安全: update_site_option() 函数使用了 sanitize_option() 函数对选项值进行清理,防止恶意数据进入数据库。 咱们自己在开发插件或主题时,也要注意对用户输入进行验证和过滤,避免安全问题。
  • 缓存: get_site_option()update_site_option() 函数都使用了 WordPress 的缓存机制,提高性能。 如果你修改了数据库中的 wp_sitemeta 表,记得清除缓存,否则可能无法获取到最新的值。 可以使用 wp_cache_delete() 函数清除缓存。
  • 过滤器和动作: get_site_option() 函数使用了 pre_site_option_{$option} 过滤器, update_site_option() 函数使用了 update_site_option_{$option}updated_site_option_{$option} 动作。 这些过滤器和动作允许开发者在获取和更新选项前后执行自定义操作,非常灵活。
  • 多站点切换: 在多站点环境下,有时需要切换到不同的站点。 WordPress 提供了 switch_to_blog()restore_current_blog() 函数来实现站点切换。 但是,在切换站点之后,get_site_option() 函数仍然会获取网络级别的选项,而不是当前站点的选项。 如果你需要获取当前站点的选项,应该使用 get_option() 函数。

六、实战演练

假设我们要开发一个插件,用于设置整个网络的主题颜色。 我们可以使用 get_site_option()update_site_option() 函数来实现这个功能。

// 获取主题颜色
function my_plugin_get_theme_color() {
  return get_site_option( 'my_plugin_theme_color', '#ffffff' ); // 默认白色
}

// 更新主题颜色
function my_plugin_update_theme_color( $color ) {
  update_site_option( 'my_plugin_theme_color', $color );
}

// 在主题中应用主题颜色
add_action( 'wp_head', function() {
  $theme_color = my_plugin_get_theme_color();
  echo '<style>body { background-color: ' . esc_attr( $theme_color ) . '; }</style>';
});

// 创建一个后台设置页面,允许管理员修改主题颜色 (这里省略了后台设置页面的代码)
// ...

这段代码定义了两个函数:my_plugin_get_theme_color() 用于获取主题颜色,my_plugin_update_theme_color() 用于更新主题颜色。 然后,使用 wp_head 动作,在主题的 <head> 标签中添加一个 <style> 标签,将主题颜色应用到 body 元素的 background-color 属性上。 最后,创建了一个后台设置页面,允许管理员修改主题颜色。

总结

今天咱们一起扒了 get_site_option()update_site_option() 这两个函数的源码,了解了它们是如何在网络级别存储和检索选项的。 掌握了这些知识,你就能更好地开发多站点插件和主题,为你的用户提供更强大的功能。 记住,安全第一,缓存要清理,过滤器和动作要用好! 好了,今天的讲座就到这里,感谢大家!

发表回复

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