详细阐述 `add_filter` 和 `apply_filters` 的源码,它们是如何实现过滤器(Filter)机制的?

各位程序猿/媛,欢迎来到今天的“Filter滤镜魔法:add_filterapply_filters 深度解析”讲座!今天,咱们要一起揭开WordPress过滤器机制的神秘面纱,看看 add_filterapply_filters 这对黄金搭档是如何协同工作,让我们的代码像加了滤镜一样,瞬间变得更加灵活、可定制的。

一、 过滤器:代码界的“变形金刚”

什么是过滤器?简单来说,它就像代码界的“变形金刚”,允许我们在特定的时候修改数据。想象一下,你正在处理一篇博客文章的内容,你可能希望在显示之前:

  • 替换某些敏感词汇
  • 添加一些广告
  • 自动链接一些关键词

如果没有过滤器,你就需要在每个需要修改内容的地方都写一遍代码。这不仅繁琐,而且难以维护。但有了过滤器,你只需要注册一个“滤镜”,告诉WordPress在文章内容显示之前,先经过你的“滤镜”处理一下,一切就变得轻松多了!

二、 add_filter:注册你的“滤镜”

add_filter 函数负责注册我们的“滤镜”。它的语法如下:

add_filter( string $tag, callable $function_to_add, int $priority = 10, int $accepted_args = 1 ): bool
  • $tag: 字符串,过滤器的名称。这是个关键,apply_filters 会根据这个名称来找到对应的过滤器。可以把它想象成一个“频道”,不同的“频道”对应不同的过滤操作。
  • $function_to_add: callable 类型,也就是一个可调用的函数或方法。这就是我们实际的“滤镜”,它负责接收数据,进行处理,然后返回修改后的数据。
  • $priority: 整数,可选参数,表示过滤器的优先级。数值越小,优先级越高,意味着越早被执行。默认值是 10。如果多个过滤器都注册在同一个 $tag 上,优先级高的会先执行。
  • $accepted_args: 整数,可选参数,表示过滤器函数接收的参数个数。默认值是 1。这个很重要,如果你的过滤器函数需要接收多个参数,一定要设置正确。

add_filter 源码剖析:

为了更好地理解 add_filter 的工作原理,我们来简化一下它的源码,剔除一些错误处理和兼容性代码,保留其核心逻辑:

// 简化版的 add_filter
function my_add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
    global $wp_filter, $merged_filters, $wp_current_filter;

    // 将过滤器函数信息存储到 $wp_filter 数组中
    $idx = _wp_filter_build_unique_id( $tag, $function_to_add, $priority );

    $wp_filter[ $tag ][ $priority ][ $idx ] = array(
        'function' => $function_to_add,
        'accepted_args' => $accepted_args
    );

    // 对优先级进行排序,保证执行顺序
    ksort( $wp_filter[ $tag ] );

    unset( $merged_filters[ $tag ] ); // 清除缓存,确保过滤器重新合并

    return true;
}

//辅助函数,生成唯一ID
function _wp_filter_build_unique_id( $tag, $function, $priority ) {
    static $filter_id_count = 0;

    if ( is_string( $function ) ) {
        return $function;
    }

    if ( is_object( $function ) ) {
        // Closures are currently implemented as objects
        $function = array( $function, '' );
    } else {
        $function = (array) $function;
    }

    if ( is_object( $function[0] ) ) {
        return spl_object_hash( $function[0] ) . $function[1];
    } elseif ( is_string( $function[0] ) ) {
        return $function[0] . '::' . $function[1];
    }
    return false;
}

这段代码做了什么?

  1. $wp_filter: 这是一个全局数组,用于存储所有注册的过滤器信息。它的结构是这样的:

    $wp_filter = array(
        'filter_tag' => array( // 过滤器名称
            priority => array( // 优先级
                'unique_id' => array( // 唯一ID,用于区分不同的过滤器函数
                    'function' => 'callable', // 过滤器函数
                    'accepted_args' => int // 接收的参数个数
                )
            )
        )
    );
  2. _wp_filter_build_unique_id: 这个函数负责生成一个唯一的 ID,用于区分不同的过滤器函数。它会根据函数名、类名、对象等信息生成一个字符串。

  3. 存储过滤器信息: 将过滤器函数、优先级、接收参数个数等信息存储到 $wp_filter 数组中。

  4. 优先级排序: 对同一个 $tag 下的过滤器按照优先级进行排序,确保执行顺序。

例子:

// 定义一个过滤器函数,将内容中的 "WordPress" 替换为 "WP"
function my_filter_wordpress( $content ) {
    return str_replace( 'WordPress', 'WP', $content );
}

