搜索全站
插件 13 分钟阅读

add_action 与 add_filter 的区别:钩子机制一次讲清

动作钩子和过滤器到底差在哪?本文从核心源码、执行时机、返回值要求三个角度讲清 add_action 与 add_filter,附自定义钩子的完整写法与优先级冲突排查方法。

U
UFO_PF 2023-07-24 · 阅读 13 分钟
ad 插件
阅读提示
本文约 5,077 字,预计阅读 13 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:为什么改了过滤器却没效果

插件开发初期最容易卡住的地方,往往不是不会写 PHP,而是搞不清该用 add_action() 还是 add_filter()。典型症状是:照着教程写了一段修改文章内容的函数,挂上去前台毫无变化;或者用 add_filter() 写了一段发送邮件的逻辑,结果邮件发了三次。

这两种错误的根因是同一个:把动作当过滤器用,或者反过来。表面上看,两个函数的参数完全一样(钩子名、回调、优先级、参数个数),WordPress 核心里 add_action() 甚至就是 add_filter() 的直接转发。但它们的调用约定完全不同,搞错了不会报错,只会静默出错——这比报错更难查。

本文从核心源码、执行时机、返回值这三个维度把区别讲清楚,并给出自定义钩子的写法,让你在自己的插件里也能正确地暴露扩展点。

原理分析:源码层面两者是什么关系

先看 WordPress 核心的源码(这段代码是真实的核心实现):

