剖析 WordPress `apply_filters()` 函数源码:它与 `do_action()` 的核心区别是什么,以及如何处理返回值?

各位代码界的段子手们,早上好/下午好/晚上好!我是你们今天的WordPress源码解析师,人称“代码挖掘机”。今天咱们不挖矿,挖WordPress的源码,目标直指apply_filters()这个小妖精,顺便把它孪生兄弟do_action()拉出来遛遛。

咱们要像剥洋葱一样,一层一层地扒开它的源码,看看它到底是个什么玩意儿,以及它和do_action()之间那些不得不说的故事,最后再聊聊它那让人头疼的返回值。准备好了吗?发车!

第一站:初识 apply_filters()do_action()

在开始深入源码之前,我们先简单了解一下这两位爷是干嘛的。

  • apply_filters() 主要用于修改数据。想象一下,你正在往一个水杯里倒水,apply_filters()就像是一个过滤器,水经过它之后,可能会变得更纯净,或者被染上颜色,最终进入你的肚子。
  • do_action() 主要用于执行动作。 比如点击一个按钮,触发一系列事件,do_action()就像是一个触发器,它会通知所有监听这个事件的函数,让它们赶紧开始干活。

用一个表格来概括一下:

功能 apply_filters() do_action()
主要用途 修改数据 执行动作
返回值 修改后的数据 无返回值
核心理念 数据过滤 事件触发

第二站:apply_filters() 源码剖析

好了,现在我们来扒一扒apply_filters()的源码。以下代码基于WordPress 6.x版本。

function apply_filters( $tag, $value, ...$args ) {
    global $wp_filter, $wp_current_filter;

    $wp_current_filter[] = $tag;

    $args = array_slice( func_get_args(), 1 );

    $priority = has_filter( $tag );

    if ( false !== $priority && isset( $wp_filter[ $tag ] ) ) {

        reset( $wp_filter[ $tag ] );

        if ( true === $priority ) {
            $priority = key( $wp_filter[ $tag ] );
        }

        do {
            foreach ( (array) $wp_filter[ $tag ][ $priority ] as $function => $args_num ) {

                if ( ! has_filter( $tag, $function, $priority ) ) {
                    continue;
                }

                $args[0] = $value;

                $the_ = call_user_func_array( $function, array_slice( $args, 0, (int) $args_num ) );

                if ( null !== $the_ ) {
                    $value = $the_;
                }

            }

            $priority++;

        } while ( isset( $wp_filter[ $tag ][ $priority ] ) );
    }

    array_pop( $wp_current_filter );

    return $value;
}

