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版本: