作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
对于熟悉Drupal6的用户来说,CCK应该是必选的第三方模块,使用这个模块,可以方便的扩展内容类型的字段信息。Drupal7最大的一个改进,就是将CCK模块核心化,在Drupal7里面,它的名字已经换成了Field,并成为Drupal7下面的核心必选模块。有了这个模块,我们就可以方便的为节点、评论、分类术语、用户添加扩展字段了,是的,它可以应用于节点、评论、分类术语、用户等等,而不像Drupal6下面的CCK那样只适用于节点类型。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们在本章,我们将通过创建一个自定义的字段类型,来学习Field API相关的各种知识。我们先来介绍一下这个模块的背景知识:
我以前在给客户做网上书店的时候,就遇到过这样的需求,图书内容类型下面包含一个“拼音名称”字段,用来输入图书名称的拼音,那个时候我们采用了这样的解决办法,使用专门的软件,将书名转换为拼音,这方面有很多现有的工具可用,然后将这一信息导入到Drupal系统中来。那个时候我就在想,如果能够开发一个第三方模块,自动地生成拼音字段,就可以省去很多的麻烦。后来又遇到了这样的一个需求,对节点标题按照拼音的a、b、c、d…z进行检索,此时如果我们自带了一个拼音字段,那么实现起来就会方便很多。这就是我们这个模块的实际的背景。
实现所用的技术,我决定在Drupal7下面实现,然后采用定制一个字段类型的形式,实现节点标题的拼音字段。中文转拼音,的确有很多现成的PHP程序,但是大多数都不是开源的,我突然想到Transliteration模块,也可以完成中文转拼音这一工作。同时对这个模块又做了抽象,除了我们中文有这样的需求以外,日文、俄文…等等,其它文字是否也有这样的需求。这就是我为什么选用Transliteration模块作为转换程序的原因,因为它用途更广,更国际化。
我们需要创建一个字段,这个字段有一个对应的源字段,该字段的值,由源字段的值使用Transliteration模块转换而成,我们不需要负责它的输入。此时我所想到的就是,使用一个textfield就可以搞定这个字段。如果这样的话,其实使用现有的模块也可以解决问题,比如Computed Field模块,只需要把我们的转换代码放在对应的配置中,我想就可以解决问题了,当然我们此时仍然可以开发一个模块。后来我想到了另一个问题,那就是Transliteration模块的转换并不是100%准确的,我不知道其它语言的转换,但是对于中文来说,确实存在这样的问题。因此,这又多出来了一个需求,那就是允许用户手动地调整这个字段。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
首先我们来判断一下,这个字段什么时候为空,让我们添加以下代码:
/**
* Implements hook_content_is_empty().
*/
function transliteration_title_field_is_empty($item, $field) {
if (empty($item['value'])) {
return TRUE;
}
return FALSE;
}
这段代码相当简单,我们这里没有考虑是否是手动输入,只考虑了文本框。当然用到字段为空的地方并不常见,特别是对于我们这个字段。这里我们主要顺带介绍一下这个hook_content_is_empty钩子函数,在这个钩子函数中,根据字段的输入值,进行判断,当字段为空时放回TRUE,否则返回FALSE。字段是否为空,有时候,对于一些特殊的字段来说,还是很有必要的。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
接下来,我们需要检查用户的输入是否正确,我们主要检查用户手动输入的情况,看用户输入的字符是否是ASCII字符,因为经transliteration模块转换后的字符串只包含ASCII字符。我们来看看这个字段的验证函数:
/**
* Implements hook_field_validate().
*
* Possible error codes:
* - 'transliteration_title_invalid': The transliteration title is not valid
*/
function transliteration_title_field_validate($obj_type, $object, $field, $instance, $langcode, $items, &$errors) {
foreach ($items as $delta => $item) {
//drupal_set_message($item['value']);
//[\x20-\x7e] [A-Za-z0-9]
if (!empty($item['manual']) && !preg_match('/^[\x20-\x7e]+$/', $item['value'])) {
$message = t('"%value" is not a valid transliteration titles', array('%value' => $item['value']));
/*
//这段代码不能正常工作
$errors[$field['field_name']][$langcode][$delta][] = array(
'error' => "transliteration_title_invalid_".$field['field_name'],
'message' => t('It is not a valid transliteration titles'),
);
*/
form_set_error($field['field_name'] .']['.$langcode.']['. $delta .'][value', $message);
}
}
}
这段代码的含义是,如果当前为手动输入,而用户输入的字符包含非ASCII字符时,设置一个错误信息。这里使用了正则表达式,来检查ASCII字符,我费了很大的功夫才找到了这个还能工作的表达式。另外,$errors[$field['field_name']][$langcode][$delta][]这种方式,工作起来总有问题,所以采用了更为原始一点的form_set_error,这在表单API里面讲过。注意$field['field_name'] .']['.$langcode.']['. $delta .'][value',在这个字符串的末位,没有“]”符号,对于这种数组形式的表单元素,就是这种用法。
此时,如果在手动方式下,输入一个包含中文字符的字符串,系统就会报错,表单不能提交。如图所示:
当含有中文字符时,系统提示输入错误
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们完成了字段的验证以后,还有一项工作要做,那就是在将字段的值,保存到数据库之前,重新设置一下,在不是手动输入的情况下,将其设置为源字段经过transliteration模块转换后的值。为此,我们还需要实现一个钩子函数,在module文件中添加以下代码:
/**
* Implements hook_field_presave().
*/
function transliteration_title_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
//drupal_set_message($field['settings']['size']);
if($field['type'] == 'transliteration_title_field'){
if(isset($entity->{$instance['widget']['settings']['source_field_id']})){
$source_field = $entity->{$instance['widget']['settings']['source_field_id']};
if(is_array($source_field)){
$source_field_items = $source_field[$langcode];
foreach ($source_field_items as $delta => $item) {
if(empty($items[$delta]['manual'])){
$upper_lower_case = $instance['widget']['settings']['upper_lower_case'];
if (function_exists($upper_lower_case)) {
$items[$delta]['value'] = $upper_lower_case(transliteration_get($item['value']));
$items[$delta]['manual'] = 0;
}else{
$items[$delta]['value'] = transliteration_get($item['value']);
$items[$delta]['manual'] = 0;
}
}
}
}else{
if(empty($items[0]['manual'])){
$upper_lower_case = $instance['widget']['settings']['upper_lower_case'];
if (function_exists($upper_lower_case)) {
//drupal_set_message('123');
//drupal_set_message( $upper_lower_case);
$items[0]['value'] = $upper_lower_case(transliteration_get($entity->{$instance['widget']['settings']['source_field_id']}));
$items[0]['manual'] = 0;
}else{
//drupal_set_message('123456');
$items[0]['value'] = transliteration_get($entity->{$instance['widget']['settings']['source_field_id']});
$items[0]['manual'] = 0;
}
}
}
}
}
}
这段代码,首先对源字段作了检查,假如默认的源字段为title,那么此时实体(节点)的title属性就是一个字符串值。如果是一个普通的字段,那么这里就会是一个数组。然后对于这两种情况,进行了分别处理。前者相对简单一点,后者稍微复杂了一点。这里面的核心代码是:
$items[$delta]['value'] = $upper_lower_case(transliteration_get($item['value']));
这里我们对源字段的值进行了transliteration转换,然后根据我们的大小写情况,对字符串作了进一步处理,最后将返回值赋给了$items[$delta]['value']。这是最主要的代码,里面又细分了多种情况,但是逻辑基本上一致。
此时,当我们编辑这个节点,取消手动输入,提交后,再回到编辑页面,我们看到系统为我们自动转换成了拼音。如图所示:
系统自动根据标题生成了拼音
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
这就是我们想要的效果。不过,当我们访问节点页面时,并没有看到这个字段的内容,系统已经为其生成内容了阿。对于字段的显示,仍然需要由我们的模块来负责,谁让这个字段是由这个模块定义的呢。我们添加最后的两个钩子函数:
/**
* Implements hook_field_formatter_info().
*
*/
function transliteration_title_field_formatter_info() {
$formats = array(
'transliteration_title_default' => array(
'label' => t('Default'),
'description' => t('Default display for the transliteration title.'),
'field types' => array('transliteration_title_field'),
),
'transliteration_title_plain' => array(
'label' => t('Plain text'),
'description' => t('Display the transliteration title as plain text.'),
'field types' => array('transliteration_title_field'),
),
);
return $formats;
}
/**
* Implements hook_field_formatter_view().
*/
function transliteration_title_field_formatter_view($object_type, $object, $field, $instance, $langcode, $items, $display) {
$element = array();
switch ($display['type']) {
case 'transliteration_title_default':
foreach ($items as $delta => $item) {
//drupal_set_message(print_r($item));
$element[$delta] = array('#markup' => $item['value']);
}
break;
case 'transliteration_title_plain':
foreach ($items as $delta => $item) {
$element[$delta] = array('#markup' => check_plain($item['value']));
}
break;
}
return $element;
}
首先,我们实现了hook_field_formatter_info钩子函数,在这里我们定义了两个显示格式,一个是默认的格式,一个是纯文本的格式,本质上两者也没有太大的区别。此时清空缓存,然后访问字段的管理显示页面,我们会看到我们定义的字段格式,如图所示:
我们定义的格式显示在了管理显示页面
最后,我们在hook_field_formatter_view钩子中,定义了两种显示格式的具体实现,这里需要注意的是,这个钩子函数返回的是一个呈现数组。当我们再次访问节点页面时,就可以看到这个字段的值了。
我们的这个实例模块,到这里就讲解完毕,这里我将模块的实现,按照我们使用字段的先后顺序,逐一的剥离处理。希望读完这一节后,大家能对通过模块定义一个字段类型有所了解,对我们日常所用的字段有更深一层的把握。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
大多数字段模块,都自带了验证功能,但有时候,这些验证并不能满足我们实际的需求。对于常用的文本字段,其验证功能更弱。假如我们创建了一个book节点类型,为其添加了一个isbn文本字段,此时我们想对isbn作进一步的验证,假定验证规则为,如果输入的字符串的长度既不是10也不是13,那么我们就认为没有通过验证,同时假定当前语言为“und”。
不妨将这个模块命名为isbn_validation,我们来看看这个模块的主代码:
/**
* Implements hook_field_attach_validate().
*/
function isbn_validation_field_attach_validate($entity_type, $entity, &$errors) {
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
if($entity_type =='node' && $bundle == 'book'){
$field_isbn_length = strlen($entity->field_isbn['und'][0]['value']);
if(($field_isbn_length != 10) && ($field_isbn_length != 13)){
$errors['field_isbn']['und'][0][] = array(
'error' => 'field_isbn',
'message' => t('无效的ISBN号.'),
);
}
}
}
对于其它模块定义的字段,我们想对其进行验证的话,可以使用hook_field_attach_validate,在上面的代码中,我们对文本字段field_isbn按照我们的验证进行了检查,如果没有通过,则设置错误信息。注意这里的$errors是通过引用传递的,如果它不为空,表单就通不过验证。这里的结构为:
$errors[字段名][当前语言][delta][]
而右边数组值,则包含两个键,一个是'error',表示错误代码;一个是 'message',表示出现错误时显示给用户的错误提示。
通过编写代码,进行验证,对于不懂程序的人来说,麻烦了很多。不过drupal.org上有一个第3方模块“Field validation”,可以方便的为每个字段实例添加正则表达式规则验证,可以帮我们解决常见的验证问题。项目地址:http://drupal.org/project/field_validation。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
如果你玩过三国杀,并且还比较熟悉的话,那么你一定了解里面的一个武将,袁术,他最常见的技能就是能够多摸几张牌,相信这个很多人都熟悉,他还有一个技能,叫做“伪帝”,就是说他具有当前主公的主公技,但是他本身不是主公。我这里借用一下,将显示在管理字段页面的非字段称之为“伪字段”,表示它们本身不是字段,但是又具有字段的一些属性。
有些信息,比如节点的标题,它的实现方式没有采用Field API的形式,但是为了管理的方便,Drupal也将其显示在了管理字段界面,Drupal是怎么实现这一点的呢?通过node.module我们可以找到对应的代码:
/**
* Implements hook_field_extra_fields().
*/
function node_field_extra_fields() {
$extra = array();
foreach (node_type_get_types() as $type) {
if ($type->has_title) {
$extra['node'][$type->type] = array(
'form' => array(
'title' => array(
'label' => $type->title_label,
'description' => t('Node module element'),
'weight' => -5,
),
),
);
}
}
return $extra;
}
节点模块就是通过这段代码,将节点的标题显示在了字段管理界面,注意这里将'weight'属性设置为了-5,所以节点的标题默认总是显示在其它字段的上面,如图所示。
在管理字段页面,节点标题总是显示最前面
如果熟悉Ubercart模块的话,我们知道,在这个电子商务模块中,产品属性并没有实现为字段的形式,或许将来会朝这方面发展,它的竞争对手Commerce模块已经完全采用了字段的形式了。在Ubercart的Drupal7版本里面,产品属性就采用了伪字段的形式,下面是该模块的对应代码,这里我们有删减:
function uc_product_field_extra_fields() {
$extra = array();
foreach (uc_product_types() as $type) {
$extra['node'][$type] = array(
'display' => array(
'display_price' => array(
'label' => t('Display price'),
'description' => t('High-visibility sell price.'),
'weight' => -1,
),
……
'add_to_cart' => array(
'label' => t('Add to cart form'),
'description' => t('Add to cart form'),
'weight' => 10,
),
),
);
}
return $extra;
}
当然,这些伪字段并不是具有所有字段的属性,比如在字段的管理显示页面,就没有出现这些伪字段。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
有时候,一个字段模块自己提供的格式器,可能会出现不够用的情况,比如说图片字段,在Drupal7里面,它的格式器可以用来显示原始图片、各种缩略图,但是无法显示图片的链接。如果使用views模块的时候,想输出图片字段的URL,默认是不可能的,当然我们可以通过定制views的模板文件,来输出图片的URL。如果我们能够为图片提供一个URL路径格式器,就可以解决上述的问题了。
是的,在drupal.org上就存在着这样的一个第三方模块“Image URL Formatter”,专门来解决这个问题的,这个项目地址是http://drupal.org/project/image_url_formatter。我们来学习一下这个模块,这个模块的代码大部分是从image模块中直接拷贝过来的。
/**
* Implements hook_field_formatter_info().
*/
function image_url_formatter_field_formatter_info() {
$formatters = array(
'image_url' => array(
'label' => t('Image URL'),
'field types' => array('image'),
'settings' => array('image_style' => '', 'image_link' => ''),
),
);
return $formatters;
}
我们在这里为image字段定义了一个新的格式器“Image URL”,顾名思义,它就是用来输出图片的URL路径的。
/**
* Implements hook_field_formatter_settings_form().
*/
function image_url_formatter_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$image_styles = image_style_options(FALSE);
$element['image_style'] = array(
'#title' => t('Image style'),
'#type' => 'select',
'#default_value' => $settings['image_style'],
'#empty_option' => t('None (original image)'),
'#options' => $image_styles,
);
$link_types = array(
'content' => t('Content'),
'file' => t('File'),
);
$element['image_link'] = array(
'#title' => t('Link image url to'),
'#type' => 'select',
'#default_value' => $settings['image_link'],
'#empty_option' => t('Nothing'),
'#options' => $link_types,
);
return $element;
}
这里的定义和核心image模块中的对应实现完全一致。我们创建一个节点类型,并为其添加图片字段,然后启用这个模块,在管理显示页面,假定我们选择了Image URL作为显示格式,如图所示:
图片在管理显示页面的对应项
当我们点击最右边的配置按钮时,就会出现更多的配置选项,如图所示:
格式器自带的设置项
上述代码就对应于右边的两个配置项。这下我们明白了钩子hook_field_formatter_settings_form是用来做什么的。在这个钩子里面,API函数image_style_options值得学习一下,它用来获取图片的样式选项。
/**
* Implements hook_field_formatter_settings_summary().
*/
function image_url_formatter_field_formatter_settings_summary($field, $instance, $view_mode) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$summary = array();
$image_styles = image_style_options(FALSE);
// Unset possible 'No defined styles' option.
unset($image_styles['']);
// Styles could be lost because of enabled/disabled modules that defines
// their styles in code.
if (isset($image_styles[$settings['image_style']])) {
$summary[] = t('URL for image style: @style', array('@style' => $image_styles[$settings['image_style']]));
}
else {
$summary[] = t('Original image URL');
}
$link_types = array(
'content' => t('Linked to content'),
'file' => t('Linked to file'),
);
// Display this setting only if image is linked.
if (isset($link_types[$settings['image_link']])) {
$summary[] = $link_types[$settings['image_link']];
}
return implode('<br />', $summary);
}
上述代码就对应于图片字段,最右边配置按钮前面的那段描述,如图所示:
格式器具体配置的描述
这个钩子hook_field_formatter_settings_summary,就是为格式器的当前具体配置,返回一个简洁的描述,方便用户浏览。
/**
* Implements hook_field_formatter_view().
*/
function image_url_formatter_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
$element = array();
switch ($display['type']) {
case 'image_url':
// Check if the formatter involves a link.
if ($display['settings']['image_link'] == 'content') {
$uri = entity_uri($entity_type, $entity);
}
elseif ($display['settings']['image_link'] == 'file') {
$link_file = TRUE;
}
foreach ($items as $delta => $item) {
if (isset($link_file)) {
$uri = array(
'path' => file_create_url($item['uri']),
'options' => array(),
);
}
//debug($item);
$element[$delta] = array(
'#theme' => 'image_url_formatter',
'#item' => $item,
'#image_style' => $display['settings']['image_style'],
'#path' => isset($uri) ? $uri : '',
);
}
break;
}
return $element;
}
在这段代码中,我们返回了图片的呈现数组,在这个呈现数组的每个delta项中,对应的主题函数替换为了theme_image_url_formatter,而不是image模块里面的theme_image_formatter。这里面,API函数file_create_url用来把uri转换为URL,uri就是文件在Drupal里面存储的路径信息,但是这种信息,都是这种形式的“public://img/my123.jpg”,我们需要使用这个函数将其转换一下。
/**
* Implements hook_theme().
*/
function image_url_formatter_theme() {
return array(
'image_url_formatter' => array(
'variables' => array(
'item' => NULL,
'path' => NULL,
'image_style' => NULL,
),
),
);
}
/**
* Returns HTML for an image url field formatter.
*
* @param $variables
* An associative array containing:
* - item: An array of image data.
* - image_style: An optional image style.
* - path: An array containing the link 'path' and link 'options'.
*
* @ingroup themeable
*/
function theme_image_url_formatter($variables) {
$item = $variables['item'];
$image = array(
'path' => $item['uri'],
'alt' => $item['alt'],
);
// Do not output an empty 'title' attribute.
if (drupal_strlen($item['title']) > 0) {
$image['title'] = $item['title'];
}
$output = file_create_url($item['uri']);
if ($variables['image_style']) {
//debug($image);
$image['style_name'] = $variables['image_style'];
$output = image_style_url($image['style_name'], $item['uri']);
}
if ($variables['path']) {
$path = $variables['path']['path'];
$options = $variables['path']['options'];
// When displaying an image inside a link, the html option must be TRUE.
$options['html'] = TRUE;
$output = l($output, $path, $options);
}
return $output;
}
在这个主题函数中,我们返回的是图片的URL,而不是带有img标签的图片了。整个模块,全部都是复制拷贝image模块的代码,只有这里在逻辑上作了修改。image_style_url用来获取图片特定样式下对应图片的URL的。有时候,我们会用到这个函数。
我们看到,为一个已有的字段定义一个新的格式器,并不麻烦,而且很多代码都可以从原有模块中复制过来。采用这种方式的好处是,这个格式器定义好了以后,很多人都可以用,虽然定义的时候稍微麻烦了一点,但是以后会方便很多,送人玫瑰,手留余香。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
通过本章的学习,你应该可以:
自定义一个字段模块
为已有字段添加验证
了解什么是伪字段
为已有字段定制格式器
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
现在让我们实际的构建这个模块,我们不妨把这个模块的中文名字叫做“标题拼音字段”,英文名字叫做“Transliteration title”,这样更国际化一点。我们在sites\all\modules\custom目录下,创建一个名为transliteration_title的文件夹,向里面添加两个文件transliteration_title.info、transliteration_title.module。接着向info文件中添加以下信息:
name = Transliteration title
description = Transliteration title field.
core = 7.x
dependencies[] = transliteration
注意,我们这里使用了依赖关系,表示这个模块依赖于transliteration模块。如果没有安装于transliteration模块,并且这个模块现在还不存在于我们的站点目录下,那么我们就无法启用我们的这个自定义模块。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
接下来,我们向module文件中添加以下代码:
<?php
/**
* Implements hook_field_info().
*/
function transliteration_title_field_info() {
return array(
'transliteration_title_field' => array(
'label' => 'Transliteration title',
'description' => t('This field stores and renderes transliteration title.'),
'instance_settings' => array(
'size' => 255,
'upper_lower_case' => 'strtolower',
'source_field_id' => 'title',
),
'default_widget' => 'transliteration_title_field',
'default_formatter' => 'transliteration_title_default',
),
);
}
我们在这里实现了hook_field_info(),在这个钩子中,我们返回了一个关联数组,通过这个关联数组,我们定义了一个新的字段transliteration_title_field,它包含以下键:
label:表示这个字段的名字。
description: 表示这个字段的描述。
instance_settings: 表示这个字段实例的配置,这里给出的是默认配置,在创建好该字段后,可以修改这一配置,这个配置中的键是随意的,取决于你的实际需要。这里我们包含了三个键,size是默认textfield的大小,upper_lower_case表示字符串大小写的情况, source_field_id表示源字段的机读名字。
default_widget:默认的字段输入控件。这里为transliteration_title_field。
default_formatter:默认的字段显示格式器。这里为transliteration_title_default。
此时,如果我们启用这个模块,然后进入一个内容类型的管理字段页面,在字段类型的选择列表中,还找不到我们定义的这个字段,如图所示:
我们新建的字段类型还没有显示出来
除了实现hook_field_info()钩子以外,我们还需要实现多个钩子,才能让这个模块工作起来。我们按照这样的一个顺序,添加字段、配置字段、输入字段、显示字段,这样的一个顺序来开发我们的模块,这也是我们使用一个字段类型的通常顺序。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
让我们实现另一个钩子,向module文件中添加以下代码:
/**
* Implements hook_field_widget_info().
*/
function transliteration_title_field_widget_info() {
return array(
'transliteration_title_field' => array(
'label' => t('Transliteration title'),
'field types' => array('transliteration_title_field'),
'multiple values' => FIELD_BEHAVIOR_DEFAULT,
),
);
}
这里我们定义了这个字段的输入控件,字段输入控件的名字,和字段的名字这里是一样的,在Drupal7里面,很多字段类型都采用了这种习惯。当然,一个字段可以有多个控件类型,我们这里只有一个而已。此时,当我们再次访问一个内容类型的管理字段页面时,我们就可以添加“Transliteration title”字段了,如图所示:
字段类型“Transliteration title”显示了出来
我们看到,如果我们想要在管理字段页面显示出来我们的新建字段,我们需要实现两个钩子hook_field_info()和hook_field_widget_info()。
我们创建一个测试节点类型,比如“test type”,然后向该内容类型添加一个名为“Test”的“Transliteration title”字段,此时一切都能正常工作。但是,当我们配置这个字段,没有我们想要的配置字段,当我们想为这个字段输入内容时,我们发现在节点的输入表单中找不到这个字段。当我们检查这个字段创建的数据库表时,我们发现它并没有包含用来保存用户输入的列,如图所示:
字段对应表field_data_field_test的结构
为此,我们首先需要在添加这个字段时,能够正确地创建我们想要的表结构,对于这个字段,我们需要保存两个值,一个是转换后的字符串,我们不妨采用默认的value;一个表示是否是手动输入的,我们这里使用manual。
接下来,让我们添加一个transliteration_title.install文件,然后向该文件中添加以下内容:
<?php
/**
* @file
* Install file for the transliteration title module.
*/
/**
* Implements hook_field_schema().
*/
function transliteration_title_field_schema($field) {
return array(
'columns' => array(
'value' => array(
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'sortable' => TRUE,
),
'manual' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
);
}
注意,这个钩子和hook_schema有点类似,它是专门用于定义字段的表结构的。这里我们按照前面的要求,定义了两列:'value'、'manual'。
让我们删除以前添加的“Test”字段,再重新创建一遍。我们来看一下新字段对应数据库表的表结构,如图所示:
字段对应表field_data_field_test的结构
注意,transliteration_title_field_schema里面的'value'、'manual',对应于这个表结构中的field_test_value、field_test_manual。表结构中的其余列,在所有字段的field_data表中都是通用的。这里前缀“field_test”就是我们这个字段的机读名字。我们来看看对应关系:
“field_test” + “_” + “value” = “field_test_value”
“field_test” + “_” + “manual” = “field_test_manual”
字段名 + 列名
字段对应表名的命名规则:
“field_data” + “_” + “field_test” = “field_data_field_test”
“field_data” + “_” + “body” = “field_data_body”
“field_data” + 字段名
如果我们想要读取我们这个字段中的值,假定当前语言为未定义语言,也就是“und”:
$node->field_test['und'][0]['value'];
$node->field_test['und'][0]['manual'];
这里需要注意的是,并不是所有的字段,都采用“value”存储自己的值,比如分类术语字段,采用的就是“tid”,获取分类术语字段的值,所用的代码就是:
$node->field_myfield['und'][0]['tid'];
而不是
$node->field_myfield['und'][0]['value'];
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
接下来,让我们看一下,如何为字段实例添加自己的设置,在我们这里,不需要实现钩子hook_field_settings_form,我们只需要在字段控件的设置表单中,添加自己的设置即可。
/**
* Implements hook_field_widget_settings_form().
*/
function transliteration_title_field_widget_settings_form($field, $instance) {
$widget = $instance['widget'];
$settings = $widget['settings'];
$form['size'] = array(
'#type' => 'textfield',
'#title' => t('Size of textfield'),
'#default_value' => isset($settings['size'])?$settings['size']:'255',
'#required' => TRUE,
'#element_validate' => array('_element_validate_integer_positive'),
);
$form['source_field_id'] = array(
'#type' => 'textfield',
'#title' => t('Source field ID'),
'#default_value' => isset($settings['source_field_id'])?$settings['source_field_id']:'',
'#required' => TRUE,
);
$form['upper_lower_case'] = array(
'#type' => 'select',
'#title' => t('Case'),
'#default_value' => isset($settings['upper_lower_case'])?$settings['upper_lower_case']:'',
'#required' => TRUE,
'#options' => array(
'strtolower' => 'Lower case',
'strtoupper' => 'Upper case',
'ucfirst' => 'Capitalize first letter',
'ucwords' => 'Capitalize each word',
),
);
return $form;
}
在hook_field_widget_settings_form钩子实现里面,返回的是一个普通的表单数组,里面包含配置项对应的表单元素。在这里,我们定义了三个配置项,文本栏的尺寸、源字段ID、大小写情况。添加了上述代码以后,重新刷新字段的编辑页面,我们看到上面显示了我们定义的三个字段,如图所示:
我们定义的配置项在字段的编辑页面显示了出来
当提交这个表单时,配置选项的值会保存到控件设置里面,我们在后面会用到这里的设置。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
现在,该向字段里面输入信息了,此时,我们点击创建节点链接,来创建一个该类型下面的一个节点,此时在添加节点的表单中,并没有我们这个字段对应的表单元素。现在就让我们定义具体的输入控件,向module文件里面添加以下代码:
/**
* Implements hook_field_widget_form().
*/
function transliteration_title_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
$element += array(
'#type' => $instance['widget']['type'],
'#default_value' => isset($items[$delta]) ? $items[$delta] : '',
);
return $element;
}
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
这段代码看起来有点简单,加进来以后,并没有得到我们想要的效果,是的。我们还需要实现更多一点的钩子。在Drupal的表单元素中,部分表单元素是由其它表单元素复合而成的,比如date、file,以及一些第三方的表单元素。其实我们可以把我们的这个输入控件,定义成为一个表单元素类型,这样更有利于复用。我们来看看这个表单元素类型的定义:
/**
* Implements hook_element_info().
*/
function transliteration_title_element_info() {
$elements = array();
$elements['transliteration_title_field'] = array(
'#input' => TRUE,
'#process' => array('transliteration_title_field_process'),
'#theme' => 'transliteration_title_field',
'#theme_wrappers' => array('form_element'),
);
return $elements;
}
/**
* Process the transliteration_title type element before displaying the field.
*
* Build the form element. When creating a form using FAPI #process,
* note that $element['#value'] is already set.
*
* The $fields array is in $complete_form['#field_info'][$element['#field_name']].
*/
function transliteration_title_field_process($element, $form_state, $complete_form) {
$settings = &$form_state['field'][$element['#field_name']][$element['#language']]['instance']['settings'];
$element['value'] = array(
'#type' => 'textfield',
'#maxlength' => 255,
'#title' => t('transliteration title'),
'#required' => isset($element['#required']) ? $element['#required'] : FALSE,
'#default_value' => isset($element['#value']['value']) ? $element['#value']['value'] : NULL,
);
$element['manual'] = array(
'#type' => 'checkbox',
'#title' => t('manual'),
'#default_value' => isset($element['#value']['manual']) ? $element['#value']['manual'] : NULL,
);
return $element;
}
我们在前面讲表单元素的时候,就提到过hook_element_info(),这里我们使用这个钩子函数定义了一个自己的表单元素类型,为它指定了一个定制的处理函数,在transliteration_title_field_process里面,我们看到这个表单元素是由一个文本字段和一个复选框复合而成的。
让我们清空缓存,然后刷新添加节点的表单页面,此时我们看到了新增的这个表单元素了。它和普通的表单元素没有太大的区别,如图所示:
transliteration_title_field表单元素(输入控件)
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
对于表单元素,我们可以为其定义专门的主题函数,用来控制其外观,现在添加主题相关的代码,如下所示:
/**
* Implements hook_theme().
*/
function transliteration_title_theme() {
return array(
'transliteration_title_field' => array(
'render element' => 'element',
),
);
}
/**
* FAPI theme for an individual text elements.
*/
function theme_transliteration_title_field($vars) {
drupal_add_css(drupal_get_path('module', 'transliteration_title') .'/transliteration_title.css');
$element = $vars['element'];
$output = '';
$output .= '<div class="transliteration-title-field clearfix">';
$output .= '<div class="transliteration-title-field-value">'. drupal_render($element['value']) .'</div>';
$output .= '<div class="transliteration-title-field-manual">'. drupal_render($element['manual']) .'</div>';
$output .= '</div>';
return $output;
}
这里我们实现了hook_theme,在里面注册了theme_transliteration_title_field函数,在后者的具体实现里面,我们为表单元素添加了特有的html标签,用来控制CSS样式。当然,我们这里面并没有添加任何的CSS,所以看起来样式没有什么变化。
现在我们该考虑一下如何处理用户的输入了,也就是需要对用户的输入进行验证,把这个字段的输入,正确的保存到数据库中。当然现在也是可以工作的,但是还没有按照我们的要求工作。