了解 WordPress 钩子(Hook)的机制与原理,就能明白自定义函数中 add_action 和 add_filter 的含义了,他们对应着 WordPress 动作钩子和 WordPress 过滤钩子,对于修改或制作主题与插件帮助非常大,甚至可以说比必经之路。
本文针对 WordPress Hook
运作大致做个简单的说明,而预设读者是理解基本的 PHP function 语法及运作,但对 WordPress Hook 机制不是很明白。
钩子(Hook) 机制里登场的角色
先从登场角色的个别说明开始:
WordPress 核心
指的是 WordPress 内建的程式码架构,提供 WordPress 主要的基本功能。
钩子(Hook)
也许你早已听说,Hook 本身虽是钩子的意思,但直译又有点奇怪,所以一般通常都不直译它,而是直接称它 Hook 。WordPress 的 Hook 也可以想像成“钩子”,这些“钩子”会埋在 WordPress 网站中特定几处的程式码中,埋进去时使用的语法,其“标示位置”的意义比较大,没有实质运作的内容。
当程式执行到有埋 Hook 的地方时,它会找出所有对应到自己的 Hook Function (也就是所有“钩到”该 Hook 的 hook function),并一一执行。
因此若没有针对此 Hook 去“加入”要钩上去的 Hook Function,执行到此什么也不会做。因此,它等于是 WordPress 核心预留一个执行的机会给未来想要加入定制功能的开发者。
Hook Function
Hook Function 里会有实质运作的内容,即是实作了一些定制功能,可能是存取 DB、增加 HTML code、执行其他函式等等。我们在 Hook Function 里写好所需的功能后,就可以利用“加入至对应 Hook”的语法,把 Hook Function 自已钩到该 Hook 上,使得该 Hook 被执行到时,也会连带执行自己。
Hook机制是如何运作的?
举个例子,我们拿 wp_head
及 wp_footer
这两个内建的 hook 来说明,wp_head 这个 hook 就是用来埋在负责输出标签的程式码中,而 wp_footer 就是用来埋在输出页尾的程式码中 (定义于 wp-includes/general-template.php
,用 wp_head()
及 wp_footer()
包装起来)。
这两个 hook,主要都是在布景档案中使用的,常见会出现在 header.php
及 footer.php
中。
请看下面的情境示例图,我们把 wp_head 及 wp_footer 看成是”钩子“,而别的 hook functions 就能来钩住它:
我们马上来写一个简单的例子。我们要写一个 hook function,就叫它 print_sth()
,然後把它钩上 wp_head 这个hook。因为 wp_head()
的内容实际上就只有 do_action( 'wp_head' )
; 这一行内容,而 wp_footer()
的内容也只有 do_action('wp_footer')
;所以我们直接把 do_action 的语法换到图上去,比较容易做说明,因此示意图变成:
如此,只要执行到输出 header.php 时,就会执行到 wp_head(),就如同执行到 do_action( 'wp_head' ); 此时WP核心会去找所有”钩上”wp_head 这个 hook 的 hook function,於是就找到我们写的 print_sth()
,然後就执行它,所以结果它做的事就会出现在网站上,也完成了”定制”的动作:
简单的说,Hook 机制就是:WP 核心或其他插件、主题 提供想定制功能的人一个置入定制程式码(Hook Function)到特定的执行时间点(Hook)的机会。
WordPress的 Action Hook 与 Filter Hook
WordPress 中的 Hook 有两种,分别是 Action Hook 及 Filter Hook ,我们刚才举例的 wp_head 及 wp_footer 都是属於 Action Hook。不过,一开始你可以先把这两种 Hook 看成是一样的东西,只是 Filter 多了一点点不同的特色,接着说明。
Action Hook
WP 核心 (或主题、插件)在做它们该做的事时,如果执行到有埋 action hook 的程式码 (即是 do_action 语法) 时,会去找寻对应到的 hook functions,进而执行这些 hook functions(即那些透过 add_action() 来加入的 hook functions),藉此完成定制功能。WP核心并不期待 Action Hook functions 会有回传值,所以这里的 hook function 只被视为一个 独立切出来运作的功能,WP 核心做它该做的事,你做你想做的事,做完就各自结束。 。
Filter Hook
跟 Action Hook 一样,WP核心 (或主题、插件)在做它们该做的事时,如果执行到有埋 filter hook 的程式码 (即是apply_filters语法) 时,就会去找寻对应的 hook functions ,进而执行这些 hook functions(即那些透过add_filter() 来加入的 hook functions ),藉此完成定制功能。与 Action Hook 不同之处是,所有”钩上“ Filter Hook 的 hook functions 通常都会接收到参数,而WP核心会期待你拿到它提供的参数,并做完你想做的事后,要回传(return)一个值,让WP核心再利用你回传的值来接着完成它该做的事。
透过你的干涉,修改了WP核心丢给你的参数,WP核心再接着拿你改过的参数,继续完成它该做的事,此动作就像”过滤“的动作,因而得名 filter。
比较Action Hook与Filter Hook的操作语法
比较一下两种 Hook 在埋进某处程式码时所用的语法,假设我们在某处 (可能是在输出页首的程式码处,或输出文章标题、文章内容、侧边栏…等地方,要”出现定制效果“的地方)埋下这两种 hook:
/*--------------- Action Hook ---------------*/ // 埋下一个名叫'do_more'的action hook do_action('do_more'); /*--------------- Filter Hook ---------------*/ // 埋下一个名叫'get_special'的filter hook,注意它会有回传值 $c = apply_filters('get_special',$a, $b);
然后我们可以在某处 (可能是其他插件、functions.php 等处,要”实现定制功能“的地方)添加对应的 hook function:
/*--------------- Action Hook Function---------------*/ // 增加要钩上'do_more'这个hook的hook function, // 并为此hook function取名叫more_func。 // 第一个参数是hook名称、第二个是hook function名称 add_action('do_more', 'more_func'); // 添加more_func的內容,不需回传值 function more_func() { echo 'do more thing...'; } /*--------------- Filter Hook Function ---------------*/ // 增加要钩上'get_special' hook的hook function, //并为此hook function取名叫special_func。 // 参数1是hook名称、参数2是hook function名称 // 参数3是Priority(优先序)、参数4是hook function参数的数目 add_filter('get_special', 'special_func', 10, 2); // 添加special_func的內容,需要給它回传值 function special_func($a, $b) { $c = $a.' & '.$b; //做一些事,例如把两个参数连接起來 return $c; //回传值 }
所以其实两种 Hook 的运作方式几乎一样,只差在增加 Action Hook 函式不需回传值,而增加 Filter Hook function时,你必须要回传一个值。所以 Filter Hook 函式通常都有提供参数,让想定制的人可以取得它,处理后再回传。
但如果有一个 Filter Hook,它没有任何 hook function 有去钩它,它该怎么取得回传值?答案是,直接拿第一个它给的参数,以上面的例子来说,它会直接拿 $a 丢进 $c 。另外,其实我们也可以把 filter 写的跟 action 一样,只要不回传值就行,但 action hook 就没办法”模仿“ filter hook,因为无法取得回传值。
Hook Function的优先序(Priority)
如果有很多地方(主题或者插件的 functions.php)都 添加同一个 hook ,会怎么决定出现顺序?答案很显然是可以透过 Hook Function 的 Priority 参数来作优先序的设定:
就像我们刚才说明的例子中,我们使用 add_filter 加入 special_func 时设定的优先序是 10 ,这也是 Priority 参数的预设值。如果你希望它能优先被执行,就设定小于 10 的数字,反之,就设个 100、500 之类的,让它延后被执行。
但其实这里有个隐含的冲突问题。
以 wp_head 这个 hook 为例,如果我写了一个插件,希望透过 wp_head 来输出”增加 a.css 档案“的 HTML 语法,而 a.css 会重新设定 body 元素的样式,所以我希望它可以最后才被汇入,不要被其他 css 档干扰,于是我将 Priority 设为 900,但我怎么知道 Priority 900 够不够大?若某个WP网站,它除了安装我的插件,也安装了其他插件,而其他插件刚好也重新设定body元素的样式,然后把 Priority 设为 950,此时我写的插件在处理 body 样式时就出事了,于是就跟其他插件冲突了。
所以此时我们需要了解的是:我的WP网站可能装了很多插件,我怎么知道同一个 Hook 被加了多少 Hook Function,而每个 Hook Function 的 Priority 被设定为多少?
答案是,我们可以透过 $wp_filters 这个 global 变数来取得所有 hook 的信息,像是如下的 function:
// 列出所有的hook function及其priority function list_hooked_functions($tag=false) { global $wp_filter; if ($tag) { $hook[$tag]=$wp_filter[$tag]; if (!is_array($hook[$tag])) { trigger_error("Nothing found for '$tag' hook", E_USER_WARNING); return; } } else { $hook=$wp_filter; ksort($hook); } echo '<code>'; foreach($hook as $tag => $priority) { echo "<br />>>>>>\t<strong>$tag</strong><br />"; ksort($priority); foreach($priority as $priority => $function) { echo $priority; foreach($function as $name => $properties) echo "\t$name<br />"; } } echo '</code>'; return; }
当我们呼叫 list_hooked_functions('wp_head');
时,就会列出 wp_head 这个 Hook 所钩住的所有 hook function,可以看到 priority 10 之后有好几个都没有数字,因为它们都没有特别指定 priority,所以都是 10 ,包括我们刚才写的 print_sth 也在其中:
>>>>> wp_head 1 wp_enqueue_scripts 2 feed_links 3 feed_links_extra 8 wp_print_styles 9 wp_print_head_scripts 10 rsd_link wlwmanifest_link index_rel_link parent_post_rel_link start_post_rel_link adjacent_posts_rel_link_wp_head locale_stylesheet wp_generator rel_canonical wp_shortlink_wp_head print_sth wp_admin_bar_header _admin_bar_bump_cb
所以,冲突很难提早避免,但发生冲突时,可以预先思考有没有可能是因为 priority 的设定,导致结果跟预期不符合。