分析 WordPress `update_option()` 函数的源码:插件如何存储配置数据。

各位观众老爷们,大家好!今天咱们来聊聊WordPress里一个非常重要,但又经常被咱们忽略的函数:update_option()。 别看它名字平平无奇,它可是WordPress插件存储配置数据的核心武器! 咱们今天就扒开它的源码,看看它是怎么工作的,插件作者们又是怎么利用它来保存各种乱七八糟的设置的。

开场白:为什么我们需要 update_option()?

想象一下,你开发了一个超级牛逼的WordPress插件,它可以让网站的访客在鼠标移动到图片上的时候,自动播放一段鬼畜的BGM(别问我为什么会有这种需求)。 那么问题来了,这个BGM的URL,音量大小,甚至是是否开启这个功能的开关,这些配置信息总得有个地方存起来吧?

如果每次都写死在代码里,那用户岂不是要哭死?每次想换个BGM都得改代码,重新上传插件?这简直是程序员的噩梦,用户的灾难!

所以,我们需要一个机制,能够让插件把这些配置信息持久化地存储起来,并且方便用户修改。 这时候,update_option() 就闪亮登场了!

update_option() 函数:简单易用,功能强大

update_option() 函数是WordPress提供的一个用于更新或添加选项值的函数。它的基本语法如下:

update_option( string $option, mixed $value, string|bool $autoload = null ): bool
  • $option:选项的名称,也就是你给这个配置项起的名字,比如 ‘my_plugin_bgm_url’。
  • $value:选项的值,可以是字符串,数字,数组,甚至对象!
  • $autoload:是否在WordPress启动时自动加载这个选项。 默认值是 null,表示如果选项不存在,则自动加载;如果选项已经存在,则保持原来的加载方式。 设置为 truefalse 可以强制开启或关闭自动加载。

返回值:如果更新成功,返回 true;如果更新失败,返回 false

源码剖析:update_option() 的内部运作

好了,光说不练假把式,咱们直接上源码,看看 update_option() 到底是怎么工作的。

首先,我们找到 wp-includes/option.php 文件,这就是 update_option() 函数的藏身之处。

function update_option( string $option, mixed $value, string|bool $autoload = null ): bool {
    global $wpdb;

    $option = trim( $option );
    if ( empty( $option ) ) {
        return false;
    }

    // Make sure the option name is valid.
    $option = sanitize_option( $option );

    $old_value = get_option( $option );

    /**
     * Filters a specific option value before it is updated.
     *
     * The dynamic portion of the hook name, `$option`, refers to the option name.
     *
     * @since 2.6.0
     *
     * @param mixed  $value     The new, unserialized option value.
     * @param mixed  $old_value The old option value.  It will be the same as
     *                          $value if the option does not exist yet.
     */
    $value = apply_filters( "pre_update_option_{$option}", $value, $old_value );

    /**
     * Filters an option value before it is updated.
     *
     * @since 2.3.0
     *
     * @param mixed  $value     The new, unserialized option value.
     * @param string $option    The name of the option to update.
     * @param mixed  $old_value The old option value.  It will be the same as
     *                          $value if the option does not exist yet.
     */
    $value = apply_filters( 'pre_update_option', $value, $option, $old_value );

    if ( is_object( $value ) ) {
        $value = clone $value;
    }

    $value = sanitize_option( $option, $value );

    if ( wp_maybe_serialize( $value ) === wp_maybe_serialize( $old_value ) ) {
        return false;
    }

    /**
     * Fires immediately before an option is updated.
     *
     * @since 2.8.0
     *
     * @param string $option    Name of the option to update.
     * @param mixed  $old_value The old option value.
     * @param mixed  $value     The new option value.
     */
    do_action( 'update_option', $option, $old_value, $value );

    $serialized_value = wp_maybe_serialize( $value );

    $autoload = ( null === $autoload ) ? 'yes' : ( $autoload ? 'yes' : 'no' );

    $data  = compact( 'option', 'serialized_value', 'autoload' );
    $where = array( 'option_name' => $option );

    $format = array( '%s', '%s', '%s' );
    $where_format = array( '%s' );

    if ( false === $old_value ) {
        /**
         * Fires before an option is added.
         *
         * @since 2.9.0
         *
         * @param string $option Name of the option to add.
         * @param mixed  $value  Value of the option.
         */
        do_action( 'add_option', $option, $value );

        $result = $wpdb->insert( $wpdb->options, $data, $format );
    } else {
        /**
         * Fires immediately before an existing option is updated.
         *
         * @since 2.8.0
         *
         * @param string $option    Name of the option to update.
         * @param mixed  $old_value The old option value.
         * @param mixed  $value     The new option value.
         */
        do_action( 'pre_update_existing_option', $option, $value, $old_value );
        $result = $wpdb->update( $wpdb->options, $data, $where, $format, $where_format );

        /**
         * Fires immediately after an existing option is updated.
         *
         * @since 2.8.0
         *
         * @param string $option    Name of the option to update.
         * @param mixed  $old_value The old option value.
         * @param mixed  $value     The new option value.
         */
        do_action( 'updated_option', $option, $old_value, $value );
    }

    if ( ! $result ) {
        return false;
    }

    $notoptions = wp_cache_get( 'notoptions', 'options' );
    if ( is_array( $notoptions ) && isset( $notoptions[ $option ] ) ) {
        unset( $notoptions[ $option ] );
        wp_cache_set( 'notoptions', $notoptions, 'options' );
    }

    wp_cache_delete( $option, 'options' );

    /**
     * Fires after an option is updated.
     *
     * @since 2.8.0
     *
     * @param string $option    Name of the option updated.
     * @param mixed  $old_value The old option value.
     * @param mixed  $value     The new option value.
     */
    do_action( 'updated_option', $option, $old_value, $value );

    return true;
}

