第10章 Ctools插件开发

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

Field validation一直都是在进步的,就像有一个用户所说的,每当他有了新的需求的时候(关于Field validation的),Field validation就包含了这样的新特性,恰好满足他的需求。

 

我们终于要迎来RC版了,在这个版本中,添加了导出验证规则的功能。其实我很早就想实现这个功能了,但总觉得自己还没有达到这样的水平,另外这方面的文档也没有多少。

验证规则的导入、导出,这里是基于Ctools模块的,很早我就想基于Ctools模块实现了,在此以前,我做了大量的准备工作,读了很多英文的文档,看了很多相关的例子,最终,决定动手了。其实还有一个想法,就是采用Ctools插件的形式,来实现验证器,这个想法也想了很久了,每当看到Feeds模块的插件形式,我便希望按照这种方式进行改进。其实在我编写Field validation beta1版的时候,这样的想法,就有了。


Drupal版本:

1 Ctools导入导出API

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

Ctools自带了一个帮助文档,里面有导入导出的介绍,在ctools\help下面有export.htmlexport-ui.html两个介绍,这对我的开发非常有帮助。我至少读了两遍。除此以外,我还找到了几个实现了Ctools export的模块,作为例子,阅读了它们的相关代码。



Drupal版本:

1.1 Schema特有的定义

作者:老葛,北京亚艾元软件有限责任公司,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_versionapi的当前版本。


Drupal版本:

1.2 实现hook_ctools_plugin_directory

作者:老葛,北京亚艾元软件有限责任公司,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_rulesdefault_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。现在,我们的验证规则,不仅仅存放在了数据库中,还可以通过代码来定义。


Drupal版本:

1.3 使用ctools_export_load_object加载对象

作者:老葛,北京亚艾元软件有限责任公司,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里面的代码,现在就有些多余了,我们在后面的版本中删除了这个文件。


Drupal版本:

1.4 实现导出UI插件

作者:老葛,北京亚艾元软件有限责任公司,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自带的AJAXCtools早期的版本是不支持的,我写这个功能的时候,恰好支持了这个特性。

这里面,使用了函数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了。


Drupal版本:

2 Ctools 插件开发

作者:老葛,北京亚艾元软件有限责任公司,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的代码:


Drupal版本:

2.1 install文件

作者:老葛,北京亚艾元软件有限责任公司,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]这样的格式,这样我们还需要解析用户输入的数据。


Drupal版本:

2.2 简洁的module文件

作者:老葛,北京亚艾元软件有限责任公司,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目录。


Drupal版本:

2.3 创建field_validation_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,这些都是与字段验证相关的。除了前面讲到了validatesettings_form两个成员函数以外,这里还包含了error_messageerror_element两个成员函数,分别用来获取要设置的错误消息,获取错误消息所在的元素对象。由于field_validation_validator是一个对象,所以在模块的info文件里面,我们将它注册了一下:

files[] = field_validation_validator.inc

files[] = plugins/validator/field_validation_min_length_validator.inc

    Views里面是都注册了的,这里我也学习它的做法,把包含类的文件都注册一下。这样可以实现类的缓加载。


Drupal版本:

2.4 定义一个具体的插件field_validation_min_length_validator

作者:老葛,北京亚艾元软件有限责任公司,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,这里我们实现了两个成员函数,validatesettings_formvalidate是负责逻辑验证的,如果通不过验证,我们这里会去设置一个错误消息;settings_form是一个设置表单,用来定义settings列里面所需要的设置信息。


Drupal版本:

3 如何加载一个Ctools插件

作者:老葛,北京亚艾元软件有限责任公司,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();


Drupal版本:

3.1 2.0-Beta1版的改进

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

在完成了2.0 alpha1以后,我花了将近一个星期的时间,写出来了2.0beta1。改进之处有,对于field_validation_validator,在构造函数里面,增加了value这个参数,并为所有的参数设置了默认值,这是一个小的改进。此外,还增加了成员函数set_errorget_default_settingstoken_help


Drupal版本:

3.2 set_error

作者:老葛,北京亚艾元软件有限责任公司,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是用来获取默认设置的,在编辑验证规则的时候,会用到,这是一个帮助函数。


Drupal版本:

3.3 field_validation_export_ui.inc