// wp-includes/plugin.php(核心源码节选)
function add_action( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {
    return add_filter( $hook_name, $callback, $priority, $accepted_args );
}

也就是说,add_action() 在底层就是 add_filter(),两者往同一个全局数组 $wp_filter 里写,存储结构完全一样。真正的区别不在「注册」这一步,而在「触发」这一步:

维度 动作(Action) 过滤器(Filter)
触发函数 do_action( $hook, $args... ) apply_filters( $hook, $value, $args... )
是否有输入值 无,只是通知 有,第一个参数是要被处理的值
返回值处理 丢弃所有回调的返回值 必须把处理后的值 return 回去
回调的用途 执行副作用:写数据库、发邮件、输出 HTML 修改数据并返回
典型钩子 init、wp_head、wp_footer、save_post the_content、excerpt_length、wp_title
链式关系 并行执行,互不影响 串行传递,后者接收前者的输出

关键区别就在第三行:动作钩子的返回值被丢弃,过滤器钩子的返回值会传给下一个回调并最终成为结果。

由此推出两条铁律:

  1. 用 add_filter() 时,回调必须 return,且必须返回处理后的第一个参数。忘记 return,就等于把内容清空——这是「改了过滤器没效果」或「内容变空白」的头号原因。
  2. 用 add_action() 时,回调不应该依赖返回值。如果你用 add_action() 挂了一个本该返回值的函数,那个返回值会被丢掉,功能自然不生效。

还有个容易混淆的点:输出 HTML 到底该用哪个?如果只是「在某个位置插入一段 HTML」,用动作钩子配合 echo 是常见做法(比如挂 wp_footer 输出脚本);而如果是「修改已有的一段内容」,就该用过滤器 return。判断标准是你是在创造新输出,还是在改造已有值。

保姆级实操

第一步:写第一个动作钩子

// 在页脚输出一段统计脚本(动作:执行副作用,不需要返回值)
add_action( 'wp_footer', 'mmf_footer_script' );
function mmf_footer_script() {
    if ( is_admin() ) {
        return;
    }
    echo '<script>console.log("mmf action fired");</script>';
}

注意这里用的是 echo 而不是 return,因为 do_action() 不会接收返回值。

第二步:写第一个过滤器钩子

// 在文章正文末尾追加一段文本(过滤器:必须 return)
add_filter( 'the_content', 'mmf_append_notice' );
function mmf_append_notice( $content ) {
    if ( ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() ) {
        return $content; // 不满足条件也必须原样返回,不能什么都不返回
    }
    return $content . '<p class="mmf-notice">本文由 mimangfei.com 原创。</p>';
}

这里每一个分支都有 return,包括「不处理」的分支。这是过滤器最容易写错的地方。

第三步:理解优先级与参数个数

add_filter( 'excerpt_length', 'mmf_excerpt_length', 20 ); // 优先级 20,晚于默认 10
function mmf_excerpt_length( $length ) {
    return 40;
}

// 需要多个参数时必须显式声明第 4 个参数
add_filter( 'the_content', 'mmf_conditional_content', 10, 2 );
function mmf_conditional_content( $content, $post_id ) { // 不写 2 就拿不到 $post_id
    if ( has_tag( 'notice', $post_id ) ) {
        $content .= '<p>带 notice 标签的文章。</p>';
    }
    return $content;
}

优先级数字越小越早执行,默认 10。同一优先级内按注册顺序执行。第 4 个参数声明的是「回调接收的参数个数」,不写就只有 1 个。

第四步:在自己的插件里定义动作钩子

给其他开发者留出扩展点,这是插件能否被二次开发的关键:

// 同步完成后,让其他插件可以做后续处理
function mmf_sync_product( $product_id ) {
    // ... 同步逻辑 ...
    do_action( 'mmf_after_product_sync', $product_id, 'success' );
}

别人就可以这样接入:

add_action( 'mmf_after_product_sync', function ( $product_id, $status ) {
    if ( 'success' === $status ) {
        update_post_meta( $product_id, '_synced_at', time() );
    }
}, 10, 2 );

第五步:在自己的插件里定义过滤器钩子

function mmf_calculate_price( $base ) {
    $price = $base * 1.1;
    // 允许其他插件修改最终价格,必须把 apply_filters 的结果 return 出去
    return apply_filters( 'mmf_final_price', $price, $base );
}

别人这样接入:

add_filter( 'mmf_final_price', function ( $price, $base ) {
    return $price - 5; // 减 5 元
}, 10, 2 );

第六步:排查钩子不生效的问题

用这几个函数确认钩子到底有没有被调用、回调有没有挂上:

// 当前正在执行哪个钩子
add_action( 'wp_footer', function () {
    error_log( 'current filter: ' . current_filter() );
} );

// 某个钩子上是否挂了指定回调
if ( false !== has_action( 'the_content', 'mmf_append_notice' ) ) {
    error_log( '已注册' );
}

// 某个钩子本次请求被触发了几次
error_log( 'init 执行次数: ' . did_action( 'init' ) );

// 正在执行某个钩子的过程中返回 true
if ( doing_action( 'save_post' ) ) {
    // ...
}

第七步:移除别人的回调

// 移除时必须提供与注册时完全相同的优先级
add_action( 'init', 'mmf_remove_others' );
function mmf_remove_others() {
    remove_filter( 'the_content', 'wpautop', 10 );
    remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
}

remove_action() 同样只是 remove_filter() 的别名。注意移除必须在回调注册之后执行,所以通常挂在 init 或更晚的钩子上。

常见踩坑

  1. 过滤器回调忘记 return:表现为内容变空。这是新手第一大坑。

  2. 需要第二个参数却没声明参数个数:add_filter( 'the_content', $cb ) 默认只传 1 个参数,回调里写第二个参数会拿到 null 或报错。

  3. 用 add_action 挂本该过滤的逻辑:返回值被丢弃,功能静默失效。

  4. 移除回调时优先级不一致:remove_filter() 的第三个参数必须和注册时一致,否则移除失败且不报错。

  5. 匿名函数无法被移除:add_action('init', function(){}) 注册的闭包拿不到句柄,remove_action 没法移除。需要被移除的回调一律用命名函数。

  6. 案例: 某开发者写了一个给 WooCommerce 商品价格加手续费的功能,用 add_action( 'woocommerce_get_price_html', 'add_fee' ) 挂载,函数里老老实实 return $price . '(含手续费)',结果前台价格毫无变化。原因就是用了动作而不是过滤器——woocommerce_get_price_html 是一个过滤器钩子,返回值必须通过 add_filter 才能传回核心。改成 add_filter 后立刻生效。判断方法很简单:去核心源码里搜这个钩子名,看它是被 do_action 还是 apply_filters 调用的。

  7. 在钩子里做重型操作:init、wp_head 这类钩子每个请求都跑,里面放外部 API 请求会直接拖慢全站。

总结

一句话区分:动作是「通知你去做一件事」,返回值没人要;过滤器是「给你一个值去改」,必须把改完的值还回去。

判断用哪个的实操方法:在核心或插件源码里搜索钩子名,看它被 do_action() 调用(用 add_action)还是被 apply_filters() 调用(用 add_filter)。

代码规范三条:过滤器每个分支都要 return;需要多个参数时显式声明第 4 个参数;需要被移除的回调必须用命名函数。

下一步:理解了钩子之后,就可以做能被配置的插件了——创建自定义设置页面 讲 add_menu_page 与 Settings API;而在设置页面里保存数据之前,必须先掌握 nonce 与 capability 安全 里的防护写法。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。