代码看起来有点长,但别怕,咱们一点一点地分析:

  1. 参数校验与清理:

    $option = trim( $option );
    if ( empty( $option ) ) {
        return false;
    }
    $option = sanitize_option( $option );

    首先,它会去掉 option 名称两边的空格,并且检查是否为空。 如果为空,直接返回 false,说明更新失败。然后,它还会使用 sanitize_option() 函数对 option 名称进行过滤,防止出现一些奇怪的字符,保证安全性。

  2. 获取旧值:

    $old_value = get_option( $option );

    接下来,它会使用 get_option() 函数获取 option 对应的旧值。 为什么要获取旧值呢? 后面会讲到,主要是为了判断新值和旧值是否相同,如果相同,就没必要更新了。

  3. Filter Hook(过滤器钩子):

    $value = apply_filters( "pre_update_option_{$option}", $value, $old_value );
    $value = apply_filters( 'pre_update_option', $value, $option, $old_value );

    这里出现了两个 filter hook。 WordPress的钩子机制允许插件或主题在特定的时间点修改数据或执行自定义操作。apply_filters 是一个应用过滤器钩子的函数。

    • pre_update_option_{$option}: 这是一个动态的过滤器钩子,{$option} 会被替换成具体的选项名称。 插件可以使用这个钩子在特定的选项更新之前修改其值。
    • pre_update_option:这是一个通用的过滤器钩子,插件可以使用这个钩子在任何选项更新之前修改其值。

    通过这些钩子,其他插件或主题可以在 update_option() 函数执行之前,对即将保存的选项值进行修改。 这种机制非常灵活,可以实现很多高级的功能。

  4. 对象克隆与再次Sanitize:

    if ( is_object( $value ) ) {
        $value = clone $value;
    }
    
    $value = sanitize_option( $option, $value );

    如果 $value 是一个对象,那么会先克隆一个副本,避免直接修改原始对象。 之后,它会再次使用 sanitize_option() 函数对 $value 进行过滤,确保数据的安全性。 这次的 sanitize_option() 函数接收两个参数,第一个是 option 名称,第二个是 $value, 这样可以根据不同的 option 名称,使用不同的过滤规则。

  5. 判断新旧值是否相同:

    if ( wp_maybe_serialize( $value ) === wp_maybe_serialize( $old_value ) ) {
        return false;
    }

    这里使用 wp_maybe_serialize() 函数对新值和旧值进行序列化,然后比较它们是否相同。 如果相同,说明没有变化,直接返回 false,避免不必要的数据库操作。

  6. Action Hook(动作钩子):

    do_action( 'update_option', $option, $old_value, $value );

    这里又出现了一个 action hook。 WordPress的钩子机制允许插件或主题在特定的时间点修改数据或执行自定义操作。do_action 是一个执行动作钩子的函数。

    update_option:这是一个动作钩子,插件可以使用这个钩子在选项更新之前执行一些自定义操作。

  7. 序列化数据:

    $serialized_value = wp_maybe_serialize( $value );

    这里使用 wp_maybe_serialize() 函数对 $value 进行序列化。 如果 $value 是一个数组或对象,那么会被序列化成字符串,方便存储到数据库中。 如果 $value 是一个字符串或数字,那么会直接返回,不做任何处理。

  8. 确定是否自动加载:

    $autoload = ( null === $autoload ) ? 'yes' : ( $autoload ? 'yes' : 'no' );

    这里根据传入的 $autoload 参数,确定是否自动加载这个选项。 如果 $autoloadnull,则默认为 yes; 如果 $autoloadtrue,则设置为 yes; 如果 $autoloadfalse,则设置为 no

  9. 构建数据和查询条件:

    $data  = compact( 'option', 'serialized_value', 'autoload' );
    $where = array( 'option_name' => $option );
    
    $format = array( '%s', '%s', '%s' );
    $where_format = array( '%s' );

    这里构建了要插入或更新的数据和查询条件。 $data 数组包含了 option 名称,序列化后的值,以及是否自动加载的信息。 $where 数组包含了查询条件,用于定位要更新的记录。 $format$where_format 数组定义了数据的格式,用于防止SQL注入。

  10. 插入或更新数据:

    if ( false === $old_value ) {
        do_action( 'add_option', $option, $value );
        $result = $wpdb->insert( $wpdb->options, $data, $format );
    } else {
        do_action( 'pre_update_existing_option', $option, $value, $old_value );
        $result = $wpdb->update( $wpdb->options, $data, $where, $format, $where_format );
        do_action( 'updated_option', $option, $old_value, $value );
    }

    这里根据 $old_value 是否为 false,判断是插入还是更新数据。 如果 $old_valuefalse,说明这个 option 之前不存在,那么就执行插入操作,使用 $wpdb->insert() 函数将数据插入到 wp_options 表中。 如果 $old_value 不为 false,说明这个 option 之前已经存在,那么就执行更新操作,使用 $wpdb->update() 函数更新 wp_options 表中的数据。

  11. 清理缓存:

    $notoptions = wp_cache_get( 'notoptions', 'options' );
    if ( is_array( $notoptions ) && isset( $notoptions[ $option ] ) ) {
        unset( $notoptions[ $option ] );
        wp_cache_set( 'notoptions', $notoptions, 'options' );
    }
    
    wp_cache_delete( $option, 'options' );

    这里清理了缓存,保证数据的及时更新。 WordPress 使用缓存来提高性能,所以当数据发生变化时,需要及时清理缓存,避免出现数据不一致的情况。

  12. 再次触发动作钩子:

    do_action( 'updated_option', $option, $old_value, $value );

    这里再次触发了 updated_option 动作钩子,插件可以使用这个钩子在选项更新之后执行一些自定义操作。

  13. 返回结果:

    return true;

    如果一切顺利,返回 true,表示更新成功。