作者:老葛,北京亚艾元软件有限责任公司,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里面包含的所有可用的插件:

图片1.png


Drupal版本:

4 后续的改进:

作者:老葛,北京亚艾元软件有限责任公司,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模块,用来解决实体里面属性的验证问题。

':input[name="settings[bypass]"]'

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里面所给的补丁。


Drupal版本:

5 心得体会

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

通过维护一个这样逐渐流行的社区模块,自己在技术上进步了很多。


Drupal版本:

8 后记

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

写完第3集以后,就开始写第四集,而第四集里面开始写的就是这个field validation,记录我开发这个模块的历程,开始觉得很有必要,后来收到很多人的反馈,希望我多写一点案例形式的,突然间不知道该写什么了,中间又发生了一些事情,到底要不要继续写下去,写下去的话,写什么?一直困扰着我,我不可能让所有的人满意的,不可能你需要一个相册网站,我就恰好写一个相册的例子给你。只需要我写的东西,对那些认真读过的人有所帮助,就可以了,不会去试图满足所有人的胃口,可能这也是一个进步。

我之所以坚持写Field validation,首先是我比较熟悉,自己写的;其次它是中国人所写的影响最大的模块之一,目前我还没有找到哪个模块(中国人写的)比这个影响更大;里面的很多代码是可以直接借鉴到项目中来的,特别是有关EntityFieldQuery的代码,很实用;还有就是它是学习Ctools插件系统的一个很好的例子,将来也会是学习Drupal插件系统的很好的例子,这个插件系统是比较简单的,而且解决了Drupal中的一个常见的问题;如果还有的话,就是希望将来有一天,有更多的中国开发者,能够超越Field validation

    此外,有关Field validation的这两部分,是最开始写的,然后写的Breadcrumb2相关的部分。我觉得,Ctools插件开发,自定义自己的钩子函数,这些在中国用的不多,所以将它们放到了后面。


Drupal版本:

6代码更加规范

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

首先一个进步就是编码规范,为了让自己的模块,更易于让人接受,自己在后面,使用Coder模块,按照它所给出提示,修正了几乎所有的代码格式问题。问题最多的两个地方,一个就是我把tab键设置成两个空格了,后来手工的一个一个的把tab转为了空格,这样在其它编辑器/IDE下面也是格式良好的;另一个常见的问题,就是控制语句括号的两边要有空格,以前我总是这样写:

if($flag){

  …

}

改正后:

If ($flag) {

  …

}

在括号两边加了两个空格。

当然,还有很多其它的格式,也不是在所有情况下都遵守编码规范,比如在PHP验证器里面,有这样的代码:

return eval($this->rule->settings['data']);

这个eval函数,如果你读过我以前翻译的Drupal专业开发指南的话,我们都知道是不推荐使用的,但是也不是绝对的,个别模块里面还是使用了这个函数。在这里,这个函数能够给我们带来极大的便利,我没有想到更好的函数可用,Drupal自身封装的php_eval在这里不够灵活,它没办法直接操作调用函数里面可用的变量。


Drupal版本:

7深入掌握了插件机制

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

对插件机制的理解,有了更深的认识。以前总觉得,钩子机制,是Drupal核心最本质,最具特色的一个机制,Drupal之所有这么灵活,就是它本身所待的这种钩子机制。其它模块,如果想与Drupal系统交互,只需要实现相应的钩子就可以了。但是钩子机制本身,也是有很大的局限性的,特别是随着Drupal应用的领域越来越广,这种局限性,就越来越明显的暴露出来。钩子机制有哪些局限性呢?所有的钩子实现都需要放到module文件里面,我们知道,每一个页面,都会加载所有的module文件,把所有的钩子代码都放到module文件里面,使得Drupal消耗的内存非常的大,我们很早就讲到了这个问题,在第一集的第一章里面就介绍了这个问题,这在Drupal里面,一直是一个让人头疼的问题。为了解决这个问题,Drupal7里面实现了注册表的机制,在Drupal7的目标里面包含了这个目标,最初的目标是把所有的文件、钩子函数、普通函数、类、接口,都注册一下,当需要一个函数,一个类的时候,再去加载这个函数所在的文件,这个功能最初实现了,运行良好,但是,后来人们发现这个机制和更底层的缓存技术冲突,好像是opcode缓存,没有办法,这个功能又回退了回去,只实现了部分功能,就是类、接口的注册。