别害怕,我们来一行一行地解读:

  1. global $wp_filter, $wp_current_filter;

    • $wp_filter:这是一个全局变量,存储了所有注册的过滤器(filter)。它是一个多维数组,结构大概是这样的:

      $wp_filter = [
          'the_content' => [ // filter 的名称 (tag)
              10 => [ // 优先级
                  'wpautop' => [ // 函数名
                      'function' => 'wpautop', // 函数本身
                      'accepted_args' => 1 // 接受的参数个数
                  ],
                  'shortcode_unautop' => [
                      'function' => 'shortcode_unautop',
                      'accepted_args' => 1
                  ]
              ],
              20 => [
                  'convert_chars' => [
                      'function' => 'convert_chars',
                      'accepted_args' => 1
                  ]
              ]
          ],
          'the_title' => [
              10 => [
                  'trim' => [
                      'function' => 'trim',
                      'accepted_args' => 1
                  ]
              ]
          ]
      ];
    • $wp_current_filter:这是一个数组,用于跟踪当前正在执行的过滤器。这主要是为了防止循环调用,避免无限递归。
  2. $wp_current_filter[] = $tag;

    • 将当前的过滤器名称($tag)添加到 $wp_current_filter 数组中。
  3. $args = array_slice( func_get_args(), 1 );

    • 获取传递给 apply_filters() 的所有参数,除了第一个参数($tag,过滤器名称)之外,都放到 $args 数组中。
    • func_get_args() 获取所有参数。
    • array_slice() 从数组中提取一部分。
  4. $priority = has_filter( $tag );

    • 检查是否存在名为 $tag 的过滤器。has_filter() 函数会返回过滤器的优先级(如果存在),否则返回 false
  5. if ( false !== $priority && isset( $wp_filter[ $tag ] ) ) { ... }

    • 如果存在名为 $tag 的过滤器,并且 $wp_filter 数组中也存在该过滤器,则执行以下代码。
  6. reset( $wp_filter[ $tag ] );

    • $wp_filter[$tag] 数组的内部指针重置到第一个元素。
  7. if ( true === $priority ) { $priority = key( $wp_filter[ $tag ] ); }

    • 如果 has_filter() 返回 true,表示存在过滤器,但是没有指定优先级,那么就使用 $wp_filter[$tag] 的第一个键作为优先级。
  8. do { ... } while ( isset( $wp_filter[ $tag ][ $priority ] ) );

    • 这是一个循环,它会遍历所有具有相同 $tag 的过滤器,按照优先级从小到大依次执行。
  9. foreach ( (array) $wp_filter[ $tag ][ $priority ] as $function => $args_num ) { ... }

    • 遍历当前优先级下的所有过滤器函数。
    • $function:是函数名。
    • $args_num:是函数接受的参数个数。
  10. if ( ! has_filter( $tag, $function, $priority ) ) { continue; }

    • 再次检查是否存在名为 $tag、函数名为 $function、优先级为 $priority 的过滤器。这可能是为了处理动态添加/删除过滤器的情况。如果不存在,则跳过当前循环。
  11. $args[0] = $value;

    • 将原始值 $value 放到 $args 数组的第一个位置。这是因为过滤器函数通常需要接收原始值作为第一个参数。
  12. $the_ = call_user_func_array( $function, array_slice( $args, 0, (int) $args_num ) );

    • 这是最关键的一行代码!
    • call_user_func_array():用于调用一个函数,并将一个数组作为参数传递给它。
    • $function:是要调用的函数名。
    • array_slice( $args, 0, (int) $args_num ):从 $args 数组中提取前 $args_num 个参数,作为传递给 $function 的参数。
    • $the_:存储函数调用的返回值。
  13. if ( null !== $the_ ) { $value = $the_; }

    • 如果过滤器函数返回了非 null 的值,则将 $value 更新为该返回值。这就是 apply_filters() 修改数据的核心机制。
  14. $priority++;

    • 增加优先级,以便处理下一个优先级的过滤器。
  15. array_pop( $wp_current_filter );

    • $wp_current_filter 数组中移除当前的过滤器名称。
  16. return $value;

    • 返回最终修改后的值。

第三站:do_action() 源码剖析

接下来,我们看看do_action()的源码。

function do_action( $tag, ...$args ) {
    global $wp_filter, $wp_actions, $wp_current_filter, $wp_did_action;

    $wp_current_filter[] = $tag;

    if ( isset( $wp_actions[ $tag ] ) ) {
        ++$wp_actions[ $tag ];
    } else {
        $wp_actions[ $tag ] = 1;
    }

    if ( isset( $wp_filter[ $tag ] ) ) {
        $priority = has_action( $tag );

        if ( true === $priority ) {
            $priority = key( $wp_filter[ $tag ] );
        }

        do {
            foreach ( (array) $wp_filter[ $tag ][ $priority ] as $function => $args_num ) {

                if ( ! has_action( $tag, $function, $priority ) ) {
                    continue;
                }

                call_user_func_array( $function, array_slice( $args, 0, (int) $args_num ) );
            }
            $priority++;
        } while ( isset( $wp_filter[ $tag ][ $priority ] ) );
    }

    array_pop( $wp_current_filter );

    return;
}

是不是感觉似曾相识?没错,do_action() 的源码和 apply_filters() 非常相似,主要区别在于:

  1. $wp_actions 全局变量: do_action() 使用 $wp_actions 数组来记录某个 action 被触发的次数。

  2. 没有返回值: do_action() 不返回任何值。它只是触发一系列函数,这些函数执行一些操作,但不修改任何数据。

  3. call_user_func_array() 的处理: do_action() 直接调用 $function,不关心返回值,也不更新 $value