wp_options 表:存储配置数据的家

update_option() 函数最终会将数据存储到 WordPress 的 wp_options 表中。 咱们来看看这个表的结构:

字段名 数据类型 说明
option_id bigint(20) 选项ID,自增长
option_name varchar(191) 选项名称,也就是我们传入的 $option 参数
option_value longtext 选项值,也就是我们传入的 $value 参数序列化后的字符串
autoload varchar(20) 是否自动加载,取值为 ‘yes’ 或 ‘no’

可以看到,option_name 字段存储的是选项的名称,option_value 字段存储的是选项的值(序列化后的字符串)。

插件开发者如何使用 update_option()?

现在,咱们来看看插件开发者是如何使用 update_option() 函数来存储配置数据的。

<?php
/**
 * Plugin Name: My Awesome Plugin
 * Description: A plugin that plays BGM on image hover.
 * Version: 1.0.0
 */

// 添加一个管理菜单
add_action( 'admin_menu', 'my_plugin_add_menu' );

function my_plugin_add_menu() {
    add_options_page(
        'My Plugin Settings', // 页面标题
        'My Plugin', // 菜单标题
        'manage_options', // 权限要求
        'my-plugin-settings', // 菜单slug
        'my_plugin_settings_page' // 页面内容回调函数
    );
}