// 注册这个过滤器,优先级为 10
add_filter( 'the_content', 'my_filter_wordpress', 10 );

// 定义另一个过滤器函数,在内容末尾添加版权信息
function my_filter_copyright( $content ) {
    return $content . '<p>Copyright © 2023</p>';
}

// 注册这个过滤器,优先级为 20,比上面的过滤器晚执行
add_filter( 'the_content', 'my_filter_copyright', 20 );

在这个例子中,我们注册了两个过滤器,都作用于 'the_content' 这个 $tag 上。my_filter_wordpress 的优先级更高,所以它会先执行,将 "WordPress" 替换为 "WP",然后再执行 my_filter_copyright,添加版权信息。

三、 apply_filters:触发“滤镜”生效

apply_filters 函数负责触发过滤器生效。它的语法如下:

apply_filters( string $tag, mixed $value, mixed ...$args ): mixed
  • $tag: 字符串,过滤器的名称,必须与 add_filter 中注册的 $tag 相同。
  • $value: 混合类型,需要被过滤的值。这就是原始数据,会被传递给过滤器函数进行处理。
  • ...$args: 可选参数,额外的参数,会被传递给过滤器函数。

apply_filters 源码剖析:

同样,我们来简化一下 apply_filters 的源码:

// 简化版的 apply_filters
function my_apply_filters( $tag, $value, ...$args ) {
    global $wp_filter, $merged_filters, $wp_current_filter;

    $wp_current_filter[] = $tag;

    // 确保过滤器已经合并
    if ( ! isset( $merged_filters[ $tag ] ) ) {
        $wp_filter[ $tag ] = _wp_filter_build_unique_id( $tag, '', 0 );
        $merged_filters[ $tag ] = true;
    }

    // 如果没有注册任何过滤器,直接返回原始值
    if ( ! isset( $wp_filter[ $tag ] ) ) {
        array_pop( $wp_current_filter );
        return $value;
    }

    // 循环遍历所有注册的过滤器,并依次执行
    $priority_arr = $wp_filter[ $tag ];
    ksort( $priority_arr, SORT_NUMERIC );

    foreach ( $priority_arr as $priority => $functions ) {
        foreach ( $functions as $function ) {
            // 构造参数数组
            $all_args = array_merge( array( $value ), $args );
            $num_args = $function['accepted_args'];

            // 截取需要的参数
            $args_to_pass = array_slice( $all_args, 0, $num_args );

            // 调用过滤器函数
            $value = call_user_func_array( $function['function'], $args_to_pass );
        }
    }

    array_pop( $wp_current_filter );

    return $value;
}

这段代码做了什么?

  1. $wp_current_filter: 这是一个全局数组,用于记录当前正在执行的过滤器。用于检测循环调用。

  2. 检查过滤器是否存在: 判断是否已经注册了指定 $tag 的过滤器。如果没有,直接返回原始值。

  3. 循环遍历过滤器: 按照优先级顺序循环遍历所有注册的过滤器函数。

  4. 构造参数数组: 将原始值 $value 和额外的参数 $args 合并成一个参数数组。

  5. 截取参数: 根据过滤器函数声明的接收参数个数,截取需要的参数。

  6. 调用过滤器函数: 使用 call_user_func_array 函数动态调用过滤器函数,并将参数传递给它。

  7. 更新 $value: 将过滤器函数返回的值作为新的 $value,传递给下一个过滤器函数。

例子:

// 获取文章内容
$content = get_the_content();

// 应用 'the_content' 过滤器,对文章内容进行处理
$filtered_content = apply_filters( 'the_content', $content );

// 显示处理后的文章内容
echo $filtered_content;

在这个例子中,apply_filters 函数会找到所有注册在 'the_content' 这个 $tag 上的过滤器,并依次执行它们,对 $content 进行处理。最终,$filtered_content 变量中存储的就是经过所有过滤器处理后的文章内容。

四、一个更完整的例子

假设我们想创建一个插件,允许用户自定义文章的摘要长度,并且在摘要末尾添加一个 "Read More" 链接。

<?php
/*
Plugin Name: Custom Excerpt Length
Description: Allows users to customize the excerpt length and add a "Read More" link.
Version: 1.0
Author: Your Name
*/

// 1. 定义一个选项,允许用户在后台设置摘要长度
function custom_excerpt_length_settings_page() {
    add_options_page(
        'Custom Excerpt Length Settings', // 页面标题
        'Excerpt Length', // 菜单标题
        'manage_options', // 权限
        'custom-excerpt-length', // 菜单 slug
        'custom_excerpt_length_settings_page_content' // 回调函数
    );
}
add_action( 'admin_menu', 'custom_excerpt_length_settings_page' );

