作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
对插件机制的理解,有了更深的认识。以前总觉得,钩子机制,是Drupal核心最本质,最具特色的一个机制,Drupal之所有这么灵活,就是它本身所待的这种钩子机制。其它模块,如果想与Drupal系统交互,只需要实现相应的钩子就可以了。但是钩子机制本身,也是有很大的局限性的,特别是随着Drupal应用的领域越来越广,这种局限性,就越来越明显的暴露出来。钩子机制有哪些局限性呢?所有的钩子实现都需要放到module文件里面,我们知道,每一个页面,都会加载所有的module文件,把所有的钩子代码都放到module文件里面,使得Drupal消耗的内存非常的大,我们很早就讲到了这个问题,在第一集的第一章里面就介绍了这个问题,这在Drupal里面,一直是一个让人头疼的问题。为了解决这个问题,Drupal7里面实现了注册表的机制,在Drupal7的目标里面包含了这个目标,最初的目标是把所有的文件、钩子函数、普通函数、类、接口,都注册一下,当需要一个函数,一个类的时候,再去加载这个函数所在的文件,这个功能最初实现了,运行良好,但是,后来人们发现这个机制和更底层的缓存技术冲突,好像是opcode缓存,没有办法,这个功能又回退了回去,只实现了部分功能,就是类、接口的注册。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
对字段验证,还有一些思考,关于token的思考,token模块本身不支持字段token,这在Drupal7是一个很大的功能缺失,但是一直没有人解决,我注意到这个问题,是因为我想为Field validation添加更多的token支持的功能时,比如验证一个数字的大小时,我希望这个字段的值大于另一个字段时,这个时候如果有字段token的支持,验证器就会非常的灵活。在第三方模块里面,有两个模块提供的token支持,一个就是token模块,一个是entity_token模块。为了更好的支持字段token,我编写了一个模块http://drupal.org/project/compound_token,但是很遗憾,这个模块的关注度一直很小,而且它与token模块有冲突,我向Drupal核心提交了一个补丁,解决这个冲突,但是我的这种方式,遭到了核心维护者,也是token模块的维护者的反对。其实compound_token更接近于entity_token。有时候,我们有很多好的想法,但是这些想法,在别人看来,非常的幼稚,自己会为得不到承认、认可而感到异常的失落。
此外,我还阅读了Symfony\Validator里面的所有代码,发现自己的想法,实现,很多人早就做了这样的工作,而且比自己做的更好。一直在追踪http://drupal.org/node/1696648的进展,fago和attiks, attiks想往Drupal8里面引入一个验证框架,这样表单验证、实体验证、字段验证都会统一起来,attiks是重新编写的自己实现,很多想法来自于Symfony\Validator,加入了Drupal特有的一些东西,而fago开始是支持attiks的工作的,但是后来又变了卦,转向支持基于Symfony\Validator的实现,而不是重新发明轮子。说实在的,attiks所做的工作,我也能够完成,只是没有他这样的机会,作为一个中国的Drupal开发者,我们是没有多少与这些核心开发者交流的机会的。Attiks所写的程序,想法是来自于Symfony\Validator,其实还有一部分想法,是直接来自于field validation2.X,看着别人拿走了自己的想法,写出来了自己的东西,多少有点吃不到葡萄,说葡萄酸的味道。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
Drupal7对钩子函数的缓加载,还是有改进的,Drupal7里面是这样改进的,通过hook_hook_info,我们就可以把多个钩子函数划分成一组,比如组名叫做rules,那么我们就可以把rules相关的钩子实现,都放到mymodule.rules.inc文件中去。这样当,调用rules相关的钩子时,才会加载mymodule.rules.inc文件。不过,不是很多程序员都了解这个机制,就是很多资深的Drupal开发者,也不了解这个机制。比如,Ubercart里面就没有类似的实现,不过我在uc_ctools模块里面,帮助Ubercart实现hook_hook_info钩子。我们来看看我的实现:
/**
* Implementation of hook_hook_info().
*/
function uc_ctools_hook_info() {
//Hooks provided by uc_cart
foreach (array('add_to_cart', 'add_to_cart_data', 'cart_display', 'cart_pane', 'cart_pane_alter', 'checkout_complete', 'checkout_pane', 'checkout_pane_alter', 'update_cart_item') as $hook) {
$hooks['uc_' . $hook] = array(
'group' => 'ubercart',
);
}
//Hooks provided by uc_file
foreach (array('download_authorize', 'file_action', 'file_transfer_alter') as $hook) {
$hooks['uc_' . $hook] = array(
'group' => 'ubercart',
);
}
//Hooks provided by uc_order
foreach (array('invoice_templates', 'line_item', 'line_item_alter', 'line_item_data_alter', 'order', 'order_actions', 'order_pane', 'order_pane_alter', 'order_product_alter', 'order_product_delete', 'order_product_can_ship', 'order_state') as $hook) {
$hooks['uc_' . $hook] = array(
'group' => 'ubercart',
);
}
//Hooks provided by uc_product
foreach (array('alter', 'class', 'default_classes', 'description', 'description_alter', 'models', 'types') as $hook) {
$hooks['uc_product_' . $hook] = array(
'group' => 'ubercart',
);
}
//Hooks provided by uc_stock
$hooks['uc_stock_adjusted'] = array(
'group' => 'ubercart',
);
//Hooks provided by uc_store
$hooks['tapir_table_alter'] = array(
'group' => 'ubercart',
);
foreach (array('form_alter', 'message', 'store_status') as $hook) {
$hooks['uc_' . $hook] = array(
'group' => 'ubercart',
);
}
//Hooks provided by uc_taxes
$hooks['uc_calculate_tax'] = array(
'group' => 'ubercart',
);
//Hooks provided by uc_payment
foreach (array('payment_entered', 'payment_gateway', 'payment_gateway_alter', 'payment_method', 'payment_method_alter') as $hook) {
$hooks['uc_' . $hook] = array(
'group' => 'ubercart',
);
}
//Hooks provided by uc_quote
foreach (array('shipping_method', 'shipping_type') as $hook) {
$hooks['uc_' . $hook] = array(
'group' => 'ubercart',
);
}
//Hooks provided by uc_shipping
$hooks['uc_shipment'] = array(
'group' => 'ubercart',
);
return $hooks;
}
我把Ubercart核心自带的钩子,都合并到了ubercart这个组里面了,如果你的模块需要实现Ubercart的钩子函数,只需要将具体实现放到mymodule.ubercart.inc文件里面就可以了。
这样,我们就可以缓加载钩子函数了,这是一个办法,一个折中的办法。如果,你了解这一点的话,当你遇到token相关的钩子实现时,就可以放到mymodule.tokens.inc里面,当遇到rules的钩子实现时,把对应的钩子函数放到mymodule.rules.inc里面。
我也是在接触了Drupal7一年以后,才了解到这个机制。开始我都不明白,为什么可以把rules相关的钩子函数放到mymodule.rules.inc里面,我们来看一下rules的实现:
/**
* Implementation of hook_hook_info().
*/
function rules_hook_info() {
foreach(array('plugin_info', 'data_info', 'condition_info', 'action_info', 'event_info', 'file_info', 'evaluator_info', 'data_processor_info') as $hook) {
$hooks['rules_' . $hook] = array(
'group' => 'rules',
);
$hooks['rules_' . $hook . '_alter'] = array(
'group' => 'rules',
);
}
$hooks['default_rules_configuration'] = array(
'group' => 'rules_defaults',
);
$hooks['default_rules_configuration_alter'] = array(
'group' => 'rules_defaults',
);
return $hooks;
}
我们看到,这里分成了两个组,一个是rules,一个是rules_defaults。如果进一步了解的话,还是看一看module_invoke_all, module_implements这两个函数,前者调用了后者,而在module_implements函数里面,参看api.drupal.org。里面包含这样的几段代码:
cache_clear_all('hook_info', 'cache_bootstrap');
…
$hook_info = module_hook_info();
…
$include_file = isset($hook_info[$hook]['group']) && module_load_include('inc', $module, $module . '.' . $hook_info[$hook]['group']);
…
if ($group) {
module_load_include('inc', $module, "$module.$group");
}
读懂了这里的代码,我们就基本上彻底的了解这个机制。有兴趣的读者可以进一步的读一下module_hook_info这个函数里面的代码。在api.drupal.org上面阅读即可。我原来一直以为token模块实现了钩子hook_hook_info,但是在token模块里面找了又找,就是找不到,后来,发现它的实现放到了system模块里面:
function system_hook_info() {
$hooks['token_info'] = array(
'group' => 'tokens',
);
$hooks['token_info_alter'] = array(
'group' => 'tokens',
);
$hooks['tokens'] = array(
'group' => 'tokens',
);
$hooks['tokens_alter'] = array(
'group' => 'tokens',
);
return $hooks;
}
我就是借鉴了这里的用法,在uc_ctools模块里面,把Ubercart的相关钩子归成了一个组。
hook_hook_info这种方式,部分解决了问题,由于知道的人不多,很多人仍然将相应的钩子实现放到module文件里面。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
插件的方式,就能很好的解决这个缓加载的问题,Drupal在默认的情况下,是不会去加载插件的,只有当需要的时候,才去加载相应的插件。在field validation 1.x里面,我们也拆分成了多个文件,比如field_validation.validators.inc,field_validation.rules.inc,我们把所有的验证器都放到了field_validation.validators.inc中,很多人以为,这样也实现了缓加载,其实不然,在field_validation.module文件里面,代码是这样写的:
include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'field_validation') . '/' . 'field_validation.validators.inc';
include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'field_validation') . '/' . 'field_validation.rules.inc';
这意味着在加载field_validation.module文件的同时,会自动加载field_validation.validators.inc,field_validation.rules.inc两个文件。这种方式是没有实现缓加载的,不信的话,你可以测试一下,新写一个模块,在里面直接调用field_validation.validators.inc,field_validation.rules.inc文件里面的方法,此时是可以直接调用的,这意味着这两个文件也被加载了进来。
我们大致算一下,在1.x版本里面,普通页面请求过来以后,field validation加载到内存里面的文件大小有:
12 K(field_validation.module) + 2k (field_validation.rules.inc)+ 30K (field_validation.validators.inc)= 44K
而在2.x里面,禁用了field_validation_ui模块以后,总共加载到内存里面的大小:
3 K(field_validation.module) = 3K
如果算上field_validation_ui模块,加载到内存里面的大小:
3 K(field_validation.module)+ 7K(field_validation_ui.module )= 10K
在这两种情况下,分别少加载了41K、34K大小的文件到内存中去。这就是field validation2.X对 1.X版本的一个重要改进。
那么在调用一个验证器的情况下,又会加载多少文件大小内,显然在field validation1.X下面,加载的大小仍然为44K,而在2.x里面,禁用了field_validation_ui模块以后,总共加载到内存里面的大小:
3 K(field_validation.module)+ 3K(field_validation_numeric2_validator.inc) = 6K
验证器的文件大小不一样,3K取一个大致的平均数,此时也只有6K大小。如果算上field_validation_ui的module文件,也只有13K大小。
在调用验证器的情况,我们仍然节省了38K或31K大小的内存。总之,我们对性能的改进,是有所帮助的。
在维护1.x版本的时候,每当我坐一个小小的修改时,生怕改动了其它验证器,而在2.x里面,一个验证器,一个文件,管理起来非常方便,我修改这个验证器,肯定影响不到其它的验证器,至少不用提心吊胆了。采用插件的形式,更好理解,其他程序员学习的成本更低了,他只需要复制一个插件,重命名,改一下里面关键的验证逻辑,然后将修改后的验证器,放到field_validation\plugins\validator目录下面,就可以正常工作了,这样的好处,就是不用修改已有的文件。以前加一个验证器的时候,总要修改field_validation.validators.inc文件,现在只需要创建一个自己独立的文件就可以了。
插件的方式,比钩子的方式,要灵活很多,是钩子方式的一种进步。在我实现了2.0-beta1以后,我有了更多的关于插件的想法,比如核心里面区块,其实也可以转换为插件;比如Ubercart里面的支付方法、运送方法、窗格,也都可以转换为插件的形式,这就是uc_ctools的由来。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
什么钩子可以转换为插件,通常module_invoke实现的钩子,都可以转换为插件,module_invoke_all的钩子,部分可以转换为这样的插件,部分不可以。我写出2.0-beta1的时候,Drupal8核心还没有引入插件系统的,大概又过来3个月左右,插件机制进入了Drupal8的内核,Drupal核心里面的区块、图片样式、RSS聚合器模块里面的相关钩子,都转换成为了插件的形式。
在Drupal8里面,很多使用module_invoke_all的钩子,无法转换为插件的钩子,都可以转为面向对象的事件分发的机制。字段类型、验证框架等很多Drupal核心的里面的系统,都在采用插件的形式。随着插件机制的普及,事件分发机制的流行,Drupal核心里面的很多钩子,都会逐步的消失,被取代。是否把所有的module_invoke_all都转为事件分发机制,这在Drupal8里面是没有要求的,就是说,当需要转的时候,条件成熟了,再转过来,有这么一个渐进的过程。至于Drupal的钩子是否会在将来的版本里面完全消失,这个一时半会还是不会的,将来,钩子机制将会作为插件系统的一个很好的补充,而存在。