作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
一个好的Drupal模块,通常能够充分的利用已有模块的各种功能,而不是重复的发明轮子。当我们打算实现这个面包屑模块的时候,有一个目标就是尽量的简单易用、代码精炼,充分的利用常用的第3方模块,能够帮助我们实现这个目标。我们这里主要集成Views、Rules,顺带集成Field Validation。前面两个都是社区内,排名非常靠前的模块,Field Validation就是老葛写的,集成一下自己写的模块,对于推广使用Field Validation也是很有帮助的。
Field validation的集成相对简单一点,而且我本人也非常的熟悉。从Field validation2.1开始,里面自带一个子模块,叫做property validation,我们这里集成的其实是property validation。一个实体通常由两部分组成,字段(Field)和属性(property),注意在Entity API里面将两者都统一为了属性;Field validation负责字段验证,property validation负责属性验证,将两者统一起来,是我的一个目标。我们这里主要想为path这个属性,添加一些验证,比如保证它是唯一的,保证这个路径在内部确实存在,不加这些验证,也能够正常工作。
清空缓存,向Views里面添加新的字段,此时就会看到我们这里定义的字段了:
选中,上面的删除、编辑链接,点击“添加并配置字段”按钮,但是我们却得到了这样的页面:
我多次清除缓存,都不起作用,包括到Views的高级配置页面,单独的清除views的缓存,仍然无效。我对比了一下model里面的代码,忽然想到了,需要在info文件里面,将这些类注册一下:
files[] = breadcrumb2.admin.inc
files[] = breadcrumb2.info.inc
files[] = breadcrumb2.test
files[] = views/breadcrumb2.views.inc
files[] = views/breadcrumb2_handler_link_field.inc
files[] = views/breadcrumb2_handler_delete_link_field.inc
files[] = views/breadcrumb2_handler_edit_link_field.inc
files[] = views/breadcrumb2_handler_breadcrumb_operations_field.inc
然后,再清空缓存。这下就正常了。很多人想不到,需要在info文件里面注册一下。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
由于前面,我们新增了编辑、删除两个链接,所以我们可以将字段“Breadcrumb: Breadcrumb ID (Breadcrumb ID)”和“Global: Custom text (Edit)”删除掉了。同时,将每页显示的10条,改为显示30条;设置“无结果行为”,无结果时显示信息“No breadcrumbs available.”;然后保存视图。
我们在实际的项目中,也经常需要将配置好的Views导出成代码的形式点击这里的导出链接即可
系统会为我们自动生成好代码。如下图所示:
我们将这里的代码复制下来,然后打开breadcrumb2.views.inc文件,在里面在里面添加钩子函数breadcrumb2_views_default_views,中间的部分,就是我们复制过来的:
/**
* Implements hook_views_default_views().
*/
function breadcrumb2_views_default_views() {
$view = new view();
$view->name = 'breadcrumbs';
$view->description = '';
$view->tag = 'default';
$view->base_table = 'breadcrumb';
$view->human_name = 'Breadcrumbs';
$view->core = 7;
$view->api_version = '3.0';
$view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
/* Display: Master */
$handler = $view->new_display('default', 'Master', 'default');
$handler->display->display_options['use_more_always'] = FALSE;
$handler->display->display_options['access']['type'] = 'none';
$handler->display->display_options['cache']['type'] = 'none';
$handler->display->display_options['query']['type'] = 'views_query';
$handler->display->display_options['exposed_form']['type'] = 'basic';
$handler->display->display_options['pager']['type'] = 'full';
$handler->display->display_options['pager']['options']['items_per_page'] = '30';
$handler->display->display_options['pager']['options']['offset'] = '0';
$handler->display->display_options['pager']['options']['id'] = '0';
$handler->display->display_options['pager']['options']['quantity'] = '9';
$handler->display->display_options['style_plugin'] = 'table';
$handler->display->display_options['style_options']['columns'] = array(
'bid' => 'bid',
);
$handler->display->display_options['style_options']['default'] = '-1';
$handler->display->display_options['style_options']['info'] = array(
'bid' => array(
'sortable' => 0,
'default_sort_order' => 'asc',
'align' => '',
'separator' => '',
'empty_column' => 0,
),
);
/* No results behavior: Global: Text area */
$handler->display->display_options['empty']['area']['id'] = 'area';
$handler->display->display_options['empty']['area']['table'] = 'views';
$handler->display->display_options['empty']['area']['field'] = 'area';
$handler->display->display_options['empty']['area']['empty'] = TRUE;
$handler->display->display_options['empty']['area']['content'] = 'No breadcrumbs available.';
$handler->display->display_options['empty']['area']['format'] = 'filtered_html';
/* Field: Breadcrumb: path */
$handler->display->display_options['fields']['path']['id'] = 'path';
$handler->display->display_options['fields']['path']['table'] = 'breadcrumb';
$handler->display->display_options['fields']['path']['field'] = 'path';
/* Field: Breadcrumb: Breadcrumb Link */
$handler->display->display_options['fields']['link']['id'] = 'link';
$handler->display->display_options['fields']['link']['table'] = 'field_data_link';
$handler->display->display_options['fields']['link']['field'] = 'link';
$handler->display->display_options['fields']['link']['click_sort_column'] = 'url';
$handler->display->display_options['fields']['link']['delta_offset'] = '0';
$handler->display->display_options['fields']['link']['separator'] = ' ? ';
/* Field: Breadcrumb: Delete Link */
$handler->display->display_options['fields']['delete_breadcrumb']['id'] = 'delete_breadcrumb';
$handler->display->display_options['fields']['delete_breadcrumb']['table'] = 'breadcrumb';
$handler->display->display_options['fields']['delete_breadcrumb']['field'] = 'delete_breadcrumb';
$handler->display->display_options['fields']['delete_breadcrumb']['label'] = '';
$handler->display->display_options['fields']['delete_breadcrumb']['element_label_colon'] = FALSE;
/* Field: Breadcrumb: Edit Link */
$handler->display->display_options['fields']['edit_breadcrumb']['id'] = 'edit_breadcrumb';
$handler->display->display_options['fields']['edit_breadcrumb']['table'] = 'breadcrumb';
$handler->display->display_options['fields']['edit_breadcrumb']['field'] = 'edit_breadcrumb';
$handler->display->display_options['fields']['edit_breadcrumb']['label'] = '';
$handler->display->display_options['fields']['edit_breadcrumb']['element_label_colon'] = FALSE;
/* Sort criterion: Breadcrumb: Breadcrumb ID */
$handler->display->display_options['sorts']['bid']['id'] = 'bid';
$handler->display->display_options['sorts']['bid']['table'] = 'breadcrumb';
$handler->display->display_options['sorts']['bid']['field'] = 'bid';
$handler->display->display_options['sorts']['bid']['order'] = 'DESC';
/* Filter criterion: Breadcrumb: path */
$handler->display->display_options['filters']['path']['id'] = 'path';
$handler->display->display_options['filters']['path']['table'] = 'breadcrumb';
$handler->display->display_options['filters']['path']['field'] = 'path';
$handler->display->display_options['filters']['path']['operator'] = 'contains';
$handler->display->display_options['filters']['path']['exposed'] = TRUE;
$handler->display->display_options['filters']['path']['expose']['operator_id'] = 'path_op';
$handler->display->display_options['filters']['path']['expose']['label'] = 'path';
$handler->display->display_options['filters']['path']['expose']['operator'] = 'path_op';
$handler->display->display_options['filters']['path']['expose']['identifier'] = 'path';
$handler->display->display_options['filters']['path']['expose']['remember_roles'] = array(
2 => '2',
1 => 0,
3 => 0,
);
$views[$view->name] = $view;
return $views;
}
最后的两行代码,是我们人工添加进来的。我们需要返回一个$views数组。我建议大家熟悉一下,这里导出的代码。我们以前在做项目的时候,有时候会遇到这种情况,一下子列出所有的条目,此时带有预览功能,系统会读取所有的条目,而条目数太大,以至于超出了PHP的内存限制,所以此时进入不了视图的编辑页面,此时我们可以将视图导出,然后修改导出后的代码,修改过后,一页显示10条记录,然后再导入这个视图的代码,就解决了问题。
此外,需要注意的是,Views无法导出特殊字符的,我们在Views里面,配置的时候,使用了“»”这个符号,导出的时候,变成了“?”,因为它是特殊字符。此时,我们可以将文件的编码格式转为UTF-8,然后将分隔符设置为“ » ”:
$handler->display->display_options['fields']['link']['separator'] = ' » ';
不过现在清空缓存,我们的代码无法被识别出来,删除链接还是删除链接,如果我们删除了刚才定义的views,就无法恢复回去。我经过很长时间的测试,调试,最后发现,如果将函数breadcrumb2_views_default_views放到breadcrumb2.module文件中来,就起作用,否则的话,就不起作用,最后,我向breadcrumb2.module文件,直接添加以下代码:
include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'breadcrumb2') . '/views/' . 'breadcrumb2.views.inc';
就是当加载module文件的时候,自动的加载breadcrumb2.views.inc,现在我们清除缓存,就只能revert(恢复)我们的视图了,无法删除它了。
我检查了model的代码,我的代码和它基本上完全一样的,但是在model里面,就不需要这个include_once。以前导出代码的时候,也从来不用这个的,Views会自动的加载这个文件。我不知道哪里出了问题。这个问题,就留在将来解决吧,反正现在问题不大。
我们这里只需要记住,将导出的代码放到hook_views_default_views的这个钩子函数中,并在函数最后,增加以下两个代码即可:
$views[$view->name] = $view;
return $views;
前面讲到,我们在module文件中,使用include_once,通过直接加载breadcrumb2.views.inc文件解决的问题,如果你到drupal.org/project/ breadcrumb2上面下载beta4以前的版本时,你会发现include_once这行代码都是存在的。
有句话,讲的很好,兼听则明,偏信则暗,我们写模块的时候,也应该多参照几个模块,我突然想把Rules集成和Views的集成分开来写,同时为两者增加更多一些内容。这个时候,我想到了,介绍一下自己写的Field collection views模块,这个模块主要也是集成Views。我打开本地的Field collection views模块一看,发现导出的views都放在了field_collection_views.views_default.inc这个文件中了。突然想起来,自己以前在Drupal6下面,也是放到这个文件中的。
我将field_collection_views.views_default.inc复制过来,重命名为breadcrumb2.views_default.inc,将文件的编码格式改为UTF-8,将breadcrumb2.views.inc文件中的breadcrumb2_views_default_views函数剪切到breadcrumb2.views_default.inc文件中,同时保存两个文件。将module文件中的include_once这行代码注释掉。
清除缓存,测试,一切正常。只能说model模块写的有问题,但是不知道为什么,它的导出的views就可以这样放,我的就不可以。这个问题,我们就不深究了。
我们这章,就主要讲Views了。刚才提到,我们受Field collection views模块的启发,我们这里介绍一个这个模块的代码。这个模块的用法,我们在Think in Drupal的第二集,里面已经介绍过了,是对Field Collection模块的一个很好的补充。Field collection views模块的主要功能,就是为Field collection类型的字段,提供一个formatter(格式化器),使用Views来呈现Field collection items。
我们来看一下module文件里面的代码:
/**
* Implements hook_field_formatter_info().
*/
function field_collection_views_field_formatter_info() {
return array(
'field_collection_views_view' => array(
'label' => t('Views field-collection items'),
'field types' => array('field_collection'),
'settings' => array(
'name' => 'field_collection_view',
'display_id' => 'default',
'add' => t('Add'),
),
),
);
}
/**
* Implements hook_field_formatter_view().
*/
function field_collection_views_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
$element = array();
$settings = $display['settings'];
switch ($display['type']) {
case 'field_collection_views_view':
//debug($items);
$args = '';
$i = 1;
foreach ($items as $delta => $item) {
if ($i == 1) {
$args .= $item['value'];
}
else {
$args .= '+' . $item['value'];
}
$i++;
}
$view_name = isset($settings['name']) ? $settings['name'] : 'field_collection_view';
$display_id = isset($settings['display_id']) ? $settings['display_id'] : 'default';
$content = views_embed_view($view_name, $display_id, $args);
$element[0] = array(
'#markup' => $content,
);
if (empty($items)) {
field_collection_field_formatter_links($element, $entity_type, $entity, $field, $instance, $langcode, $items, $display);
}
break;
}
return $element;
}
/**
* Implements hook_field_formatter_settings_form().
*/
function field_collection_views_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$elements['name'] = array(
'#type' => 'textfield',
'#title' => t('Name'),
'#default_value' => $settings['name'],
'#description' => t('The machine name of the view to embed.'),
);
$elements['display_id'] = array(
'#type' => 'textfield',
'#title' => t('Display id'),
'#default_value' => $settings['display_id'],
'#description' => t('The display id to embed.'),
);
$elements['add'] = array(
'#type' => 'textfield',
'#title' => t('Add link title'),
'#default_value' => $settings['add'],
'#description' => t('Leave the title empty, to hide the link.'),
);
return $elements;
}
/**
* Implements hook_field_formatter_settings_summary().
*/
function field_collection_views_field_formatter_settings_summary($field, $instance, $view_mode) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$links = array_filter(array_intersect_key($settings, array_flip(array('name', 'display_id'))));
if ($links) {
return '<em>Embed View:</em> ' . check_plain(implode(', ', $links));
}
else {
return t('Not showing any view.');
}
}
/**
* Implements hook_views_api().
*/
function field_collection_views_views_api() {
return array(
'api' => '3.0-alpha1',
'path' => drupal_get_path('module', 'field_collection_views') . '/views',
);
}
我们在Think in Drupal的第1集里面,讲字段API的时候,讲过,如何为已有字段定制格式器,我们这里做的就是这个工作,hook_field_formatter_info,hook_field_formatter_view,hook_field_formatter_settings_form,hook_field_formatter_settings_summary,这四个钩子函数,是定制格式器时,需要实现的四个钩子函数。
在field_collection_views_field_formatter_view里面,注意粗体字部分,这里面的逻辑是,我们获取所有的field collection item的ids,然后把它们作为参数传递给对应的视图(view)。views_embed_view,这个函数,我们已经非常熟悉了,注意这里的参数传递方式,我们这里使用了“+”号,来传递多个参数。这是实际当中,经常用的一个技巧。
最后,我们实现了hook_views_api,这里注意的是'api',我们这里用的是'3.0-alpha1',是Views的最初版本,如果把它改为'3.0',也是可以的。
在field_collection_views.views.inc,我们实现了hook_views_data这个钩子,为['field_collection_item']追加了几个新的字段,追加这些字段的目的是,为了在Views里面拼凑出来用来编辑、删除、添加的链接。
<?php
/**
* @file
* Provide extras views data for field_collection.module.
*/
/**
* Implements hook_views_data()
*/
function field_collection_views_views_data() {
// hostEntityId
$data['field_collection_item']['host_entity_id'] = array(
'title' => t('Host Entity ID'),
'help' => t('The ID of the Host Entity.'),
'field' => array(
'handler' => 'field_collection_views_handler_field_host_entity_id',
),
);
$data['field_collection_item']['host_entity_path'] = array(
'title' => t('Host Entity Path'),
'help' => t('The Path of the Host Entity.'),
'field' => array(
'handler' => 'field_collection_views_handler_field_host_entity_path',
),
);
$data['field_collection_item']['host_entity_type'] = array(
'title' => t('Host Entity Type'),
'help' => t('The Type of the Host Entity.'),
'field' => array(
'handler' => 'field_collection_views_handler_field_host_entity_type',
),
);
$data['field_collection_item']['field_path'] = array(
'title' => t('Field path'),
'help' => t('The base path of the field-collection field.'),
'field' => array(
'handler' => 'field_collection_views_handler_field_field_path',
),
);
return $data;
}
这里,有一些可以改进的地方,比如这里,我们可以使用hook_views_data_alter,而不是使用hook_views_data。为什么呢?因为我们这里没有定义自己的表,只是为已有的表添加字段而已。
另外的一个改进,可能就是,直接使用:
$data['field_collection_item']['edit_field_collection_item ']
$data['field_collection_item']['delete_field_collection_item ']
而不是去拼凑我们的字段了。
在field_collection_views.views_default.inc文件中,我们实现了hook_views_default_views,这里放置我们导出的代码。我把中间的代码,省略掉了:
/**
* Implementation of hook_views_default_views().
*/
function field_collection_views_views_default_views() {
$view = new view;
$view->name = 'field_collection_view';
$view->description = '';
$view->tag = 'default';
$view->base_table = 'field_collection_item';
$view->human_name = 'field collection view';
$view->core = 7;
$view->api_version = '3.0';
$view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
.....
$views[$view->name] = $view;
return $views;
}
我们这里给出一个最简单的例子,field_collection_views_handler_field_host_entity_id,这个和我们在前面实现的,简易程度差不多,都非常简单:
<?php
/**
* @file
* Contains the host_entity_id field handler.
*/
/**
* Field handler to display the host_entity_id
*/
class field_collection_views_handler_field_host_entity_id extends views_handler_field {
function query() {
// Do nothing, as this handler does not need to do anything to the query itself.
}
function option_definition() {
$options = parent::option_definition();
return $options;
}
function options_form(&$form, &$form_state) {
parent::options_form($form, $form_state);
}
/**
* Work out the host_entity_id
*/
function render($values) {
$host_entity_id = 0;
//$item_id = $this->get_value($values, 'item_id');
$item_id = $values->item_id;
//debug($values);
$field_collection_item = field_collection_item_load($item_id);
$host_entity_id = $field_collection_item->hostEntityId();
return $host_entity_id;
}
}
由于field_collection_views_handler_field_host_entity_id是一个类,所以我们需要在info文件中,注册一下这个文件,使用下面的代码:
files[] = views/field_collection_views.views.inc
files[] = views/field_collection_views_handler_field_host_entity_id.inc
files[] = views/field_collection_views_handler_field_host_entity_path.inc
files[] = views/field_collection_views_handler_field_field_path.inc
files[] = views/field_collection_views_handler_field_host_entity_type.inc
在实际项目中,大部分都已经有了Views的集成,我们需要做的,也就是用的到的,可能就是这里所讲的这些,导出views,在已有的基础上添加一个字段什么的。以后,有机会,我们介绍更多的Views的集成。
集成property validation,和集成Field validation的过程其实是一样的。首先是我们在后台配置验证规则,然后将其导出成代码,这和后面所讲的将Views的代码导出,是一样的。
启用Property validation模块以后,导航到admin/structure/property_validation:
这是Ctools的Export UI提供的一个默认界面,Property validation模块自带了一个验证规则title_min_words。我们这里添加两个验证规则,具体配置如下:
唯一性验证规则:
Name(机读) |
breadcrumb_path_unique |
Rule Name(用户可读) |
Breadcrumb path unique |
Entity type |
Breadcrumb |
Bundle name |
Breadcrumb |
Property name |
path |
Validator |
Unique values |
Scope of unique |
Entity |
Custom error message |
Breadcrumb path should be unique. |
URL有效性验证规则:
Name(机读) |
breadcrumb_path_valid_path |
Rule Name(用户可读) |
Breadcrumb path valid path |
Entity type |
Breadcrumb |
Bundle name |
Breadcrumb |
Property name |
path |
Validator |
URL |
Internal path |
选中 |
Custom error message |
Breadcrumb path should be a valid path. |
现在,我们通过breadcrumb/add,添加面包屑,发现验证规则不起作用,比如第一次将path输入为“node/1”,第二次还是输入“node/1”,唯一性验证不起作用。此外,输入一个不存在的路径,比如“admin1”,还是可以直接保存。但是编辑的时候就会报错。经检查,并不是,property validation模块的问题,而是我们自己这里代码写的有问题。我们对breadcrumb2.admin.inc文件里面的breadcrumb2_form_validate函数,做以下修改:
function breadcrumb2_form_validate($form, &$form_state) {
$breadcrumb = $form_state['breadcrumb'];
$breadcrumb->path = $form_state['values']['path'];
// Notify field widgets to validate their data.
field_attach_form_validate('breadcrumb2', $breadcrumb, $form, $form_state);
}
粗体字部分,为我们新增的代码。我们把$breadcrumb对象存储在了$form_state['breadcrumb']里面,但是这里的值,都是旧的,我们这里验证的时候,需要将属性的值重新设置一下。field_attach_form_validate能够自动的提取当前字段的值,对于属性的值,我们需要单独的设置。这就是粗体字代码的含义。
修正这个错误以后,重新测试,现在这两个验证规则,都起作用了。唯一不足的时候,URL支持的内部路径,没有将别名排除出去,不过这个影响不大。
我们回到admin/structure/property_validation,对于每个验证规则,它右边都有一组操作链接,默认为编辑,我们展开所有的操作链接,点击导出(Export)链接。下面是唯一性验证导出后的代码:
$rule = new stdClass();
$rule->disabled = FALSE; /* Edit this to true to make a default rule disabled initially */
$rule->api_version = 2;
$rule->rulename = 'Breadcrumb path unique';
$rule->name = 'breadcrumb_path_unique';
$rule->property_name = 'path';
$rule->entity_type = 'breadcrumb2';
$rule->bundle = 'breadcrumb2';
$rule->validator = 'property_validation_unique_validator';
$rule->settings = array(
'data' => 'entity',
);
$rule->error_message = 'Breadcrumb path should be unique.';
我们这里的导出,是基于Crools的Export插件功能的。可能读到这里的时候,很多人会问,Crools是怎么智能的导出代码呢?有兴趣的可以阅读以下ctools\includes\export.inc里面的ctools_export_object函数,这个函数负责导出对象。我开始的时候,对这个导出机制也理解的不是很透彻,后来阅读了这个函数的源代码,才知道有些东西是怎么来的,在哪里定义的。
接着,我们在module文件里面,追加以下代码:
/**
* Implementation of hook_ctools_plugin_api().
*
* Tell Ctools that we support the default_property_validation_rules API.
*/
function breadcrumb2_ctools_plugin_api($owner, $api) {
if ($owner == 'property_validation' && $api == 'default_property_validation_rules') {
return array('version' => 2);
}
}
这里我们实现了hook_ctools_plugin_api,通过这个钩子函数,我们告诉Ctools,我们实现了默认的property_validation验证规则,如果你熟悉了钩子函数以后,你发现所有的钩子函数都是很类似的,这里也一样。钩子函数里面的逻辑通常都比较简单。
接下来,我们新建一个breadcrumb2.default_property_validation_rules.inc文件,在里面添加以下代码:
<?php
/**
* @file
* Provides default property validation rules for breadcrumb path.
*/
/**
* Implementation of hook_default_property_validation_rule().
*
* Provide default validation rules.
*/
function breadcrumb2_default_property_validation_rule() {
$export = array();
$rule = new stdClass();
$rule->disabled = FALSE; /* Edit this to true to make a default rule disabled initially */
$rule->api_version = 2;
$rule->rulename = 'Breadcrumb path unique';
$rule->name = 'breadcrumb_path_unique';
$rule->property_name = 'path';
$rule->entity_type = 'breadcrumb2';
$rule->bundle = 'breadcrumb2';
$rule->validator = 'property_validation_unique_validator';
$rule->settings = array(
'data' => 'entity',
);
$rule->error_message = 'Breadcrumb path should be unique.';
$export['breadcrumb_path_unique'] = $rule;
$rule = new stdClass();
$rule->disabled = FALSE; /* Edit this to true to make a default rule disabled initially */
$rule->api_version = 2;
$rule->rulename = 'Breadcrumb path valid path';
$rule->name = 'breadcrumb_path_valid_path';
$rule->property_name = 'path';
$rule->entity_type = 'breadcrumb2';
$rule->bundle = 'breadcrumb2';
$rule->validator = 'property_validation_url_validator';
$rule->settings = array(
'external' => 0,
'internal' => 1,
);
$rule->error_message = 'Breadcrumb path should be a valid path.';
$export['breadcrumb_path_valid_path'] = $rule;
return $export;
}
通过钩子hook_default_property_validation_rule可以定义验证规则。这个钩子函数里面,代码都是我们使用Ctools导出来的,将导出的代码,复制过来的时候,我们需要稍微的调整一下代码格式。
现在我们清除缓存,重新回到admin/structure/property_validation,此时右边的操作链接里面,“删除”(delete)链接没有了,换成了恢复(revert):
而在存储(Storage)里面,也显示的是“Overridden”了。如果我们恢复(Revert)一下,“Overridden”就变成“Default”了。这里的配置和Views里面是一致的。
Field validation模块,正在日趋流行,但是安装量还没有达到3,4万的规模,之所以集成它,主要为了大家展示一下Ctools导出插件的具体应用,另外老葛也希望,Breadcrumb2这个模块可以为Field validation模块带来更多的安装量。我们下面来看看Views的集成,这是项目中经常用的。
我们的面包屑模块,是基于Entity API的,Entity API提供了基本的Views集成,我们这里要做的是,在它的基础之上再添加一些集成,从而充分满足我们的需要。有兴趣的可以阅读一下entity\views下面的源代码。需要说明一下的是,这里的代码非常抽象,我看了以后,也只是大致了解了一下代码的结构。不过这并不影响,我们接下来的工作。
我们回到admin/structure/breadcrumbs,现在这个页面还是一个空白页面,我们这里想要显示的内容是,面包屑列表,并提供按照路径的查询功能。同时可以编辑面包屑,删除面包屑,还可以直接添加面包屑。
我们首先创建一个Views,导航到admin/structure/views,点击添加视图(Add new view),做以下配置:
在“Show”的下拉选择框里面,已经包含了“Breadcrumbs”了,这是Entity API模块提供的集成。
点击继续并编辑按钮,这里主要添加了三个字段,“Breadcrumb: path (path) ”、“Breadcrumb: Breadcrumb Link (Breadcrumb Link)”、 “Global: Custom text (Edit)”;对于“Breadcrumb: Breadcrumb Link (Breadcrumb Link)”字段,由于它是多值的,默认的分隔符为“, ”,我们将它修改为了“ » ”,和Drupal核心保持一致;对于“Global: Custom text (Edit)”,我们覆写了它的输出,将它输出成链接的形式,指定的路径为“breadcrumb/[bid]/edit”;之后将Breadcrumb: Breadcrumb ID (Breadcrumb ID)字段排除显示;将格式“Format”设置为表格的形式;添加过滤器“Breadcrumb: path (exposed)”,并将其暴露出来;添加排序标准“Breadcrumb: Breadcrumb ID (desc)”,按照降序排列,这样新增的面包屑放在前面。这是配置好的样子:
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
接下来,修改breadcrumb2.admin.inc文件里面的breadcrumb2_overview_breadcrumbs函数,这是修改后的样子:
/**
* Displays the breadcrumb admin overview page.
*/
function breadcrumb2_overview_breadcrumbs(){
$build['#markup'] = views_embed_view('breadcrumbs','default');
return $build;
}
现在我们回到admin/structure/breadcrumbs,看一下效果,我这里预先定义了很多面包屑,这里给出一个简单的截图:
下面的分页功能,是正常的,如果对这个页面,做比较多的测试的话,会发现这里面存在一个问题。在上面的搜索框里面,输出“node/1”,点击应用按钮。此时页面直接跳转到了http://localhost/breadcrumb2/?path=node%2F1&=Apply。这个时候,我们会发现views_embed_view这个函数的局限性了。它无法将当前页面的路径传递给暴露出来的表单。为了解决这个问题,我们将代码修改为:
/**
* Displays the breadcrumb admin overview page.
*/
function breadcrumb2_overview_breadcrumbs(){
//$build['#markup'] = views_embed_view('breadcrumbs','default');
$view = views_get_view('breadcrumbs', 'default');
$view->override_url = $_GET['q'];
return $view->preview();
//return $build;
}
这里面,我们首先使用了views_get_view,获取视图,接着使用$view->override_url覆写URL,这里使用的是当前路径,这样就可以将当前路径传递给暴露出来的表单了。最后使用$view->preview()获取内容。通过这种方式,就解决了前面所说的问题。不过还是有一个很小的问题,但是不影响过程,就是搜索的时候,Overlay不起作用了。
访问admin/content页面,在最上面,有一个“Add content”链接,我们也想添加一个这样的链接,方法有很多,第一个方法,就是把链接放到breadcrumb2_overview_breadcrumbs函数里面,通过这个函数,加进来。第二种方法,就是通过node里面提供的方法添加。打开node.module文件,它里面有这样的钩子实现:
/**
* Implements hook_menu_local_tasks_alter().
*/
function node_menu_local_tasks_alter(&$data, $router_item, $root_path) {
// Add action link to 'node/add' on 'admin/content' page.
if ($root_path == 'admin/content') {
$item = menu_get_item('node/add');
if ($item['access']) {
$data['actions']['output'][] = array(
'#theme' => 'menu_local_action',
'#link' => $item,
);
}
}
}
这就是节点模块里面的实现方法,通过hook_menu_local_tasks_alter这个钩子实现。我开始也不知道这个钩子的含义,是我想到的了这个功能,然后在node.module文件里面逐个函数的查找、浏览,最终才发现的。
我们把它改造一下,改造成我们想要的,向breadcrumb2.module文件中,添加以下代码:
/**
* Implements hook_menu_local_tasks_alter().
*/
function breadcrumb2_menu_local_tasks_alter(&$data, $router_item, $root_path) {
// Add action link to 'breadcrumb/add' on 'admin/structure/breadcrumbs' page.
if ($root_path == 'admin/structure/breadcrumbs') {
$item = menu_get_item('breadcrumb/add');
if ($item['access']) {
$data['actions']['output'][] = array(
'#theme' => 'menu_local_action',
'#link' => $item,
);
}
}
}
清除缓存,就可以看到“Add breadcrumb”链接了:
说到节点模块,我们还会主要到一个功能,当我们点击node/add这个链接时,它会自动的在覆盖层(Overlay)中打开,我们也希望,当用户点击“Add breadcrumb”,在弹出的覆盖层里面,打开我们的添加表单,而不是直接访问breadcrumb/add页面。
这个功能,节点模块里面就有,通过查找node.module文件里面的函数,我注意到了一个钩子函数的实现:
/**
* Implements hook_admin_paths().
*/
function node_admin_paths() {
if (variable_get('node_admin_theme')) {
$paths = array(
'node/*/edit' => TRUE,
'node/*/delete' => TRUE,
'node/*/revisions' => TRUE,
'node/*/revisions/*/revert' => TRUE,
'node/*/revisions/*/delete' => TRUE,
'node/add' => TRUE,
'node/add/*' => TRUE,
);
return $paths;
}
}
因为我们知道,后台的管理界面,通常都是通过Overlay打开的。钩子函数hook_admin_paths,能够将一些路径设置为管理路径,尽管这些路径没有以admin打头。同样,依葫芦画瓢,改造一下,向我们的module文件追加以下代码:
/**
* Implements hook_admin_paths().
*/
function breadcrumb2_admin_paths() {
$paths = array(
'breadcrumb/*' => TRUE,
'breadcrumb/*/edit' => TRUE,
'breadcrumb/*/delete' => TRUE,
'breadcrumb/add' => TRUE,
);
return $paths;
}
这里,我们将面包屑相关的操作链接,定义成为了管理路径,清除缓存,重新点击“Add breadcrumb”,在弹出框中显示出来了添加表单。
以前是这个样子的:
现在在Overlay中打开了:
这是一个进步。
我们在前面的Views的配置里面,已经看到,现有的功能已经基本能够满足我们的需求了。有时候,我们还需要为Views提供更多地字段,这里的更多,指的是超出了Entity API默认提供的那一部分。比如面包屑的编辑、删除链接,我们想直接提供出来,而不是通过字段覆写的方式实现。
向breadcrumb2.module文件追加以下代码:
/**
* Implements hook_views_api().
*/
function breadcrumb2_views_api() {
return array(
'api' => 3,
'path' => drupal_get_path('module', 'breadcrumb2') . '/views',
);
}
我们这里实现了钩子hook_views_api,并将我们Views集成的文件,统一放在了breadcrumb2\views目录下面,我们现在就创建这样的一个子目录文件夹。接下来,我们在这个子文件夹下面创建一个文件breadcrumb2.views.inc,并向里面添加以下代码:
<?php
/**
* @file
* Providing extra integration with views.
*/
/**
* Implements hook_views_data_alter ()
*/
function breadcrumb2_views_data_alter(&$data) {
$data['breadcrumb']['link_breadcrumb'] = array(
'field' => array(
'title' => t('Link'),
'help' => t('Provide a link to the breadcrumb.'),
'handler' => 'breadcrumb2_handler_link_field',
),
);
$data['breadcrumb']['edit_breadcrumb'] = array(
'field' => array(
'title' => t('Edit Link'),
'help' => t('Provide a link to the edit form for the breadcrumb.'),
'handler' => 'breadcrumb2_handler_edit_link_field',
),
);
$data['breadcrumb']['delete_breadcrumb'] = array(
'field' => array(
'title' => t('Delete Link'),
'help' => t('Provide a link to delete the breadcrumb.'),
'handler' => 'breadcrumb2_handler_delete_link_field',
),
);
// This content of this field are decided based on the menu structure that
// follows breadcrumb/%breadcrumb2/op
$data['breadcrumb']['operations'] = array(
'field' => array(
'title' => t('Operations links'),
'help' => t('Display all operations available for this breadcrumb.'),
'handler' => 'breadcrumb2_handler_breadcrumb_operations_field',
),
);
}
我们通过实现hook_views_data_alter (),向breadcrumb 的Views中,添加了4个字段,分别为link_breadcrumb、edit_breadcrumb、delete_breadcrumb、operations。用来方便的编辑、删除、查看、管理面包屑。
这里的$data就是一个大的数组,$data['breadcrumb']是基表,$data['breadcrumb']['edit_breadcrumb']是下面的字段,里面的title、help键就不用介绍了,handler用来指定哪个类负责处理这个字段。
对于breadcrumb2_handler_link_field ,我们需要在breadcrumb2\views下面创建一个名为breadcrumb2_handler_link_field.inc的文件,文件创建好以后,里面添加以下代码:
<?php
/**
* @file
* Contains a Views field handler to take care of displaying links to entities
* as fields.
*/
class breadcrumb2_handler_link_field extends views_handler_field {
function construct() {
parent::construct();
$this->additional_fields['bid'] = 'bid';
}
function option_definition() {
$options = parent::option_definition();
$options['text'] = array('default' => '', 'translatable' => TRUE);
return $options;
}
function options_form(&$form, &$form_state) {
parent::options_form($form, $form_state);
$form['text'] = array(
'#type' => 'textfield',
'#title' => t('Text to display'),
'#default_value' => $this->options['text'],
);
}
function query() {
$this->ensure_my_table();
$this->add_additional_fields();
}
function render($values) {
$text = !empty($this->options['text']) ? $this->options['text'] : t('view');
$bid = $values->{$this->aliases['bid']};
return l($text, 'breadcrumb/' . $bid);
}
}
在这个文件里面,我们定义了一个类breadcrumb2_handler_link_field,它继承了类views_handler_field。在定义里面,它实现了构造函数construct,选项定义option_definition,选项表单options_form,查询query(),呈现render($values),四个成员函数。options_form就是在Views里面添加字段时,弹出来的配置表单对话框;render($values)是负责呈现这个字段的;query()里面,可以加点查询什么的,我们这里比较简单。
接下来在breadcrumb2\views下面创建一个名为breadcrumb2_handler_edit_link_field.inc的文件,文件创建好以后,里面添加以下代码:
<?php
/**
* @file
* Contains a Views field handler to take care of displaying edit links
* as fields
*/
class breadcrumb2_handler_edit_link_field extends breadcrumb2_handler_link_field {
function construct() {
parent::construct();
}
function render($values) {
// Check access.
if (!user_access('administer breadcrumbs')) {
return;
}
$text = !empty($this->options['text']) ? $this->options['text'] : t('edit');
$bid = $values->{$this->aliases['bid']};
return l($text, 'breadcrumb/' . $bid . '/edit');
}
}
breadcrumb2_handler_edit_link_field的定义就比较简单,这里归功于面向对象的继承,在呈现函数里面我们构建了一个编辑链接。
然后再创建breadcrumb2_handler_delete_link_field.inc文件,里面的代码如下:
<?php
/**
* @file
* Contains a Views field handler to take care of displaying deletes links
* as fields
*/
class breadcrumb2_handler_delete_link_field extends breadcrumb2_handler_link_field {
function construct() {
parent::construct();
}
function render($values) {
// Check access.
if (!user_access('administer breadcrumbs')) {
return;
}
$text = !empty($this->options['text']) ? $this->options['text'] : t('delete');
$bid = $values->{$this->aliases['bid']};
return l($text, 'breadcrumb/' . $bid . '/delete');
}
}
最后创建breadcrumb2_handler_breadcrumb_operations_field.inc文件,并添加以下代码:
<?php
/**
* This field handler aggregates operations that can be done on a breadcrumb
* under a single field providing a more flexible way to present them in a view
*/
class breadcrumb2_handler_breadcrumb_operations_field extends views_handler_field {
function construct() {
parent::construct();
$this->additional_fields['bid'] = 'bid';
}
function query() {
$this->ensure_my_table();
$this->add_additional_fields();
}
function render($values) {
$links = menu_contextual_links('breadcrumb2', 'breadcrumb', array($this->get_value($values, 'bid')));
if (!empty($links)) {
return theme('links', array('links' => $links, 'attributes' => array('class' => array('links', 'inline', 'operations'))));
}
}
}
在breadcrumb2_handler_breadcrumb_operations_field里面,我们使用了menu_contextual_links来返回对应的链接数组,并使用theme('links'来呈现这些链接。
如果对这里的代码,不理解的话,可以参看model里面的定义,和那里的定义是一样的。我只是修改了一下。有关Views集成的更多代码,可以直接参看Views对Drupal核心模块的支持,位于目录views\modules目录下面。在Think in Drupal的第三集里面,我曾经向Ubercart提交了一个补丁,这个补丁就是有关Views集成的。我补丁里面的代码如下,粗体部分就是我添加的:
/**
* Implements hook_views_data().
*/
function uc_order_views_data() {
...
// Ordered products.
$data['uc_order_products']['table']['group'] = t('Ordered product');
$data['uc_order_products']['table']['base'] = array(
'field' => 'order_product_id',
'title' => t('Ordered products'),
'help' => t('Products that have been ordered in your Ubercart store.'),
);
// Expose nodes to ordered products as a relationship.
$data['uc_order_products']['nid'] = array(
'title' => t('Nid'),
'help' => t('The nid of the ordered product. If you need more fields than the nid: Node relationship'),
'relationship' => array(
'title' => t('Node'),
'help' => t('Relate product to node.'),
'handler' => 'views_handler_relationship',
'base' => 'node',
'field' => 'nid',
'label' => t('node'),
),
'filter' => array(
'handler' => 'views_handler_filter_numeric',
),
'argument' => array(
'handler' => 'views_handler_argument_node_nid',
),
'field' => array(
'handler' => 'views_handler_field_node',
),
);
// Expose orders to ordered products as a relationship.
$data['uc_order_products']['order_id'] = array(
'title' => t('Order ID'),
'help' => t('The order ID of the ordered product. If you need more fields than the order ID: Order relationship'),
'relationship' => array(
'title' => t('Order'),
'help' => t('Relate product to order.'),
'handler' => 'views_handler_relationship',
'base' => 'uc_orders',
'field' => 'order_id',
'label' => t('order'),
),
'filter' => array(
'handler' => 'views_handler_filter_numeric',
),
'argument' => array(
'handler' => 'views_handler_argument_numeric',
),
'field' => array(
'handler' => 'uc_order_handler_field_order_id',
),
);
// Pull in node fields directly.
$data['node']['table']['join']['uc_order_products'] = array(
'left_field' => 'nid',
'field' => 'nid',
);
// Pull in product fields directly.
$data['uc_products']['table']['join']['uc_order_products'] = array(
'left_field' => 'nid',
'field' => 'nid',
);
....
return $data;
}
我们看到,将我们的数据库表,集成到Views里面并不复杂,只需要实现hook_views_data即可;如果要修改其它模块的hook_views_data,则可以使用hook_views_data_alter。