
前两篇写完 WordPress 头像裂图和 str_replace 之后,我对着主题里的 functions.php 发了一会儿呆——这个文件里 add_filter 出现了好几处,我大概知道它们”起作用了”,但说不清到底怎么起的。
最先冒出来的疑问是一个很朴素的:这个 add_filter 是 WordPress 这个 CMS 独有的吗? PHP 里我从没见过这个函数,那学会它是不是只对 WordPress 有用?
把这个问题弄清楚之后,我发现它其实是理解整个 WordPress 扩展机制的总开关。今天把它掰开讲一遍,尽量不用术语。
结论分两层,别混在一起:
add_filter 这个名字是 WordPress 自己定义的函数,就写在核心文件 wp-includes/plugin.php 里。PHP 语言本身没有它,你换 Laravel、换 ThinkPHP,甚至换成另一个 CMS,都调不到——这是 WordPress 的”自家规矩”。addEventListener、jQuery 的 on()、各类框架里的”事件监听””回调””订阅”—名字不同,套路一模一样。所以别把它当什么独门咒语。你把 add_filter 理解成一个”登记处”就够了:你跑到登记处留个名字和电话号码,WordPress 到点了就按号码打给你。
官方的函数签名长这样:
add_filter( 钩子名, 你的函数名, 优先级 = 10, 接收参数个数 = 1 );
翻译成人话:
后两个参数平时不用管,用默认值就行。绝大多数情况下,你看到的就是两行代码。
不管想干什么,套路固定:
return 回去;add_filter 登记:告诉 WordPress 挂在哪个钩子上;用一个最小例子感受一下,比如我想让每篇文章的正文末尾自动加一句话:
function my_add_footer_note( $content ) {
$content .= '<p>本文为原创,转载请注明出处。</p>';
return $content; // 关键:一定要 return
}
add_filter( 'the_content', 'my_add_footer_note' );
就这些。没有第三步,没有”调用”动作——你从不主动执行 my_add_footer_note,是 WordPress 在输出正文前替你执行的。
光看最小例子没感觉,我们直接看我站上真实在用的代码,三种典型形态都齐了。
add_filter( 'preprocess_comment', 'wpb_preprocess_comment' );
function wpb_preprocess_comment($comment) {
$txt = strstr($comment['comment_content'], "http");
if ( strlen( $comment['comment_content'] ) > 350 ) {
wp_die('你的字数太长了,是不是在发广告哦??');
}
elseif($txt != false){
wp_die('包含了非法字符:http');
}
return $comment;
}
这条挂在 preprocess_comment 上——意思是”每条评论写进数据库之前,先交给我的函数过一遍”。函数做了两件事:字数超过 350 就拦下,内容里带 http 也拦下(防广告);没问题就 return $comment 放行。
注意这里的模式:接过来一个东西 → 检查或改一下 → 原样或改后还回去。这就是 filter 的标准长相。
add_filter('wp_title', 'lingfeng_wp_title', 10, 2);
function lingfeng_wp_title($title, $sep) {
// ... 拼接站点名、处理分页、去掉短横线字符实体等
return $title;
}
这行把后两个参数用上了:10 是优先级(用默认值,写不写都一样,写出来是为了让后面那个参数有位置),2 是告诉 WordPress”我这个函数要接 2 个参数”——所以下面的函数签名是 ($title, $sep),能同时拿到标题和分隔符。
这里是新手最容易卡住的地方:第四个参数不写,你函数里就接不到第二个变量。你写 function($title) 却指望拿到 $sep,只会得到一个 undefined 报错。
add_action('save_post', 'auto_add_tags');
function auto_add_tags(){
$tags = get_tags( array('hide_empty' => false) );
$post_id = get_the_ID();
$post_content = get_post($post_id)->post_content;
if ($tags) {
foreach ( $tags as $tag ) {
if ( strpos($post_content, $tag->name) !== false)
wp_set_post_tags( $post_id, $tag->name, true );
}
}
}
这条干的事是:文章一保存,就扫描正文,把出现过已有标签名的自动设成标签。请你特别看一眼这个函数的结尾——它没有 return 任何东西。
为什么可以有 return 也可以没有?这就引出了下面这个必须分清的概念。
这是 WordPress 钩子体系里最容易混淆的一对。其实差别只有一句话:
filter 是”改数据”,必须把值还回去;action 是”趁机做事”,不用还。
展开成对比:
| 对比项 | add_filter(过滤器) | add_action(动作) |
|---|---|---|
| 干什么 | 接过一个值,改完还回去 | 趁某个时刻做一件事 |
| 要不要 return | 必须 return,否则值变空 | 不需要,return 会被忽略 |
| 典型场景 | 改正文、改标题、改头像地址 | 发邮件、写日志、自动加标签 |
| 我的代码里 | wpb_preprocess_comment |
auto_add_tags |
还有个冷知识:在 WordPress 源码里,add_action 其实就是 add_filter 的一层包装,底层调用的是同一个函数。区别不在机制,而在你用它的意图——一个要改值,一个只是借个时机。所以判断标准很简单:问自己”我要不要把什么东西还回去”,要就用 filter,不要就用 action。
一句话:为了让你不改核心文件,还能随便改功能。
想一下没有钩子时会怎样:你想让评论自动过滤广告,唯一的办法是打开 WordPress 安装目录里的处理文件,在里面加几行判断。然后 WordPress 一升级,这个文件被新版覆盖——你的改动灰飞烟灭,下次升级还得重做一遍。
有了钩子,你的代码放在主题的 functions.php(更规范的做法是子主题)里,WordPress 升级只替换它自己的文件,动不到你。这就是为什么稍微懂行的人都说”永远不要改 WordPress 核心“,而钩子正是让你不必改核心的那个替代品。
头号事故,没有之一。前面讲头像代码时提过一次,这里再说一遍:filter 函数不 return,WordPress 拿到的就是空值——正文变空白、标题消失、头像变空。而且它不报错,页面就是莫名其妙少一块,查起来特别费劲。写完 filter 先检查有没有 return,养成条件反射。
第三个参数数字越小越先执行,默认 10。当同一个数据被好几个函数依次修改时,先后顺序会影响最终结果。比如有个插件在优先级 10 上给正文加了广告,你想在广告之后再加自己的内容,就要把优先级设成 11 或 20;想在广告之前,就设成 9 或更小。
两个函数同名,PHP 直接报致命错误,整站白屏。这个坑的隐蔽之处在于:今天没问题,不代表明天没问题——装个新插件、换个主题,撞上了就挂。
防它的办法是给函数名加前缀。回头看我文件里的命名:lingfeng_wp_title、wpb_preprocess_comment、liu_replace_gravatar——每个都带着自己的前缀,这正是在防撞名。当初写这些代码的人是懂规矩的。你以后自己写函数,也照这个来:你的名字_作用。
如果上面的都忘了,只留一句:
add_filter 就是告诉 WordPress:以后你要用这个值之前,先让我改一下。
你那个 functions.php 里之所以到处都是它,正因为这是给 WordPress 加规矩最省事、也最安全的路子——不用碰核心文件,不用写插件,几行就生效,升级还冲不掉。
回到开头那个问题:add_filter 确实是 WordPress 独有的函数,但它教给你的东西不局限于 WordPress——”登记回调、等待被叫”这个套路,你以后碰到任何框架的事件系统都会再遇到一次。所以搞懂它不算亏。
我的习惯是:遇到一个看不懂的函数,先问它”我传进去什么、还回来什么”,再问”是谁在什么时候调用它”。这两问答上来,绝大多数函数就不吓人了。
(本文是 WordPress 踩坑系列的第四篇,前面几篇讲了评论头像裂图的修复和 str_replace 的替换规则,都是从同一个 functions.php 引出来的问题。)