// 页面内容回调函数
function my_plugin_settings_page() {
    // 处理表单提交
    if ( isset( $_POST['submit'] ) ) {
        update_option( 'my_plugin_bgm_url', sanitize_text_field( $_POST['bgm_url'] ) );
        update_option( 'my_plugin_bgm_volume', floatval( $_POST['bgm_volume'] ) );
        update_option( 'my_plugin_enabled', isset( $_POST['enabled'] ) ? true : false );

        echo '<div class="updated"><p>Settings saved.</p></div>';
    }

    // 获取选项值
    $bgm_url = get_option( 'my_plugin_bgm_url', 'https://example.com/default_bgm.mp3' );
    $bgm_volume = get_option( 'my_plugin_bgm_volume', 0.5 );
    $enabled = get_option( 'my_plugin_enabled', true );

    // 输出表单
    ?>
    <div class="wrap">
        <h1>My Plugin Settings</h1>
        <form method="post" action="">
            <table class="form-table">
                <tr valign="top">
                    <th scope="row">BGM URL</th>
                    <td>
                        <input type="text" name="bgm_url" value="<?php echo esc_attr( $bgm_url ); ?>" class="regular-text">
                    </td>
                </tr>
                <tr valign="top">
                    <th scope="row">BGM Volume</th>
                    <td>
                        <input type="number" name="bgm_volume" value="<?php echo esc_attr( $bgm_volume ); ?>" step="0.1" min="0" max="1">
                    </td>
                </tr>
                <tr valign="top">
                    <th scope="row">Enable Plugin</th>
                    <td>
                        <label>
                            <input type="checkbox" name="enabled" <?php checked( $enabled, true ); ?>>
                        </label>
                    </td>
                </tr>
            </table>
            <?php submit_button(); ?>
        </form>
    </div>
    <?php
}

// 在图片hover时播放BGM
add_action( 'wp_footer', 'my_plugin_add_bgm_script' );

function my_plugin_add_bgm_script() {
    if ( ! get_option( 'my_plugin_enabled', true ) ) {
        return;
    }

    $bgm_url = get_option( 'my_plugin_bgm_url', 'https://example.com/default_bgm.mp3' );
    $bgm_volume = get_option( 'my_plugin_bgm_volume', 0.5 );

    ?>
    <script>
        jQuery(document).ready(function($) {
            $('img').hover(function() {
                var audio = new Audio('<?php echo esc_url( $bgm_url ); ?>');
                audio.volume = <?php echo esc_attr( $bgm_volume ); ?>;
                audio.play();
            });
        });
    </script>
    <?php
}

这个插件做了以下几件事:

  1. 添加管理菜单: 它使用 add_options_page() 函数在 WordPress 后台的设置菜单下添加了一个名为 "My Plugin" 的子菜单。
  2. 创建设置页面: my_plugin_settings_page() 函数定义了设置页面的内容。 它包含一个表单,允许用户设置 BGM 的 URL,音量大小,以及是否启用插件。
  3. 处理表单提交: 当用户提交表单时,它使用 update_option() 函数将用户输入的值保存到 wp_options 表中。 注意,这里使用了 sanitize_text_field()floatval() 函数对用户输入进行过滤,防止 XSS 攻击。
  4. 在图片hover时播放BGM: my_plugin_add_bgm_script() 函数在页面的 <footer> 中添加了一段 JavaScript 代码。 这段代码会在鼠标移动到图片上时,播放指定的 BGM。它使用 get_option() 函数获取之前保存的 BGM URL 和音量大小。

最佳实践:使用 update_option() 的注意事项

  • Option 名称要唯一: 为了避免与其他插件或主题冲突,Option 名称应该具有唯一性。 建议在 Option 名称前加上插件或主题的名称作为前缀,比如 my_plugin_bgm_url
  • 数据过滤要做好: 在将用户输入的数据保存到 wp_options 表之前,一定要进行过滤,防止 XSS 攻击。 可以使用 WordPress 提供的 sanitize_text_field()esc_url_raw()intval() 等函数进行过滤。
  • 不要存储大量数据: wp_options 表主要用于存储配置数据,不适合存储大量数据。 如果需要存储大量数据,建议创建自定义的数据库表。
  • 合理使用 autoload: 如果某个 Option 在网站的每个页面都需要用到,那么可以将其设置为自动加载。 否则,建议关闭自动加载,以提高网站的性能。
  • 及时清理不再使用的 Option: 如果某个插件被卸载了,那么应该及时清理其创建的 Option,避免 wp_options 表变得臃肿。

总结:update_option() 是个好同志

update_option() 函数是 WordPress 插件开发中不可或缺的一部分。 它可以让我们方便地存储和管理插件的配置数据,提高插件的可用性和用户体验。 只要我们掌握了它的原理和使用方法,并且注意一些最佳实践,就能更好地利用它来开发出优秀的 WordPress 插件。

好了,今天的讲座就到这里。 希望大家有所收获,下次再见!

发表回复

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