Drupal版本:

7.5 对字段验证的进一步思考

作者:老葛,北京亚艾元软件有限责任公司,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的进展,fagoattiks, attiks想往Drupal8里面引入一个验证框架,这样表单验证、实体验证、字段验证都会统一起来,attiks是重新编写的自己实现,很多想法来自于Symfony\Validator,加入了Drupal特有的一些东西,而fago开始是支持attiks的工作的,但是后来又变了卦,转向支持基于Symfony\Validator的实现,而不是重新发明轮子。说实在的,attiks所做的工作,我也能够完成,只是没有他这样的机会,作为一个中国的Drupal开发者,我们是没有多少与这些核心开发者交流的机会的。Attiks所写的程序,想法是来自于Symfony\Validator,其实还有一部分想法,是直接来自于field validation2.X,看着别人拿走了自己的想法,写出来了自己的东西,多少有点吃不到葡萄,说葡萄酸的味道。


Drupal版本:

7.1hook_hook_info

作者:老葛,北京亚艾元软件有限责任公司,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文件里面。


Drupal版本:

7.2插件方式的好处

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

插件的方式,就能很好的解决这个缓加载的问题,Drupal在默认的情况下,是不会去加载插件的,只有当需要的时候,才去加载相应的插件。在field validation 1.x里面,我们也拆分成了多个文件,比如field_validation.validators.incfield_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.incfield_validation.rules.inc两个文件。这种方式是没有实现缓加载的,不信的话,你可以测试一下,新写一个模块,在里面直接调用field_validation.validators.incfield_validation.rules.inc文件里面的方法,此时是可以直接调用的,这意味着这两个文件也被加载了进来。

我们大致算一下,在1.x版本里面,普通页面请求过来以后,field validation加载到内存里面的文件大小有:

12 Kfield_validation.module) + 2k field_validation.rules.inc+ 30K field_validation.validators.inc= 44K

而在2.x里面,禁用了field_validation_ui模块以后,总共加载到内存里面的大小:

3 Kfield_validation.module) = 3K

如果算上field_validation_ui模块,加载到内存里面的大小:

3 Kfield_validation.module+ 7Kfield_validation_ui.module = 10K

在这两种情况下,分别少加载了41K34K大小的文件到内存中去。这就是field validation2.X对 1.X版本的一个重要改进。

那么在调用一个验证器的情况下,又会加载多少文件大小内,显然在field validation1.X下面,加载的大小仍然为44K,而在2.x里面,禁用了field_validation_ui模块以后,总共加载到内存里面的大小:

3 Kfield_validation.module+ 3Kfield_validation_numeric2_validator.inc) = 6K

验证器的文件大小不一样,3K取一个大致的平均数,此时也只有6K大小。如果算上field_validation_uimodule文件,也只有13K大小。

在调用验证器的情况,我们仍然节省了38K31K大小的内存。总之,我们对性能的改进,是有所帮助的。

 

在维护1.x版本的时候,每当我坐一个小小的修改时,生怕改动了其它验证器,而在2.x里面,一个验证器,一个文件,管理起来非常方便,我修改这个验证器,肯定影响不到其它的验证器,至少不用提心吊胆了。采用插件的形式,更好理解,其他程序员学习的成本更低了,他只需要复制一个插件,重命名,改一下里面关键的验证逻辑,然后将修改后的验证器,放到field_validation\plugins\validator目录下面,就可以正常工作了,这样的好处,就是不用修改已有的文件。以前加一个验证器的时候,总要修改field_validation.validators.inc文件,现在只需要创建一个自己独立的文件就可以了。

插件的方式,比钩子的方式,要灵活很多,是钩子方式的一种进步。在我实现了2.0-beta1以后,我有了更多的关于插件的想法,比如核心里面区块,其实也可以转换为插件;比如Ubercart里面的支付方法、运送方法、窗格,也都可以转换为插件的形式,这就是uc_ctools的由来。


Drupal版本:

7.4什么钩子可以转换为插件

作者:老葛,北京亚艾元软件有限责任公司,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的钩子是否会在将来的版本里面完全消失,这个一时半会还是不会的,将来,钩子机制将会作为插件系统的一个很好的补充,而存在。


Drupal版本: