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的 hook module_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 <?php function 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 <?php function 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 World Our 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 World Our friends at module_b_helloworld want to say, Hello World array 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.module function 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.module function 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);