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社区想出了两种解决方案
第一种是 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的这些,将会事半功倍。