Drupal 一个CMS(Content Management System),Drupal的模块系统实现了一个钩子系统,什么是钩子?我已经开始了解钩子是允许您在多个地方调用。让我们先来看看一个PHP函数的编写和应用:
1 2 3 4 5 6 7 8 9 10 11 | function do_something($thing){ //这里写一些代码实现一些功能 //例如插入数据库资料 //标记一个物体 //唱一首歌。。。。 return $something} ... 程序的其他地方 ...do_something(); |
一旦我们编写函数和部署代码,我们不能轻易改变它。也就是说,它虽然很容易被编辑,但是其他程序员不敢乱改,因为他们不懂这个函数的工作原理。所以要很好的跟其他程序员沟通,就必须通过很详细的代码注释。即使你有一个团队并且拥有完善的沟通,也有问题因为修改函数而产生副作用。所以就有一个概念:永远不改现有代码,除非你已经完全了解代码所做的任何事情。
我们试图解决问题,当“一些事情”发生,我们想采取额外的动作,需求的变化,就需要新的功能加入,这对于一个开发人员来说,我们得要随时应付这些新功能的加入。那么,我们如何不改变核心代码,但又能实现这些功能呢?
钩子系统很好的解决了这个问题。当你想做些什么,不是直接调用一个函数,而是调用一个钩子。
1 2 3 4 5 | //老方法,直接call do_something函数//do_something(); //新方法,调用do_something的 hookmodule_invoke_all('do_something'); |
当一个hook 被调用了,Drupal 会
- 获得所有安装并启用模块列表
- 问每一个模块:你有do_something 这个 hook吗?
- 如果有,那么Drupal在这些模块实现了钩子调用函数
然而如果作为一个核心的开发者,你可以达到 “你想要”的同时,还能让其他开发者通过钩子实现“他们想要”。
举个例子?
这些都是抽象的概念,我们需要举个例子说明
- 编写代码来调用一个钩名叫helloworld
- 在module_a中编写代码以实现helloworld的钩子
- 在module_b中编写代码以实现helloworld的钩子
Drupal是单入口的结构,为了不影响正常访问,我们把index.php 复制一份为drupalla.php,并且改一下代码
1 2 3 4 5 6 | define('DRUPAL_ROOT', getcwd());require_once DRUPAL_ROOT . '/includes/bootstrap.inc';drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);module_invoke_all('helloworld');echo '<p>Done</p>';###menu_execute_active_handler(); |
这里我们注释了menu_execute_active_handler函数,因为这会呈现出一个Drupal页面,对于测试页面,我们不希望有过多的元素。这是为什么我注释了它。
只要我们有调用drupal_bootstrap 就可以,这将会载入了Drupal系统,包括所有为Drupal运行必要的代码。
在这个基础上,我们加了两行简单的代码:
1 2 | module_invoke_all('helloworld');echo '<p>Done</p>'; |
将会输出 Done,而module_invoke_all比较有趣,虽然我们已经调用了一个钩子,但没有一个模块实现这个钩子,所以这个调用,不会有任何事情发生。
实现一个钩子:
在本文的前面,我们创建了两个模块。模块A和模块B。确保这些模块是开启状态的。
然后打开 模块 A的 .module 文件,增加
1 2 3 4 5 6 | #File: sites/all/modules/module_a/module_a.module<?phpfunction module_a_helloworld(){ echo "<p>Our friends at ".__FUNCTION__." want to say Hello World</p>";} |
清空缓存
然后你会发现输出中多了一行
1 | Our friends at module_a_helloworld want to say Hello World |
现在你已经实现了第一个钩子,所有需要有:
- 在.module文件创建一个函数
- 函数名必须是你的模块名开头,如模块A的模块名是module_a,所以它的hook必须是module_a_xxx(),例子:module_a_helloworld()
- 在你想调用的地方,引用这个钩子。
让我们在模块B也做同样的操作,在module_b.module 里面增加
1 2 3 4 5 6 | #File: sites/all/modules/module_b/module_b.module<?phpfunction module_b_helloworld(){ echo "<p>Our friends at ".__FUNCTION__." want to say Hello World</p>";} |
清空缓存,再用浏览器打开drupalla.php这个时候,会看到输出了两条信息
1 2 3 | Our friends at module_a_helloworld want to say, Hello WorldOur friends at module_b_helloworld want to say, Hello World |
所以module_invoke_all('helloworld'); 会循环的读取所有模块的 helloworld 这个hook。
钩子返回值
让我们稍微改一下drupalla.php文件
1 2 | $results = module_invoke_all('helloworld');var_dump($results); |
输出结果是
1 2 3 4 5 6 | Our friends at module_a_helloworld want to say, Hello WorldOur friends at module_b_helloworld want to say, Hello Worldarray empty |
module_invoke_all返回的是一个空数组。让我们稍微再改改两个hook的代码,将里面的echo改为return
1 2 3 4 5 6 7 8 9 10 11 12 | #File: sites/all/modules/module_a/module_a.module...function module_a_helloworld(){ return "<p>Our friends at ".__FUNCTION__." want to say, Hello World</p>";}...#File: sites/all/modules/module_b/module_b.modulefunction module_b_helloworld(){ return "<p>Our friends at ".__FUNCTION__." want to say, Hello World</p>";} |
再看看drupalla.php,输出结果是
1 2 3 | array 0 => string '<p>Our friends at module_a_helloworld want to say, Hello World</p>' (length=65) 1 => string '<p>Our friends at module_b_helloworld want to say, Hello World</p>' (length=65) |
调用一个钩将总是返回一个数组,而且那个数组将包含值的每个模块的钩实现返回值。如果一个钩子没返回值,那数组就会是空数组。
我们再次改改模块A的代码如下,看看结果会是怎样
1 2 3 4 | function module_a_helloworld(){ return array("one","two","three");} |
这时候返回的结果是
1 2 3 4 5 | array 0 => string 'one' (length=3) 1 => string 'two' (length=3) 2 => string 'three' (length=5) 3 => string '<p>Our friends at module_b_helloworld want to say, Hello World</p>' (length=65) |
可以看出,Drupal有合并钩子实现的返回值到调用返回数组,而不是返回一个嵌套数组,而如果你数组使用一个非顺序结构,如
1 2 3 4 | function module_a_helloworld(){ return array("foo"=>"one","baz"=>"two","bar"=>"three","function"=>__FUNCTION__);} |
返回的结果变成:
1 2 3 4 5 6 | array 'foo' => string 'one' (length=3) 'baz' => string 'two' (length=3) 'bar' => string 'three' (length=5) 'function' => string 'module_a_helloworld' (length=18) 0 => string '<p>Our friends at module_b_helloworld want to say, Hello World</p>' (length=65) |
它返回一个关联数组的键合并到我们调用的返回值。所以如果同时调用了两个hook,而两个hook中有一个key是一样的,会是怎么的结果?让我们试一下
1 2 3 4 5 6 7 8 9 10 11 12 | #File: sites/all/modules/module_a/module_a.module...function module_a_helloworld(){ return array ("foo"=>"one","baz"=>"two","bar"=>"three","function"=>__FUNCTION__);}...#File: sites/all/modules/module_b/module_b.modulefunction module_b_helloworld(){ return array('function'=>__FUNCTION__);} |
两个钩实现是返回一个数组和一个关键的命名“function”,重载页面,结果如下
1 2 3 4 5 6 7 8 | array 'foo' => string 'one' (length=3) 'baz' => string 'two' (length=3) 'bar' => string 'three' (length=5) 'function' => array 0 => string 'module_a_helloworld' (length=18) 1 => string 'module_b_helloworld' (length=18) |
因此,如果有一样的关键词命名,Drupal会合并成一个嵌套数组,当你需要调用钩子,这些返回值的结构,是非常好重要的。
Hooks(钩子) vs. Events(事件)
返回值是钩子与传统event/observer系统的不同之处,一个比较简单的句子区分两者的区别
Event/Observers :嘿,只是让你知道,事情发生了。
Hooks :嘿,我们正在做这件事,你想成为它的一部分吗?
调用钩子
有些时候,你不想在所有模块中寻找所有相关的钩子,这样对性能有一定的影响,我们只想要直接调用其中一个钩子就好了,这个时候我们可以用module_invoke,
1 | $results = module_invoke('module_a','helloworld'); |
这里只调用了module_a的钩子,而module_b的没有调用。
接下来是带参数的钩子,module_invoke 跟 module_invoke_all 都支持带参数的钩子,
1 2 | module_invoke_all('helloworld','one','two','three');module_invoke('module_a','helloworld','one','two','three'); |
这些参数,反过来,传递给钩的实现。请看下面代码
1 2 3 4 | function module_a_helloworld($param, $another_param, $third){ return array('joined'=>implode('##',array($param, $another_param, $third)));} |
字符串‘one’,’two’, ‘three’是对应参数$param, $another_param, $third。
其实背后drupal是通过php的func_get_args 函数实现的。这个实现提出了一个问题,这就是我们所说的最后一件事。
有时,你希望你的钩实现能够改变一些你正在使用的数据结构。在我们2011 / PHP 5的世界里面,我们很可能实现这个通过传递一个对象。
1 2 3 4 | $object = new SomeClass();module_invoke('module_a','helloworld',$object);//we've got an object that's been modified by our hook implementations$object; |
PHP 5中对象的引用,这意味着钩实现可能让一切他们想要的变化。
然而,Drupal是有10年历史了。开始时候还是一个PHP 4世界,一切都是通过复制。通过在大数组(甚至PHP 4的原始对象)的函数,然后返回他们伴随着潜在的性能问题,因为另一个副本的数组或对象需要。PHP提供了一个解决方案,允许你声明你的函数在这样一种方式,通过引用传递参数
1 2 3 4 | function somefunction(&$param){ //& 参数意味着如果我们在函数改变了它的值,它将影响全局。} |
& 是什么作用?下面举个例子
1 2 3 4 5 6 | $a = 1;function go(&$b) { $b = $b + 1;}go($a);echo $a; |
========系统输出2,因为函数直接修改了$a的数值
1 2 3 4 5 6 | $a = 1;function go($b) { $b = $b + 1;}go($a);echo $a; |
=========系统输出1,因为$b = $b + 1只是在函数内部修改,外部不生效
这个例子,大家应该懂得&符号的作用了吧?
然而因为because module_invoke 跟module_invoke_all 都是用func_get_args,这是不可能通过钩调用传递值的引用。
我理解为:
drupal_alter 是hook调用前给你最后一次参数修改的机会,达到改变钩子的调用状态效果,所以可以理解为钩子调用前的修改;
这让Drupal社区想出了两种解决方案
第一种是 drupal_alter 函数,这是其中一种替代方法来调用钩子实现。
1 | drupal_alter('helloworld',$data); |
将会调用hook helloworld_alter,变量$data将会引致函数外的值改变。在Drupal 7中可以通过三个参数作为引用参数。
1 | drupal_alter('helloworld',$data, $foo, $bar); |
另一个需要注意的是drupal_alter不返回任何值。完全是为了允许模块钩子改变变量。
破坏封装(Breaking Encapsulation)
因为drupal_alter的约束,这里有第二种方法调用。Drupal提供一个功能叫做module_implements,它将返回一个列表的模块,实现一个特定的钩。试试下面的
1 2 | $results = module_implements('helloworld');var_dump($results); |
将会返回两个模块的数组
1 2 3 | array 0 => string 'module_a' (length=7) 1 => string 'module_b' (length=7) |
当有人想通过引用传递变量变成一个钩子实现和不使用alter命名语法,或者他们想钩实现来返回一些东西,你就会看到这样的事情
1 2 3 4 5 6 7 8 9 10 11 12 | $hook = 'helloworld';$etc = array('one','two');$param_by_ref = 'four';$all = array();foreach(module_implements($hook) as $module){ $function = $module . '_' . $hook; $single_hook_return = $function($param_by_ref, $etc); $all = array_merge($all, $single_hook_return);}var_dump($all);echo '<p>Done</p>'; |
由module_invoke_all提供的功能给复制了,但没直接用module_invoke_all。
结语
在Drupal开发中,你大部分时间将会是用钩子开发模块和应用第三方模块,同时通过Drupal 核心api,每个Api提供很多不同的钩子实现,每个都有他们的功能。灵活运用drupal的这些,将会事半功倍。
评论
原来钩子返回的是数组,谢谢分享。
调用一个钩将总是返回一个数组,而且那个数组将包含值的每个模块的钩实现返回值。
学习中。。。
学习中。。。
查了一下d7 drupal_alter的API是:
drupal_alter($type, &$data, &$context1 = NULL, &$context2 = NULL);