第四站:apply_filters()do_action() 的核心区别

现在,我们可以更清晰地总结一下 apply_filters()do_action() 的核心区别:

特性 apply_filters() do_action()
目的 修改数据 执行动作
返回值 修改后的数据 无返回值
核心操作 通过调用过滤器函数,逐个修改 $value,最终返回修改后的 $value 触发一系列函数,不关心返回值
全局变量 $wp_filter, $wp_current_filter $wp_filter, $wp_current_filter, $wp_actions
使用场景 修改文章内容、修改标题、修改选项值等 插件激活、主题切换、用户登录等

第五站:apply_filters() 的返回值处理

apply_filters() 的返回值是它最重要也是最让人头疼的地方。记住以下几点:

  1. 返回值必须是非 null 如果你的过滤器函数返回 nullapply_filters() 会忽略这个返回值,继续使用之前的 $value

  2. 返回值类型要一致: 虽然 PHP 是弱类型语言,但最好保持返回值类型的一致性。比如,如果你要修改一个字符串,那么所有过滤器函数都应该返回字符串。否则,可能会出现意想不到的错误。

  3. 注意优先级: 过滤器函数的执行顺序由优先级决定。优先级越小,越先执行。这意味着,先执行的过滤器函数可能会影响后执行的过滤器函数的输入。

代码示例:修改文章内容

// 注册一个过滤器,用于在文章内容末尾添加版权信息
add_filter( 'the_content', 'add_copyright_notice' );

function add_copyright_notice( $content ) {
    $copyright = '<p>Copyright 2023. All rights reserved.</p>';
    return $content . $copyright;
}

// 注册另一个过滤器,用于将文章内容中的敏感词替换为 "***"
add_filter( 'the_content', 'censor_sensitive_words', 11 ); // 优先级比 add_copyright_notice 高

function censor_sensitive_words( $content ) {
    $sensitive_words = array( '坏蛋', '笨蛋' );
    $replacement = '***';
    return str_replace( $sensitive_words, $replacement, $content );
}

// 使用 apply_filters() 应用过滤器
$content = "这是一篇包含 坏蛋 的文章。";
$filtered_content = apply_filters( 'the_content', $content );
echo $filtered_content;
// 输出:这是一篇包含 *** 的文章。<p>Copyright 2023. All rights reserved.</p>

在这个例子中,censor_sensitive_words() 的优先级高于 add_copyright_notice(),所以它会先执行,将文章内容中的敏感词替换为 "***",然后再执行 add_copyright_notice(),在文章内容末尾添加版权信息。

第六站:注意事项和最佳实践

  • 不要滥用过滤器: 虽然过滤器很强大,但不要滥用。只有在需要修改数据时才使用过滤器。如果只是想执行一些操作,应该使用 action。

  • 合理设置优先级: 优先级非常重要,它决定了过滤器函数的执行顺序。要仔细考虑每个过滤器函数的优先级,确保它们按照正确的顺序执行。

  • 做好类型检查: 虽然 PHP 是弱类型语言,但最好在过滤器函数中做好类型检查,确保输入和输出的类型一致。

  • 编写可测试的代码: 使用过滤器时,要编写可测试的代码。这意味着,你需要能够独立测试每个过滤器函数,确保它们能够正确地修改数据。

  • 使用明确的命名: 为过滤器函数和过滤器名称使用明确的命名,以便于理解和维护。

  • 避免循环依赖: 避免在过滤器函数中调用 apply_filters() 本身,否则可能会导致循环依赖,最终导致程序崩溃。

总结

好了,今天的讲座就到这里。我们深入剖析了 WordPress 的 apply_filters() 函数源码,了解了它和 do_action() 的核心区别,以及如何处理返回值。希望这些知识能帮助你更好地理解 WordPress 的工作原理,编写更健壮、更可维护的代码。记住,代码就像段子,写得好就能让人捧腹大笑,写得不好就只能让人尴尬挠头。所以,努力成为代码界的段子手吧!

散会!

发表回复

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