function custom_excerpt_length_settings_page_content() {
    // 获取当前设置的摘要长度
    $excerpt_length = get_option( 'custom_excerpt_length', 55 ); // 默认长度为 55

    ?>
    <div class="wrap">
        <h1>Custom Excerpt Length Settings</h1>
        <form method="post" action="options.php">
            <?php
            settings_fields( 'custom_excerpt_length_group' ); // 设置组
            do_settings_sections( 'custom-excerpt-length' ); // 设置区域
            submit_button();
            ?>
        </form>
    </div>
    <?php
}

// 2. 注册设置
function custom_excerpt_length_register_settings() {
    register_setting(
        'custom_excerpt_length_group', // 设置组名称
        'custom_excerpt_length', // 设置名称 (option name)
        'intval' // Sanitize 回调函数,确保输入是整数
    );

    add_settings_section(
        'custom_excerpt_length_section', // Section ID
        'Excerpt Length', // Section Title
        '', // Callback function (optional)
        'custom-excerpt-length' // Menu Slug
    );

    add_settings_field(
        'custom_excerpt_length_field', // Field ID
        'Length (words):', // Field Title
        'custom_excerpt_length_field_callback', // Callback function
        'custom-excerpt-length', // Menu Slug
        'custom_excerpt_length_section' // Section ID
    );
}
add_action( 'admin_init', 'custom_excerpt_length_register_settings' );

function custom_excerpt_length_field_callback() {
    $excerpt_length = get_option( 'custom_excerpt_length', 55 );
    ?>
    <input type="number" name="custom_excerpt_length" value="<?php echo esc_attr( $excerpt_length ); ?>" />
    <?php
}

// 3. 使用过滤器修改摘要长度和添加 "Read More" 链接
function custom_excerpt_length( $length ) {
    // 获取用户设置的摘要长度
    $custom_length = get_option( 'custom_excerpt_length', 55 );
    return $custom_length;
}
add_filter( 'excerpt_length', 'custom_excerpt_length', 999 ); // 降低优先级,确保最后执行

function custom_excerpt_more( $more ) {
    return ' <a class="read-more" href="'. get_permalink( get_the_ID() ) . '">' . __('Read More &raquo;', 'your-text-domain') . '</a>';
}
add_filter( 'excerpt_more', 'custom_excerpt_more' );

这个插件做了以下事情:

  1. 创建设置页面: 在 WordPress 后台创建一个设置页面,允许用户自定义摘要长度。
  2. 注册设置: 注册一个设置,用于存储用户设置的摘要长度。
  3. 使用过滤器:
    • 使用 excerpt_length 过滤器修改摘要长度。
    • 使用 excerpt_more 过滤器在摘要末尾添加 "Read More" 链接。

五、 add_filter vs add_action

很多同学容易混淆 add_filteradd_action。它们之间最主要的区别在于:

  • add_filter: 用于修改数据。过滤器函数必须接收一个值,并返回一个修改后的值。
  • add_action: 用于执行某些操作。Action 函数不需要返回值。

简单来说,add_filter 就像一个“数据转换器”,而 add_action 就像一个“事件监听器”。

六、总结

add_filterapply_filters 是 WordPress 插件开发中非常重要的工具。它们允许我们以一种灵活、可扩展的方式修改 WordPress 的核心功能。通过理解它们的原理,我们可以编写出更加强大、更加可定制的插件。

表格总结:

函数 作用 参数 返回值
add_filter 注册一个过滤器函数,当特定的过滤器被触发时,该函数会被执行。 1. $tag: 过滤器名称 (string)
2. $function_to_add: 过滤器函数 (callable)
3. $priority: 优先级 (int, optional, default: 10)
4. $accepted_args: 接受的参数个数 (int, optional, default: 1)
true (成功) 或 false (失败, 很少会失败,因为主要错误检测在调用时进行)
apply_filters 触发一个过滤器,执行所有注册在该过滤器上的函数。 1. $tag: 过滤器名称 (string)
2. $value: 需要被过滤的值 (mixed)
3. ...$args: 传递给过滤器函数的额外参数 (mixed)
经过所有注册的过滤器函数处理后的 $value (mixed)。如果没有任何过滤器注册,则返回原始的 $value

希望今天的讲座对大家有所帮助!记住,代码就像魔法,而 add_filterapply_filters 就是我们手中的魔杖,可以让我们创造出各种各样的奇迹!

发表回复

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