WordPress的Hook机制与原理

0

稍有接触过WordPresszhu主题与插件定制或修改的朋友,对WordPress的Hook机制应该不陌生,但通常刚接触WordPress Hook的新手,对其运作原理可能会有点混乱或馍糊。本文针对WordPress Hook运作大致做个简单的说明,而预设读者是理解基本的PHP function语法及运作,但对WordPress Hook机制不是很明白。

wordpress_hooks

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代码、执行其他函数…等。我们在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就能来钩住它:

wordpress_hookview

我们马上来写一个简单的例子。我们要写一个hook function,就叫它print_sth(),然后把它钩上wp_head这个hook。因为wp_head()的内容实际上就只有do_action('wp_head'); 这一行内容,而wp_footer()的内容也只有 do_action('wp_footer');,所以我们直接把do_action的语法换到图上去,比较容易做说明,因此示意图变成:

wordpress_hook_example

如此,只要执行到输出header.php时,就会执行到wp_head(),就如同执行到do_action(‘wp_head’),此时WP核心会去找所有「钩上」wp_head这个hook的hook function,于是就找到我们写的print_sth(),然后就执行它,所以结果它做的事就会出现在网站上,也完成了「定制」的动作:

wordpress_hookview

简单的说,Hook机制就是:WP核心或其他plugin、theme提供想定制功能的人一个置入定制代码(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:

1
2
3
4
5
6
7
/*--------------- 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*--------------- 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($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)

如果有很多地方(plugin或者布景functions.php)都add同一个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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 列出所有的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 '<'.'pre>';
    foreach($hook as $tag => $priority)
    {
        echo "<br />&gt;&gt;&gt;&gt;&gt;t<strong>$tag</strong><br />";
        ksort($priority);
        foreach($priority as $priority => $function)
        {
            echo $priority;
            foreach($function as $name => $properties) echo "t$name<br />";
        }
    }
    echo '<'.'/pre>';
    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的设定,导致结果跟预期不符合。

译自mrmu,朽木整编

发表评论

您的邮箱不会公开,当您的评论有新的回复时,会通过您填写的邮箱向您发送评论内容。 必填字段 *

为何看不到我发布的评论?

正在提交, 请稍候...