作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
Field validation一直都是在进步的,就像有一个用户所说的,每当他有了新的需求的时候(关于Field validation的),Field validation就包含了这样的新特性,恰好满足他的需求。
我们终于要迎来RC版了,在这个版本中,添加了导出验证规则的功能。其实我很早就想实现这个功能了,但总觉得自己还没有达到这样的水平,另外这方面的文档也没有多少。
验证规则的导入、导出,这里是基于Ctools模块的,很早我就想基于Ctools模块实现了,在此以前,我做了大量的准备工作,读了很多英文的文档,看了很多相关的例子,最终,决定动手了。其实还有一个想法,就是采用Ctools插件的形式,来实现验证器,这个想法也想了很久了,每当看到Feeds模块的插件形式,我便希望按照这种方式进行改进。其实在我编写Field validation beta1版的时候,这样的想法,就有了。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
Ctools自带了一个帮助文档,里面有导入导出的介绍,在ctools\help下面有export.html和export-ui.html两个介绍,这对我的开发非常有帮助。我至少读了两遍。除此以外,我还找到了几个实现了Ctools export的模块,作为例子,阅读了它们的相关代码。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们来看一下,Field validation的实现,首先是schema的变动:
function field_validation_schema() {
$schema['field_validation_rule'] = array(
'description' => 'Stores rule definitions',
'export' => array(
'key' => 'name',
'key name' => 'Name',
'primary key' => 'ruleid',
'identifier' => 'rule',
'default hook' => 'default_field_validation_rule',
'api' => array(
'owner' => 'field_validation',
'api' => 'default_field_validation_rules',
'minimum_version' => 1,
'current_version' => 1,
),
),
'fields' => array(
'ruleid' => array(
'type' => 'serial',
'description' => 'Unique identifier of the validation rule',
'unsigned' => TRUE,
'not null' => TRUE,
),
'rulename' => array(
'type' => 'varchar',
'description' => 'Name of the validation rule',
'not null' => TRUE,
'default' => '',
'length' => 255,
),
'name' => array(
'type' => 'varchar',
'description' => 'Machine name of the validation rule',
'not null' => TRUE,
'default' => '',
'length' => 32,
),
…..
),
);
return $schema;
}
这里面有两处变化,一个就是为验证规则添加了一个机读名字,使用这个机读名字作为导入导出同步的唯一ID。这个大家都很好理解,对应代码如下:
'name' => array(
'type' => 'varchar',
'description' => 'Machine name of the validation rule',
'not null' => TRUE,
'default' => '',
'length' => 32,
),
另一处,就是添加了一个export键,它的值为一个数组:
'export' => array(
'key' => 'name',
'key name' => 'Name',
'primary key' => 'ruleid',
'identifier' => 'rule',
'default hook' => 'default_field_validation_rule',
'api' => array(
'owner' => 'field_validation',
'api' => 'default_field_validation_rules',
'minimum_version' => 1,
'current_version' => 1,
),
),
这个数组,所包含的键的含义,我们这里简单介绍一下用到的,其余没有用到的,可以参看Ctools的帮助文档,来看一下这里用到的:
key: 就是哪个字段是唯一键,在数据库中,导出的文件中,都是唯一的
key name:唯一键的用户可读名字
primary key: 就是主键,是一个数字ID,在数据库存储时用到。
identifier :标识符,导出的验证规则对象,它所在的变量
default hook :就是默认钩子,通过这个钩子,我们可以定义一个验证规则。
api:它下面包含4个键,owner,就是当前模块的名字;api,钩子,用来定义验证规则;minimum_version就是api的最小版本;current_version,api的当前版本。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
在module文件中,我们添加了以下代码:
/**
* Implementation of hook_ctools_plugin_directory().
*/
function field_validation_ctools_plugin_directory($module, $plugin) {
if ($module == 'ctools' && $plugin == 'export_ui') {
return 'plugins/' . $plugin;
}
}
/**
* Implementation of hook_ctools_plugin_api().
*
* Tell Ctools that we support the default_field_validation_rules API.
*/
function field_validation_ctools_plugin_api($owner, $api) {
if ($owner == 'field_validation' && $api == 'default_field_validation_rules') {
return array('version' => 1);
}
}
hook_ctools_plugin_directory用来定义插件的位置,hook_ctools_plugin_api用来告诉Ctools,我们支持通过default_field_validation_rules定义验证规则。
这里我们需要区分一点,default_field_validation_rules和default_field_validation_rule的区别,前面写作的时候把两者都作为了钩子,这是不对的,正确的理解应该是这样的,default_field_validation_rule是一个钩子,可以用来定义规则,default_field_validation_rules,则对应于field_validation. default_field_validation_rules.inc文件,在这个文件中,我们可以实现钩子default_field_validation_rule。有点绕口,为了更好的理解两者之间的关系,建议大家打开RC1 版中的field_validation.default_field_validation_rules.inc文件,里面的代码如下:
/**
* Implementation of hook_default_field_validation_rule().
*
* Provide default validation rules.
*/
function field_validation_default_field_validation_rule() {
$export = array();
$rule = new stdClass;
$rule->api_version = 1;
$rule->name = 'body_min_words';
$rule->rulename = 'Body Min words';
$rule->field_name = 'body';
$rule->col = 'value';
$rule->entity_type = 'node';
$rule->bundle = 'page';
$rule->validator = 'min_words';
$rule->data = '2';
$rule->error_message = t('You should enter at least two words.');
$export['body_min_words'] = $rule;
return $export;
}
这个函数里面的$rule变量,就对应于我们前面的identifier。现在,我们的验证规则,不仅仅存放在了数据库中,还可以通过代码来定义。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们获取验证规则的代码也需要相应的调整。我们来看一个根据机读名字加载验证规则的API函数:
function field_validation_rule_load($name) {
// Use Ctools export API to fetch this rule.
ctools_include('export');
$result = ctools_export_load_object('field_validation_rule', 'names', array($name));
if (isset($result[$name])) {
return $result[$name];
}
}
这里面用到的,这两句代码,是Ctools加载可导出对象的标准用法。
ctools_include('export');
$result = ctools_export_load_object('field_validation_rule', 'names', array($name));
在field_validation_field_attach_validate里面,我们用来加载验证规则的代码是这样的:
ctools_include('export');
$rules = ctools_export_load_object('field_validation_rule', 'conditions', array('entity_type'=>$entity_type, 'bundle' => $bundle));
通过这两段代码,我们可以看到ctools_export_load_object的第一个参数,是加载的对象,这里也就是field_validation_rule,我们在schema里面创建数据库表的时候,用的就是这个名字。第二个参数可以是'names',此时第三个参数为一个数组,里面包含了一组机读名字;第二个参数还可以是'conditions',此时第三个参数也是一个数组,里面包含了多个限制条件。有兴趣的可以读读ctools_export_load_object的源代码,看看Ctools的作者怎么把这些条件转化为对应的SQL的。
我们使用ctools_export_load_object加载的都是对象的形式,以前我们的验证规则,一直都采用数组的形式,所以在field_validation_field_attach_validate里面,我添加了一句从对象到数组转换的代码:
$rule = (array)$rule_obj;
之所以这样做,是因为前面的代码都是数组,如果改为对象的话,要改很多个小地方。为了兼容起见,将加载的验证规则对象,转换为了数组的形式。
在field_validation.admin.inc里面,也有用到加载验证规则的地方,我们也做了相应的修改。由于我们使用了ctools_export_load_object,所以field_validation.rules.inc里面的代码,现在就有些多余了,我们在后面的版本中删除了这个文件。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
再往下,就是在field_validation\plugins\export_ui里面创建field_validation_export_ui.inc文件,这里的代码有点长:
/**
* Define this Export UI plugin.
*/
$plugin = array(
'schema' => 'field_validation_rule',
'access' => 'administer site configuration',
'menu' => array(
'menu item' => 'field_validation',
'menu prefix' => 'admin/structure',
'menu title' => 'Field Validation',
'menu description' => 'Administer Field Validation rules.',
),
'title singular' => t('rule'),
'title plural' => t('rules'),
'title singular proper' => t('Field Validation rule'),
'title plural proper' => t('Field Validation rules'),
'form' => array(
'settings' => 'field_validation_ctools_export_ui_form',
'validate' => 'field_validation_ctools_export_ui_form_validate',
'submit' => 'field_validation_ctools_export_ui_form_submit',
),
);
/**
* Define the add/edit form of validation rule.
*/
function field_validation_ctools_export_ui_form(&$form, &$form_state) {
ctools_include('export');
$rule = $form_state['item'];
$default_rulename = isset($rule->rulename) ? $rule->rulename : '';
$default_entity_type = isset($rule->entity_type) ? $rule->entity_type : '';
$default_bundle = isset($rule->bundle) ? $rule->bundle : '';
$default_field_name = isset($rule->field_name) ? $rule->field_name : '';
$default_col = isset($rule->col) ? $rule->col : '';
$default_validator = isset($rule->validator) ? $rule->validator : '';
$default_data = isset($rule->data) ? $rule->data : '';
$default_error_message = isset($rule->error_message) ? $rule->error_message : '';
//print debug($form_state);
$default_rulename = isset($form_state['values']['rulename']) ? $form_state['values']['rulename'] : $default_rulename;
$default_entity_type = isset($form_state['values']['entity_type']) ? $form_state['values']['entity_type'] : $default_entity_type;
$default_bundle = isset($form_state['values']['bundle']) ? $form_state['values']['bundle'] : $default_bundle;
$default_field_name = isset($form_state['values']['field_name']) ? $form_state['values']['field_name'] : $default_field_name;
$default_col = isset($form_state['values']['col']) ? $form_state['values']['col'] : $default_col;
$default_validator = isset($form_state['values']['validator']) ? $form_state['values']['validator'] : $default_validator;
$default_data = isset($form_state['values']['data']) ? $form_state['values']['data'] : $default_data;
$default_error_message = isset($form_state['values']['error_message']) ? $form_state['values']['error_message'] : $default_error_message;
//print debug($rule);
$form['rulename'] = array(
'#type' => 'textfield',
'#title' => t('Rule name'),
'#default_value' => $default_rulename,
'#required' => TRUE,
'#size' => 60,
'#maxlength' => 255,
// '#weight' => 1,
);
$entity_type_options = array(
'' => 'Choose an entity type',
);
$entity_types = entity_get_info();
foreach ($entity_types as $key => $entity_type) {
$entity_type_options[$key] = $entity_type['label'];
}
$form['entity_type'] = array(
'#type' => 'select',
'#options' => $entity_type_options,
'#title' => t('Entity type'),
'#default_value' => $default_entity_type,
'#required' => TRUE,
'#ajax' => array(
'callback' => 'field_validation_entity_type_callback',
'wrapper' => 'validation-rule-wrapper-div',
'method' => 'replace',
'effect' => 'fade',
),
);
$bundle_options = array(
'' => 'Choose a bundle',
);
// print debug($entity_types['node']);
$bundles = !empty($entity_types[$default_entity_type]['bundles']) ? $entity_types[$default_entity_type]['bundles'] : array();
// $bundles = !empty($entity_types['node']['bundles']) ? $entity_types['node']['bundles'] : array();
// print debug($default_entity_type);
foreach ($bundles as $key => $bundle) {
$bundle_options[$key] = $bundle['label'];
}
$form['bundle'] = array(
'#type' => 'select',
'#options' => $bundle_options,
'#title' => t('Bundle name'),
'#default_value' => $default_bundle,
'#required' => TRUE,
'#prefix' => '<div id="bundle-wrapper-div">',
'#suffix' => '</div>',
'#ajax' => array(
'callback' => 'field_validation_bundle_callback',
'wrapper' => 'validation-rule-wrapper-div',
'method' => 'replace',
'effect' => 'fade',
),
);
$field_name_options = array(
'' => 'Choose a field',
);
$instances = array();
if(!empty($default_entity_type) && !empty($default_bundle)){
$instances = field_info_instances($default_entity_type, $default_bundle);
}
//$instances = field_info_instances('node', 'article');
foreach ($instances as $key => $instance) {
$field_name_options[$key] = $instance['label'];
}
if(!in_array($default_field_name, array_keys($field_name_options))){
$default_field_name = '';
}
$form['field_name'] = array(
'#type' => 'select',
'#options' => $field_name_options,
'#title' => t('Field name'),
'#default_value' => $default_field_name,
'#required' => TRUE,
'#prefix' => '<div id="field-name-wrapper-div">',
'#suffix' => '</div>',
'#ajax' => array(
'callback' => 'field_validation_field_name_callback',
'wrapper' => 'col-wrapper-div',
'method' => 'replace',
'effect' => 'fade',
),
);
$field = field_info_field($default_field_name);
//print debug($field);
$col_options = array(
'' => t('Choose a column'),
);
$columns = !empty($field['columns']) ? $field['columns'] : array();
foreach ($columns as $key => $column) {
$col_options[$key] = $key;
}
if(!in_array($default_col, array_keys($col_options))){
$default_col = '';
}
$form['col'] = array(
'#type' => 'select',
'#options' => $col_options,
'#title' => t('Column'),
'#description' => t('A column defined in the hook_field_schema() of this field.'),
'#default_value' => $default_col,
'#required' => TRUE,
'#weight' => 2,
'#prefix' => '<div id="col-wrapper-div">',
'#suffix' => '</div>',
);
$validator_options = array(
'' => 'Choose a validator',
);
$validators = field_validation_get_validators();
foreach ($validators as $validator_key => $validator_info) {
$validator_options[$validator_key] = $validator_info['name'];
}
$form['validator'] = array(
'#type' => 'select',
'#options' => $validator_options,
'#title' => t('Validator'),
'#description' => t('A column defined in the hook_field_schema() of this field.'),
'#default_value' => $default_validator,
'#required' => TRUE,
'#weight' => 3,
'#ajax' => array(
'callback' => 'field_validation_validator_callback',
'wrapper' => 'data-wrapper-div',
'method' => 'replace',
'effect' => 'fade',
),
);
$form['data'] = array(
'#type' => 'textfield',
'#title' => t('Config Data'),
'#required' => FALSE,
'#size' => 60,
'#maxlength' => 255,
'#default_value' => $default_data,
'#weight' => 4,
'#prefix' => '<div id="data-wrapper-div">',
'#suffix' => '</div>',
);
//$rule_validator = $validators[$default_validator];
if (isset($validators[$default_validator]['custom_data']) && is_array($validators[$default_validator]['custom_data'])) {
$required = isset($validators[$default_validator]['custom_data']['required']) ? $validators[$default_validator]['custom_data']['required'] : TRUE;
$form['data']['#title'] = isset($validators[$default_validator]['custom_data']['label']) ? $validators[$default_validator]['custom_data']['label'] : t('Config Data');
$form['data']['#description'] = isset($validators[$default_validator]['custom_data']['description']) ? $validators[$default_validator]['custom_data']['description'] : t('Config Data');
$form['data']['#required'] = ($required !== FALSE) ? TRUE : FALSE;
}
$form['error_message'] = array(
'#type' => 'textfield',
'#title' => t('Custom error message'),
'#description' => t("Specify an error message that should be displayed when user input doesn't pass validation"),
'#required' => TRUE,
'#size' => 60,
'#maxlength' => 255,
'#default_value' => $default_error_message,
'#weight' => 5,
);
$form['#prefix'] = '<div id="validation-rule-wrapper-div">';
$form['#suffix'] = '</div>';
}
/**
* Validation handler for the validation rule add/edit form.
*/
function field_validation_ctools_export_ui_form_validate($form, &$form_state) {
$values = $form_state['values'];
}
/**
* Submit handler for the preset edit form.
*/
function field_validation_ctools_export_ui_form_submit($form, &$form_state) {
$values = $form_state['values'];
}
function field_validation_entity_type_callback($form,&$form_state){
return $form;
}
function field_validation_bundle_callback($form,&$form_state){
return $form;
}
function field_validation_field_name_callback($form,&$form_state){
return $form['col'];
}
function field_validation_validator_callback($form,&$form_state){
return $form['data'];
}
最上面的plugin,这是Ctools插件的常用定义方法,代码是从别的地方复制过来的,我只是改了一下名字而已,把对应的改为Field validation。
而field_validation_ctools_export_ui_form,就是一个普通的表单,我在里面使用了AJAX技术。这个表单是用来在admin/structure/field_validation这个管理界面,添加/编辑验证规则使用的。这样在RC1版里面,我们提供了两种方式,来添加验证规则,一种是原来编写的admin.inc里面的,一种就是这种基于Ctools的方式。在这个表单里面使用Drupal自带的AJAX,Ctools早期的版本是不支持的,我写这个功能的时候,恰好支持了这个特性。
这里面,使用了函数entity_get_info来获取所有的实体信息,返回的实体信息数组里面,也包含每个实体里面的bundle(包)信息。而获取某类实体下面的bundles信息,则采用下面的代码:
$bundles = !empty($entity_types[$default_entity_type]['bundles']) ? $entity_types[$default_entity_type]['bundles'] : array();
还有一段代码,也值得学习一下,这段代码的作用是,我们获取一个字段上面包含多少个columns,让用户选择column,而不是输入,这在RC1 里面是一个改进,我以前是不知道这种方式的:
$field = field_info_field($default_field_name);
$col_options = array(
'' => t('Choose a column'),
);
$columns = !empty($field['columns']) ? $field['columns'] : array();
foreach ($columns as $key => $column) {
$col_options[$key] = $key;
}
有关这里的AJAX效果实现,这里就不介绍了,参看第一集的省市县三级联动。验证规则的导出功能实现以后,让我欣喜不已,不过很快发现一个小问题,就是导出验证规则的时候,将ruleid也导出来了。在Ctools的作者的帮助下,很快就修正了这个问题:
'ruleid' => array(
'type' => 'serial',
'description' => 'Unique identifier of the validation rule',
'unsigned' => TRUE,
'not null' => TRUE,
'no export' => TRUE,
),
这里面,加了一个键'no export',并将它设置为TRUE,这样就可以避免导出ruleid了。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
实现了验证规则的导出以后,我对Ctools的理解又加深了一步,想着自己能够基于Ctools实现导出功能,一定也能够实现插件形式的验证器。这个想法酝酿了很久了,直到有一天,我看到了这篇文章http://www.ygerasimov.com/ctools-plugins-system,写的很详细。我之所以能够找到这篇文章,就是因为我有这个想法,我把Ctools的相关文档看了几遍,看了几个基于Ctools插件系统的模块,比如Feeds模块。最后找到了这篇文章。这是services模块的一个维护者,所写的文章,services模块也是基于Ctools插件系统的。
这篇文章英文的,里面讲的例子,很简单,我们小学学的加减乘除,他在这里把这个加法、减法、乘法、除法,处理成为了Ctools的插件,程序也非常简单。
当我看完这个例子以后,认真的读完它所有的代码以后,我的把验证器处理成为插件的想法,马上就要变成现实了。我花了1-2天的时间,整出来了2.0 alpha1,又花了3-5天的时间,整出了2.0beta1。在一个星期内,把这个想法变成了现实。我们来看一下2.0-alpha1的代码:
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
/**
* Implements hook_schema().
*/
function field_validation_schema() {
$schema['field_validation_rule'] = array(
'description' => 'Stores rule definitions',
'export' => array(
'key' => 'name',
'key name' => 'Name',
'primary key' => 'ruleid',
'identifier' => 'rule',
'default hook' => 'default_field_validation_rule',
'api' => array(
'owner' => 'field_validation',
'api' => 'default_field_validation_rules',
'minimum_version' => 2,
'current_version' => 2,
),
),
'fields' => array(
'ruleid' => array(
'type' => 'serial',
'description' => 'Unique identifier of the validation rule',
'unsigned' => TRUE,
'not null' => TRUE,
),
'rulename' => array(
'type' => 'varchar',
'description' => 'Name of the validation rule',
'not null' => TRUE,
'default' => '',
'length' => 255,
),
'name' => array(
'type' => 'varchar',
'description' => 'Machine name of the validation rule',
'not null' => TRUE,
'default' => '',
'length' => 32,
),
'field_name' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => ''
),
'col' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => 'value'
),
'entity_type' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => ''
),
'bundle' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => ''
),
'validator' => array(
'type' => 'varchar',
'description' => 'The validator key',
'not null' => TRUE,
'default' => '',
'length' => 255,
),
'settings' => array(
'type' => 'text',
'size' => 'big',
'description' => 'Serialized settings for the validator to be used',
'serialize' => TRUE,
'object default' => array(),
),
'error_message' => array(
'type' => 'varchar',
'description' => 'Rule error message',
'not null' => FALSE,
'length' => 255,
),
),
'primary key' => array('ruleid'),
'indexes' => array(
'field_name_bundle' => array('field_name', 'entity_type', 'bundle'),
),
);
return $schema;
}
与1.x相比,这里将data字段改为了settings,并且使用了序列化的形式,这一点是从Views/Panels里面学来的,好处是我们可以在settings里面定义多个字段,比如最大最小值限制,我们可以使用两个参数min/max,分别定义;而不是使用一个data,让用户输入[min,max]这样的格式,这样我们还需要解析用户输入的数据。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
再来看module文件,不足100行的代码:
/**
* Implements hook_field_attach_validate().
*/
function field_validation_field_attach_validate($entity_type, $entity, &$errors) {
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
//Using ctools to get validation rules of this bundle.
ctools_include('export');
$rules = ctools_export_load_object('field_validation_rule', 'conditions', array('entity_type' => $entity_type, 'bundle' => $bundle));
if($rules){
foreach ($rules as $rule) {
//Disabled by Ctools.
if(!empty($rule->disabled)){
continue;
}
ctools_include('plugins');
$plugin = ctools_get_plugins('field_validation', 'validator', $rule->validator);
$class = ctools_plugin_get_class($plugin, 'handler');
if(empty($class)){
continue;
}
if (!is_subclass_of($rule->validator, 'field_validation_validator')) {
drupal_set_message(t("Plugin '@validator' should extends 'field_validation_validator'.", array('@validator' => $rule->validator)));
continue;
}
$field_name = $rule->field_name;
$field = field_info_field($field_name);
$instance = field_info_instance($entity_type, $field_name, $bundle);
$languages = field_available_languages($entity_type, $field);
foreach ($languages as $langcode) {
//debug($errors);
$items = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
//print debug($rule);
foreach($items as $delta => $item){
$validator = new $class($entity_type, $entity, $field, $instance, $langcode, $items, $delta, $item, $rule, $errors);
$break = $validator->validate();
if(!empty($break)){
break;
}
}
}
}
}
}
/**
* Implements hook_field_delete().
*/
function field_validation_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) {
//$rules = field_validation_get_field_rules($instance);
$rules = array();
if ($rules) {
foreach (array_keys($rules) as $ruleid) {
db_delete('field_validation_rule')->condition('ruleid', $ruleid)->execute();
}
}
}
/**
* Implements hook_ctools_plugin_type().
*
*/
function field_validation_ctools_plugin_type() {
return array(
'validator' => array(
'use hooks' => FALSE,
),
);
}
/**
* Implementation of hook_ctools_plugin_directory().
*/
function field_validation_ctools_plugin_directory($module, $plugin) {
if ($module == 'field_validation' && $plugin == 'validator') {
return 'plugins/' . $plugin;
}
}
/**
* Implementation of hook_ctools_plugin_api().
*
* Tell Ctools that we support the default_field_validation_rules API.
*/
function field_validation_ctools_plugin_api($owner, $api) {
if ($owner == 'field_validation' && $api == 'default_field_validation_rules') {
return array('version' => 2);
}
}
写的非常的简洁,另外,在后面的版本,我删除了field_validation_field_delete这个钩子函数,由于我们的验证规则,不仅仅存放在数据库里面了,所以这个钩子实现变得有些多余。
这里我们使用field_validation_ctools_plugin_type,定义了一种新的插件类型validator,这里'use hooks'设置为了FALSE,我通常喜欢使用这种方式,我们这里是不允许通过钩子的形式定义插件;不过在Feeds模块里面,就是使用的钩子形式,但是在很多其它的模块里面,都不使用这个方式;使用field_validation_ctools_plugin_directory定义了插件所在的目录,这里是plugins\validator目录。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们来看一下field_validation_validator文件,这是一个抽象类,其它的字段验证器都继承自这个类。
abstract class field_validation_validator {
// Numbers we make calculations on.
protected $entity_type;
protected $entity;
protected $field;
protected $instance;
protected $langcode;
protected $items;
protected $delta;
protected $item;
protected $value;
protected $rule;
protected $errors;
/**
* Save arguments locally.
*/
function __construct($entity_type, $entity, $field, $instance, $langcode, $items, $delta, $item, $rule, $errors) {
$this->entity_type = $entity_type;
$this->entity = $entity;
$this->field = $field;
$this->instance = $instance;
$this->langcode = $langcode;
$this->items = $items;
$this->delta = $delta;
$this->item = $item;
$this->value = $item[$rule->col];
$this->rule = $rule;
$this->errors = $errors;
}
/**
* Validate field.
*/
public function validate() {}
/**
* Provide settings option
*/
function settings_form(&$form, &$form_state) {
$form['settings']['data'] = array(
'#title' => t('Config data'),
'#description' => t("Config data."),
'#type' => 'textfield',
//'#default_value' => $this->options['link_to_user'],
'#default_value' => '',
);
}
/**
* Return error message string for the validation rule.
*/
public function error_message() {
$error_message = $this->rule->error_message;
return $error_message;
}
/**
* Return error element for the validation rule.
*/
public function error_element() {
$error_element = $this->rule->field_name.']['.$this->langcode.']['.$this->delta.']['.$this->rule->col;
return $error_element;
}
}
它包含一个构造函数,四个成员函数;我们向构造函数里面传递了多个变量,$entity_type, $entity, $field, $instance, $langcode, $items, $delta, $item, $rule, $errors,这些都是与字段验证相关的。除了前面讲到了validate和settings_form两个成员函数以外,这里还包含了error_message,error_element两个成员函数,分别用来获取要设置的错误消息,获取错误消息所在的元素对象。由于field_validation_validator是一个对象,所以在模块的info文件里面,我们将它注册了一下:
files[] = field_validation_validator.inc
files[] = plugins/validator/field_validation_min_length_validator.inc
Views里面是都注册了的,这里我也学习它的做法,把包含类的文件都注册一下。这样可以实现类的缓加载。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
打开plugins\validator这个目录,我们看到这里定义了一个简单的验证器field_validation_min_length_validator。我们打开这个文件:
$plugin = array(
'label' => t('Min length'),
'handler' => array(
'class' => 'field_validation_min_length_validator',
),
);
class field_validation_min_length_validator extends field_validation_validator {
/**
* Validate field.
*/
public function validate() {
$min_length = $this->rule->settings['data'];
if($this->value != '' && (drupal_strlen($this->value) < $min_length)){
$error_element = $this->error_element();
$error_message = $this->error_message();
form_set_error($error_element,$error_message);
}
}
/**
* Provide settings option
*/
function settings_form(&$form, &$form_state) {
$form['settings']['data'] = array(
'#title' => t('Minimum number of characters'),
'#description' => t("Specify the minimum number of characters that have to be entered to pass validation."),
'#type' => 'textfield',
//'#default_value' => $this->options['link_to_user'],
'#default_value' => '',
);
}
}
如果不使用钩子函数定义插件的话,那么每个插件文件的最上面,必须有一个$plugin数组,用来定义插件。Ctools插件会读取这个数组里面的信息。
$plugin = array(
'label' => t('Min length'),
'handler' => array(
'class' => 'field_validation_min_length_validator',
),
);
插件里面包含哪些键,取决于你的插件系统本身,我们这里有'label','handler'两个键,后者里面包含了'class'。
field_validation_min_length_validator是一个类,继承自field_validation_validator,这里我们实现了两个成员函数,validate和settings_form;validate是负责逻辑验证的,如果通不过验证,我们这里会去设置一个错误消息;settings_form是一个设置表单,用来定义settings列里面所需要的设置信息。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
在module文件中,还有一个函数field_validation_ctools_plugin_api,这个前面已经讲过了,field_validation.default_field_validation_rules.inc文件和前面所讲的一样。我把UI部分独立了出来,前期只实现了基于Ctools的导出UI,这个足够了,代码和前面的类似,所不同的是,plugins\export_ui下面field_validation_export_ui.inc文件的代码更简单一些,这里纯粹是为了测试我的想法是不是行得通,所以省去了很多功能。
最后,我们看看插件的调用,下面的代码用来加载一个字段验证器:
ctools_include('plugins');
$plugin = ctools_get_plugins('field_validation', 'validator', $rule->validator);
$class = ctools_plugin_get_class($plugin, 'handler');
if(empty($class)){
continue;
}
if (!is_subclass_of($rule->validator, 'field_validation_validator')) {
drupal_set_message(t("Plugin '@validator' should extends 'field_validation_validator'.", array('@validator' => $rule->validator)));
continue;
}
下面的代码,实例化字段验证器,验证。
$validator = new $class($entity_type, $entity, $field, $instance, $langcode, $items, $delta, $item, $rule, $errors);
$break = $validator->validate();
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
在完成了2.0 alpha1以后,我花了将近一个星期的时间,写出来了2.0beta1。改进之处有,对于field_validation_validator,在构造函数里面,增加了value这个参数,并为所有的参数设置了默认值,这是一个小的改进。此外,还增加了成员函数set_error、get_default_settings、token_help。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
其中set_error使得错误消息的设置更加简单,一句话就能将错误消息设置好了,通常使用这样的代码设置错误消息:
$this->set_error($token);
token_help则是用来在错误消息里面支持占位符(令牌)替换模式的,就是用户在自定义错误消息里面,可以输入可用的占位符,这些占位符最后被替换为相应的变量值。这个技术在Views里面经常遇到,在Field validation的问题列表中,也有相应的功能支持请求。我们来看一下set_error这个函数的代码:
public function set_error($tokens = array()) {
$error_element = $this->get_error_element();
$error_message = $this->get_error_message();
$tokens += array(
'[entity-type]' => $this->rule->entity_type,
'[bundle]' => $this->rule->bundle,
'[field-name]' => $this->instance['label'],
'[value]' => $this->value,
);
$error_message = strtr($error_message, $tokens);
form_set_error($error_element, check_plain($error_message));
}
这里的占位符机制,使用的是strtr,而不是Drupal核心的token系统,因为我们的这个占位符机制比较简单,直接使用strtr就满足我们的需求了。
get_default_settings是用来获取默认设置的,在编辑验证规则的时候,会用到,这是一个帮助函数。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
对于UI模块,我们将原来自己编写的UI部分代码也搬了过来,同时也完善了Ctools 导出UI里面的逻辑,为其加上了AJAX效果。里面有段代码需要学习一下:
if (!empty($default_validator)) {
$plugin = ctools_get_plugins('field_validation', 'validator', $default_validator);
$class = ctools_plugin_get_class($plugin, 'handler');
$validator_class = new $class();
$validator_class->settings_form($form, $form_state);
$output = '<p>' . t('The following tokens are available for error message.' . '</p>');
$token_help = $validator_class->token_help();
if (!empty($token_help)) {
$items = array();
foreach ($token_help as $key => $value) {
$items[] = $key . ' == ' . $value;
}
$output .= theme('item_list',
array(
'items' => $items,
));
}
$form['token_help'] = array(
'#type' => 'fieldset',
'#title' => t('Replacement patterns'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#value' => $output,
'#id' => 'error-message-token-help',
'#prefix' => '<div>',
'#suffix' => '</div>',
'#weight' => 6,
);
}
占位符的描述部分的代码是从views模块里面搬过来的。这里面值得学习的是如何获取插件:
$plugin = ctools_get_plugins('field_validation', 'validator', $default_validator);
从插件里面获取类:
$class = ctools_plugin_get_class($plugin, 'handler');
实例化类,调用成员函数settings_form:
$validator_class = new $class();
$validator_class->settings_form($form, $form_state);
这些代码和module文件中的加载插件对象的代码类似,只不过这里调用了settings_form。我前面讲的,为构造函数的参数设置默认值,主要就是为了方便这里的调用。
其它的改进,就是添加了所有的插件,很多插件都作了改进;除此以外,还编写了一些插件,为了模块升级的需要,都放在了field_validation_deprecated模块里面。这里给出一个插件的代码,这是field_validation_numeric2_validator的代码:
<?php
/**
* @file
* Field validation numeric validator.
*
*/
$plugin = array(
'label' => t('Numeric values'),
'description' => t('Verifies that user-entered values are numeric, with the option to specify min and / or max values.'),
'handler' => array(
'class' => 'field_validation_numeric2_validator',
),
);
class field_validation_numeric2_validator extends field_validation_validator {
/**
* Validate field.
*/
public function validate() {
$settings = $this->rule->settings;
if ($this->value != '') {
$flag = TRUE;
if (!is_numeric($this->value)) {
$flag = FALSE;
}
else{
if (isset($settings['min']) && $settings['min'] != '' && $this->value < $settings['min']) {
$flag = FALSE;
}
if (isset($settings['max']) && $settings['max'] != '' && $this->value > $settings['max']) {
$flag = FALSE;
}
}
if (!$flag) {
$token = array(
'[min]' => isset($settings['min']) ? $settings['min'] : '',
'[max]' => isset($settings['max']) ? $settings['max'] : '',
);
$this->set_error($token);
}
}
}
/**
* Provide settings option
*/
function settings_form(&$form, &$form_state) {
$default_settings = $this->get_default_settings($form, $form_state);
//print debug($default_settings);
$form['settings']['min'] = array(
'#title' => t('Minimum value'),
'#description' => t("Optionally specify the minimum value to validate the user-entered numeric value against."),
'#type' => 'textfield',
'#default_value' => isset($default_settings['min']) ? $default_settings['min'] : '',
);
$form['settings']['max'] = array(
'#title' => t('Maximum value'),
'#description' => t("Optionally specify the maximum value to validate the user-entered numeric value against."),
'#type' => 'textfield',
'#default_value' => isset($default_settings['max']) ? $default_settings['max'] : '',
);
}
/**
* Provide token help info for error message.
*/
public function token_help() {
$token_help = parent::token_help();
$token_help += array(
'[min]' => t('Minimum value'),
'[max]' => t('Maximum value'),
);
return $token_help;
}
}
这是Beta1里面包含的所有可用的插件:
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
2.0-RC1版本里面,修正了三个小问题,一个是文字错误,一个是日期验证里面星期的日期范围验证有问题,一个是机读名字有可能超过32个字符的问题。都是很小的问题,很快就修正了。在2.0的正式版里面,新增了一个date range2 验证器,删除了date validation模块,将它迁移到了field_validation_deprecated模块里面,修正了数值验证里面的一个小问题。之后基于Drupal8的插件系统,开发了Drupal8下面的1.0alpha1版,学习了Drupal8里面的插件机制。在后面是2.1版,这里面修正了部分验证规则只适用于管理员的问题,我们使用下面的代码搞定了这个问题:
//Always bypass all access checkings.
$query->addMetaData('account', user_load(1));
修正了唯一值验证在field collection item的编辑页面不能正常工作的问题。同时增加了property_validation模块,用来解决实体里面属性的验证问题。
在2.2里面,又新增了一个field_validation_extras模块,里面包含了10多个验证器;同时,新增了允许特定角色跳过验证的功能,我们为field_validation_validator增加了一个函数,bypass_validation,用来检查当前角色是否可以跳过验证:
/**
* Bypass validation.
*/
public function bypass_validation() {
global $user;
if (!empty($this->rule->settings['bypass']) && !empty($this->rule->settings['roles'])) {
$roles = array_filter($this->rule->settings['roles']);
$user_roles = array_keys($user->roles);
foreach ($roles as $role) {
if (in_array($role, $user_roles)) {
return TRUE;
}
}
}
return FALSE;
}
同时在设置表单里面新增了以下代码:
$form['settings']['bypass'] = array(
'#title' => t('Bypass validation'),
'#type' => 'checkbox',
'#default_value' => isset($default_settings['bypass']) ? $default_settings['bypass'] : FALSE,
);
$roles_options = user_roles(TRUE);
$form['settings']['roles'] = array(
'#title' => t('Roles'),
'#description' => t("Only the checked roles will be able to bypass this validation rule."),
'#type' => 'checkboxes',
'#options' => $roles_options,
'#default_value' => isset($default_settings['roles']) ? $default_settings['roles'] : array(),
'#states' => array(
'visible' => array(
':input[name="settings[bypass]"]' => array('checked' => TRUE),
),
),
);
这里面值得学习的是这段代码:
'#states' => array(
'visible' => array(
':input[name="settings[bypass]"]' => array('checked' => TRUE),
),
),
这里的':input[name="settings[bypass]"]',这里有点类似于jQuery的味道,这里面settings[bypass]可以在firefox下面的firebug里面查到。我们在第一集的表单系统里面,有一个非常简单的例子,我也是在编写这段代码的时候,才对这个结构有了更深的认识。因为这个表单是settings元素里面的,我开始觉得应该这样设置:
':input[name="bypass"]' => array('checked' => TRUE),
但是很遗憾,不起作用。多试了几种可能,才最终明白这里的用法。
当然,在2.2里面,还增加了对Feeds模块的集成,使得Feeds的导入,可以根据一个唯一的字段进行更新,这需要应用http://drupal.org/node/661606#comment-6481214里面所给的补丁。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
写完第3集以后,就开始写第四集,而第四集里面开始写的就是这个field validation,记录我开发这个模块的历程,开始觉得很有必要,后来收到很多人的反馈,希望我多写一点案例形式的,突然间不知道该写什么了,中间又发生了一些事情,到底要不要继续写下去,写下去的话,写什么?一直困扰着我,我不可能让所有的人满意的,不可能你需要一个相册网站,我就恰好写一个相册的例子给你。只需要我写的东西,对那些认真读过的人有所帮助,就可以了,不会去试图满足所有人的胃口,可能这也是一个进步。
我之所以坚持写Field validation,首先是我比较熟悉,自己写的;其次它是中国人所写的影响最大的模块之一,目前我还没有找到哪个模块(中国人写的)比这个影响更大;里面的很多代码是可以直接借鉴到项目中来的,特别是有关EntityFieldQuery的代码,很实用;还有就是它是学习Ctools插件系统的一个很好的例子,将来也会是学习Drupal插件系统的很好的例子,这个插件系统是比较简单的,而且解决了Drupal中的一个常见的问题;如果还有的话,就是希望将来有一天,有更多的中国开发者,能够超越Field validation。
此外,有关Field validation的这两部分,是最开始写的,然后写的Breadcrumb2相关的部分。我觉得,Ctools插件开发,自定义自己的钩子函数,这些在中国用的不多,所以将它们放到了后面。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
首先一个进步就是编码规范,为了让自己的模块,更易于让人接受,自己在后面,使用Coder模块,按照它所给出提示,修正了几乎所有的代码格式问题。问题最多的两个地方,一个就是我把tab键设置成两个空格了,后来手工的一个一个的把tab转为了空格,这样在其它编辑器/IDE下面也是格式良好的;另一个常见的问题,就是控制语句括号的两边要有空格,以前我总是这样写:
if($flag){
…
}
改正后:
If ($flag) {
…
}
在括号两边加了两个空格。
当然,还有很多其它的格式,也不是在所有情况下都遵守编码规范,比如在PHP验证器里面,有这样的代码:
return eval($this->rule->settings['data']);
这个eval函数,如果你读过我以前翻译的Drupal专业开发指南的话,我们都知道是不推荐使用的,但是也不是绝对的,个别模块里面还是使用了这个函数。在这里,这个函数能够给我们带来极大的便利,我没有想到更好的函数可用,Drupal自身封装的php_eval在这里不够灵活,它没办法直接操作调用函数里面可用的变量。
作者:老葛,北京亚艾元软件有限责任公司,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的钩子是否会在将来的版本里面完全消失,这个一时半会还是不会的,将来,钩子机制将会作为插件系统的一个很好的补充,而存在。