实际上,到现在为止,我们模块的优势,还没有体现出来,只有实现了Rules的集成,才能体现出来,不然的话,一个一个的添加面包屑,是不现实的。
我们的想法,很简单,就是在创建节点的时候,通过Rules,创建对应的面包屑对象。这样当显示这个节点时,我们就可以调用这个面包屑对象了。我们首先实现一个基于分类的面包屑。面包屑的结构是这样的:
首页 » 图书 » 图书分类1 » 图书子分类1
首页 » 新闻 » 新闻分类1
基本是按照分类术语的层级结构进行了,这个结构和网上书店系统里面的结构一致。我们的目标,就是方便的实现这样的面包屑结构,并且允许用户通过Rules的后台进行进一步的配置。在我们继续进行之前,我们先来介绍一下Rules的基本术语。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
Rules,我常常翻译成为规则,它包含三个组成部分:事件(Event)、条件(Condition)、动作(Action)。事件和动作是必须的,条件是可选的,但是大部分rules里面都是有条件的。
Event(事件),就是发生了什么。比如,新增了一个节点、更新了一个节点、删除了一个节点、查看一个节点,这些都是一个事件。Rules核心自带了很多事件,我们也可以创建自己的事件。
Condition(条件),这个类似于Panels的选择规则,说白了就是一组if语句,来检查一下是否满足当前的条件,如果满足,则执行相应的动则;否则就会跳过。举个例子,比如说一个节点的内容类型,它是不是Page类型的;再比如,这个节点是否推荐到了首页、节点是否被发表,等等,这些都是条件。在Rules的条件里面,支持关系运算“or”和“and”,多个条件可以使用这两个符号连接,从而合成更复杂的条件。
Action(动作):就是将要执行的东西,比如发送一封电子邮件、显示一条提示消息、创建一个新的实体。在动作里面,支持循环(loop),在循环里面,可以继续添加动作。
我们来看一下,三者的关系图:
我们实现了在新建节点时,使用rules自动创建一个面包屑对象。现在我们将这个规则克隆一下,将克隆后的名字改为“breadcrumb for node update”,并保存。接着,添加一个新的事件“After updating existing content”(在更新已有内容之后),删除原来的事件“After saving new content”(新建内容之后)。这样当系统更新节点时,我们也会自动的更新对应的面包屑对象。
接着,新建一个规则“breadcrumb for term create”,事件为“After saving a new term”,配置好的动作为:
然后以它为基础,克隆一份,将克隆后的命名为“breadcrumb for term update”,添加事件“After updating an existing term”,删除原来的事件。保存即可。
这样,当创建一个分类术语时,Rules会自动为它创建一个面包屑对象,最后生成的面包屑和系统自带的是略有区别的,在我们的面包屑对象里面,最前面是词汇表的链接,同时包含了当前分类术语。其它都和系统默认的一致。
现在导航到admin/config/workflow/rules页面,点击规则“breadcrumb for node create”右边的export链接,rules会为我们生成代码形式。
我们要做的工作就是,新建一个文件breadcrumb2.rules_defaults.inc,在这里面输入以下代码:
<?php
/**
* @file
* breadcrumb2.rules_defaults.inc
*/
/**
* Implementation of hook_default_rules_configuration().
*/
function breadcrumb2_default_rules_configuration() {
$items = array();
$items['rules_breadcrumb_for_node_create'] = entity_import('rules_config', ' ');
return $items;
}
在这里,$items的键使用的是规则的机读名字'rules_breadcrumb_for_node_create',我们使用了函数entity_import负责导入,第一个参数为实体类型,这里为'rules_config',第二个参数,就是导出的代码;这个函数的姐妹函数应为entity_export,负责导出的,都是Entity API模块提供的。将我们导出的代码复制粘贴到下面这句代码的最后的两个单引号之间:
$items['rules_breadcrumb_for_node_create'] = entity_import('rules_config', '复制粘贴到这里');
最终效果:
function breadcrumb2_default_rules_configuration() {
$items = array();
$items['rules_breadcrumb_for_node_create'] = entity_import('rules_config', '{ "rules_breadcrumb_for_node_create" : {
"LABEL" : "breadcrumb for node create",
"PLUGIN" : "reaction rule",
"REQUIRES" : [ "rules", "breadcrumb2" ],
"ON" : [ "node_insert" ],
"DO" : [
{ "entity_create" : {
"USING" : { "type" : "breadcrumb2" },
"PROVIDE" : { "entity_created" : { "breadcrumb" : "Breadcrumb" } }
}
},
{ "data_set" : { "data" : [ "breadcrumb:path" ], "value" : "node\/[node:nid]" } },
{ "breadcrumb2_append_breadcrumb_trail" : {
"breadcrumb" : [ "breadcrumb" ],
"title" : "[node:type-name]s",
"url" : "[node:type]s"
}
},
{ "breadcrumb2_fetch_lightest_term_from_node" : {
"USING" : { "node" : [ "node" ] },
"PROVIDE" : { "lightest_term" : { "lightest_term" : "First term" } }
}
},
{ "breadcrumb2_taxonomy_get_parents_all" : {
"USING" : { "taxonomy_term" : [ "lightest-term" ] },
"PROVIDE" : { "parent_terms" : { "parent_terms" : "Parent terms" } }
}
},
{ "LOOP" : {
"USING" : { "list" : [ "parent-terms" ] },
"ITEM" : { "parent_term" : "Parent term" },
"DO" : [
{ "breadcrumb2_append_breadcrumb_trail" : {
"breadcrumb" : [ "breadcrumb" ],
"title" : "[parent-term:name]",
"url" : "[node:type]s\/category\/[parent-term:tid]"
}
}
]
}
}
]
}
}');
return $items;
}
接着,我们把其余的规则,也导出来,放到代码里面:
function breadcrumb2_default_rules_configuration() {
$items = array();
……
$items['rules_breadcrumb_for_node_update'] = entity_import('rules_config', '{ "rules_breadcrumb_for_node_update" : {
"LABEL" : "breadcrumb for node update",
"PLUGIN" : "reaction rule",
"REQUIRES" : [ "rules", "breadcrumb2" ],
"ON" : [ "node_update" ],
"DO" : [
{ "entity_create" : {
"USING" : { "type" : "breadcrumb2" },
"PROVIDE" : { "entity_created" : { "breadcrumb" : "Breadcrumb" } }
}
},
{ "data_set" : { "data" : [ "breadcrumb:path" ], "value" : "node\/[node:nid]" } },
{ "breadcrumb2_append_breadcrumb_trail" : {
"breadcrumb" : [ "breadcrumb" ],
"title" : "[node:type-name]s",
"url" : "[node:type]s"
}
},
{ "breadcrumb2_fetch_lightest_term_from_node" : {
"USING" : { "node" : [ "node" ] },
"PROVIDE" : { "lightest_term" : { "lightest_term" : "First term" } }
}
},
{ "breadcrumb2_taxonomy_get_parents_all" : {
"USING" : { "taxonomy_term" : [ "lightest-term" ] },
"PROVIDE" : { "parent_terms" : { "parent_terms" : "Parent terms" } }
}
},
{ "LOOP" : {
"USING" : { "list" : [ "parent-terms" ] },
"ITEM" : { "parent_term" : "Parent term" },
"DO" : [
{ "breadcrumb2_append_breadcrumb_trail" : {
"breadcrumb" : [ "breadcrumb" ],
"title" : "[parent-term:name]",
"url" : "[node:type]s\/category\/[parent-term:tid]"
}
}
]
}
}
]
}
}');
$items['rules_breadcrumb_for_term_create'] = entity_import('rules_config', '{ "rules_breadcrumb_for_term_create" : {
"LABEL" : "breadcrumb for term create",
"PLUGIN" : "reaction rule",
"REQUIRES" : [ "rules", "breadcrumb2", "taxonomy" ],
"ON" : [ "taxonomy_term_insert" ],
"DO" : [
{ "entity_create" : {
"USING" : { "type" : "breadcrumb2" },
"PROVIDE" : { "entity_created" : { "breadcrumb" : "Breadcrumb" } }
}
},
{ "data_set" : { "data" : [ "breadcrumb:path" ], "value" : "taxonomy\/term\/[term:tid]" } },
{ "breadcrumb2_append_breadcrumb_trail" : {
"breadcrumb" : [ "breadcrumb" ],
"title" : "[term:vocabulary:name]",
"url" : "[term:vocabulary:machine-name]"
}
},
{ "breadcrumb2_taxonomy_get_parents_all" : {
"USING" : { "taxonomy_term" : [ "term" ] },
"PROVIDE" : { "parent_terms" : { "parent_terms" : "Parent terms" } }
}
},
{ "LOOP" : {
"USING" : { "list" : [ "parent-terms" ] },
"ITEM" : { "parent_term" : "Parent term" },
"DO" : [
{ "breadcrumb2_append_breadcrumb_trail" : {
"breadcrumb" : [ "breadcrumb" ],
"title" : "[parent-term:name]",
"url" : "taxonomy\/term\/[parent-term:tid]"
}
}
]
}
}
]
}
}');
$items['rules_breadcrumb_for_term_update'] = entity_import('rules_config', '{ "rules_breadcrumb_for_term_update" : {
"LABEL" : "breadcrumb for term update",
"PLUGIN" : "reaction rule",
"REQUIRES" : [ "rules", "breadcrumb2", "taxonomy" ],
"ON" : [ "taxonomy_term_update" ],
"DO" : [
{ "entity_create" : {
"USING" : { "type" : "breadcrumb2" },
"PROVIDE" : { "entity_created" : { "breadcrumb" : "Breadcrumb" } }
}
},
{ "data_set" : { "data" : [ "breadcrumb:path" ], "value" : "taxonomy\/term\/[term:tid]" } },
{ "breadcrumb2_append_breadcrumb_trail" : {
"breadcrumb" : [ "breadcrumb" ],
"title" : "[term:vocabulary:name]",
"url" : "[term:vocabulary:machine-name]"
}
},
{ "breadcrumb2_taxonomy_get_parents_all" : {
"USING" : { "taxonomy_term" : [ "term" ] },
"PROVIDE" : { "parent_terms" : { "parent_terms" : "Parent terms" } }
}
},
{ "LOOP" : {
"USING" : { "list" : [ "parent-terms" ] },
"ITEM" : { "parent_term" : "Parent term" },
"DO" : [
{ "breadcrumb2_append_breadcrumb_trail" : {
"breadcrumb" : [ "breadcrumb" ],
"title" : "[parent-term:name]",
"url" : "taxonomy\/term\/[parent-term:tid]"
}
}
]
}
}
]
}
}');
return $items;
}
这里导出的代码,是rules自己特有的规则语言,建议大家熟悉一下,将规则导出成代码的形式并不复杂。通常我们自定义rules规则的时候,也都是先通过配置界面,配置好了以后,将规则导出。导出的代码应该放到breadcrumb2.rules_defaults.inc文件中,通过钩子函数breadcrumb2_default_rules_configuration完成。
清除缓存,回到rules的管理界面,现在规则的状态已经变成了“Overridden”,而且我们现在已经无法删除规则,只能“revert”(恢复回去)。
后来,我还想到了这样的问题,假如用户安装了breadcrumb2模块,他在安装这个模块以前添加的节点怎么办?一种办法就是使用VBO,批处理一下,每个节点重新保存一边,这样就会为其生成对应的面包屑对象;还有一个办法,就是当用户浏览这个节点的时候,如果此时面包屑对象没有创建的情况下,我们为其创建一个。这里的“此时面包屑对象没有创建的情况下”,就是Rules里面的一个需要自定义的条件。
我们来自定义这个条件,在breadcrumb2.rules.inc文件的breadcrumb2_rules_action_info函数上面,插入以下代码:
/**
* Implements hook_rules_condition_info().
*/
function breadcrumb2_rules_condition_info() {
return array(
'breadcrumb2_breadcrumb_exist' => array(
'label' => t('Breadcrumb object already exist.'),
'parameter' => array(
'path' => array('type' => 'text', 'label' => t('Path'), 'wrapped' => FALSE),
),
'group' => t('Breadcrumb2'),
'access callback' => 'breadcrumb2_rules_integration_access',
'base' => 'breadcrumb2_rules_breadcrumb_exist',
),
);
}
这里面,我们使用hook_rules_condition_info,自定义了一个rules条件,注意,这里面的数组结构,和动作里面的数组结构,是一致的,里面的键的含义,前面也都做了介绍。我们向这个条件传递了一个参数path,也就是当前路径。下面我们来看看这个条件的逻辑函数breadcrumb2_rules_breadcrumb_exist,在breadcrumb2_rules_append_breadcrumb_trail的上面,插入以下代码:
/**
* Condition: Breadcrumb object already exist.
*/
function breadcrumb2_rules_breadcrumb_exist($path) {
$breadcrumb = breadcrumb2_load_by_path($path);
if (!empty($breadcrumb)) {
return TRUE;
}
return FALSE;
}
在这个函数里面,我们根据传递过来的路径,尝试加载对应的面包屑,如果加载成功,则返回TRUE,否则返回FALSE。
现在清空缓存。然后克隆一下规则“breadcrumb for node create”,将克隆后的规则命名为“breadcrumb for node view”,添加一个新的事件“Content is viewed”,删除原来的事件“After saving new content”。接着,我们点击添加条件
选中我们刚自定义的条件,点击继续按钮,进入条件的配置页面:
条件的配置表单,也是rules为我们生成的,我们为条件定义的参数path,就对应于这里的输入框。下面的复选框“Negate”,表示取反的意思。在输入框里面输入“node/[node:nid]”,然后选中下面的复选框。保存。
这样当用户浏览一个节点页面时,假如此时该路径下面的面包屑对象不存在,系统就会自动创建一个。我们将规则“breadcrumb for node view”导出来,放到breadcrumb2.rules_defaults.inc文件中:
function breadcrumb2_default_rules_configuration() {
$items = array();
……
$items['rules_breadcrumb_for_node_view'] = entity_import('rules_config', '{ "rules_breadcrumb_for_node_view" : {
"LABEL" : "breadcrumb for node view",
"PLUGIN" : "reaction rule",
"REQUIRES" : [ "rules", "breadcrumb2" ],
"ON" : [ "node_view" ],
"IF" : [
{ "NOT breadcrumb2_breadcrumb_exist" : { "path" : "node\/[node:nid]" } }
],
"DO" : [
{ "entity_create" : {
"USING" : { "type" : "breadcrumb2" },
"PROVIDE" : { "entity_created" : { "breadcrumb" : "Breadcrumb" } }
}
},
{ "data_set" : { "data" : [ "breadcrumb:path" ], "value" : "node\/[node:nid]" } },
{ "breadcrumb2_append_breadcrumb_trail" : {
"breadcrumb" : [ "breadcrumb" ],
"title" : "[node:type-name]s",
"url" : "[node:type]s"
}
},
{ "breadcrumb2_fetch_lightest_term_from_node" : {
"USING" : { "node" : [ "node" ] },
"PROVIDE" : { "lightest_term" : { "lightest_term" : "First term" } }
}
},
{ "breadcrumb2_taxonomy_get_parents_all" : {
"USING" : { "taxonomy_term" : [ "lightest-term" ] },
"PROVIDE" : { "parent_terms" : { "parent_terms" : "Parent terms" } }
}
},
{ "LOOP" : {
"USING" : { "list" : [ "parent-terms" ] },
"ITEM" : { "parent_term" : "Parent term" },
"DO" : [
{ "breadcrumb2_append_breadcrumb_trail" : {
"breadcrumb" : [ "breadcrumb" ],
"title" : "[parent-term:name]",
"url" : "[node:type]s\/category\/[parent-term:tid]"
}
}
]
}
}
]
}
}');
return $items;
}
这个其实是一个很小的问题了,我在阅读rules的相关代码的时候,注意到,很多地方实现了这个钩子。此外部分第三方模块也实现了这个钩子函数,比如views_rules这个模块,它是这样实现的:
/**
* Implements hook_rules_file_info().
*/
function views_rules_rules_file_info() {
return array(
'rules/views_rules.action',
'rules/views_rules.ui',
);
}
这个钩子函数的作用,就是可以将mymodule.rules.inc文件拆分成多个文件,就是说,当加载了.rules.inc文件的时候,就会触发对应的hook_rules_file_info钩子函数,根据钩子函数里面的定义,加载对应的文件。这样一来,mymodule.rules.inc可以只放rules的钩子实现,可以将其函数,比如动作的逻辑处理函数,条件的逻辑处理函数,放到单独的文件中。
Breadcrumb2模块,现在我们只实现了一种方式,按照分类术语生成面包屑,将来生成面包屑的方式可能还会有很多,比如按照菜单、按照book导航的结构。也就是说,可能需要创建更多的动作,和条件。为了将来的扩展性,我们这里也实现了hook_rules_file_info。将我们的实现,放到breadcrumb2_rules_condition_info函数的上面。
/**
* Implements hook_rules_file_info().
*/
function breadcrumb2_rules_file_info() {
return array(
'rules/breadcrumb2.taxonomy',
'rules/breadcrumb2.eval',
);
}
接着,我们先创建文件夹rules,然后在里面创建文件breadcrumb2.taxonomy.inc和breadcrumb2.eval.inc。
在breadcrumb2.taxonomy.inc文件中,我们放置函数:
breadcrumb2_rules_fetch_lightest_term_from_node,
breadcrumb2_rules_taxonomy_get_parents_all,
breadcrumb2_node_get_lightest_term,
breadcrumb2_node_get_terms
在breadcrumb2.eval.inc文件中,我们放置函数:
breadcrumb2_rules_breadcrumb_exist,
breadcrumb2_rules_append_breadcrumb_trail
这些函数都是从breadcrumb2.rules.inc文件中剪切过来的。注意这里的eval,它的含义是“计算”的意思,在rules的核心代码里面,都是这样用的。比如在rules\modules目录下面,比如这里面有:
清除缓存,一切正常。我们有关Rules的集成,也就介绍到这里。
通过本章的学习,我们实现了:
(1),自定义一个rules动作
(2),自定义一个rules条件
(3),导出rules规则到代码里面,也就是通过代码提供默认的rules规则。
导航到admin/config/workflow/rules,这个rules的管理界面,需要启用Rules UI模块,才能访问这个页面。没有启用是看不到的。在这里我们点击“Add new rule”(添加一个新的规则)链接,进入页面admin/config/workflow/rules/reaction/add。在对应的事件里面,我们看到,已经有了有关面包屑实体的事件了。
这些事件包括,删除一个面包屑、创建一个新的面包屑之后、更新一个面包屑、在保存面包屑之前、查看面包屑。这些事件,都是Entity API默认提供的。
我们在这里,为Name(规则名字)输入“breadcrumb for node create”,事件选择“After saving new content”(在保存新内容后)。然后保存。这样进入Rules的配置页面admin/config/workflow/rules/reaction/manage/rules_breadcrumb_for_node_create。
我们现在先不加条件,我们添加一个动作,这里的动作很多:
由于,我们要做的事情是创建一个面包屑实体,Entity API模块提供了这样的一个动作“Create a new entity”(创建一个新的实体)。我们尽量利用已有模块提供的功能。所以这里选择这个动作“创建一个新的实体”。
此时会出来一个新的对话框,让我们选择实体类型,我们这里选择Breadcrumb就可以了:
选中之后,点击继续,此时会展开更多的配置选项:
我们创建了一个新的面包屑实体,这个实体需要使用一个变量存储起来,这里就是配置变量的名字的。图中所示,是默认的变量标签和名字,我们将其分别改为“Breadcrumb”和“breadcrumb”,并保存。
接下来,我们需要为这个面包屑设置属性和字段的值了,我们关心的有两个地方,一个是当前页面的路径,另一个就是面包屑链接,我们使用的link字段。先来看路径的设置。
我们需要再添加一个动作,这一次,我们选择使用“Set a data value”这个动作。选中以后是这样的:
在这个数据选择器里面,我们需要指定要修改哪个数据。通过选择,我们得到“breadcrumb:path”,具体的选择过程,自己用鼠标点点就知道了。Rules的这种选择,非常方便。选中,以后,点击继续按钮。
现在我们需要为“breadcrumb:path”指定一个新的值了,在这里面,我们输入“node/[node:nid]”,其中“[node:nid]”是一个替换符,展开下面的置换模式,就可以找到。我们这里把面包屑的路径设置为了当前被保存的节点的路径。
注意,我们动作添加到这里,如果现在去创建一个新的节点的话,系统就会创建一个面包屑对象。并且为我们指定对应的路径。我开始做到这里的时候,还添加了一个动作“Save entity”(保存实体)。用来将我们在前面创建的面包屑实体保存。但是后来经过反复的测试,突然发现,我们不需要“Save entity”这个动作,系统会自动的帮助我们保存面包屑对象。有兴趣的朋友,读到这里可以做一下对比。
此外,我还做过一个尝试,就是继续添加动作“Set a data value”这个动作,只不过这次要设置的数据,我们改为了“breadcrumb:link”,但是link本身是一个数组,不好直接输入。添加后是这个样子的:
我们使用这个链接本身来设置这个链接,就是绕了一个圈,又回到了原点,什么都没有做。我开始的时候曾经尝试过,比如添加一个变量,将这个变量的类型,设置为“List”的形式,然后想一个一个向里面追加链接,这种办法,尝试了,也不起作用。最终我决定,定义自己的一个动作,专门用来向面包屑对象里面追加链接。
现在我们开始编写代码了,因为现有的功能已经满足不了我们的需求了。在breadcrumb2目录下,创建文件breadcrumb2.rules.inc,向里面添加以下代码:
<?php
/**
* @file
* Rules integration for breadcrumb2.
*
* @addtogroup rules
* @{
*/
/**
* Implements hook_rules_action_info().
*/
function breadcrumb2_rules_action_info() {
$items = array();
$items['breadcrumb2_append_breadcrumb_trail'] = array(
'label' => t('Append breadcrumb trail'),
'group' => t('Breadcrumb2'),
'parameter' => array(
'breadcrumb' => array(
'type' => 'breadcrumb2',
'label' => t('Breadcrumb'),
'description' => t('The breadcrumb object who will be appended.'),
'wrapped' => TRUE,
'save' => FALSE,
),
'title' => array(
'type' => 'text',
'label' => t('title'),
'wrapped' => FALSE,
),
'url' => array(
'type' => 'text',
'label' => t('URL'),
'wrapped' => FALSE,
),
),
'base' => 'breadcrumb2_rules_append_breadcrumb_trail',
'access callback' => 'breadcrumb2_rules_integration_access',
);
return $items;
}
我们在这里实现了hook_rules_action_info,通过这个钩子函数,我们就可以为Rules添加我们自己的自定义动作了。在这个钩子的里面,是有关动作的一个数组。一个hook_rules_action_info钩子函数里面可以添加多个动作,注意这里的$items是复数形式。在这里面,我们定义了一个新的动作breadcrumb2_append_breadcrumb_trail,它里面包含以下键:
'label':动作的标签。
'group':动作所属的组。
'parameter':向这个动作传递的参数。这里面包含三个参数:'breadcrumb'、'title'、'url',这里的'breadcrumb'就是面包屑对象,我们要向它上面追加链接,'title'、'url'则是链接的两个组成部分。
'base':用来定义这个动作的具体逻辑。
'access callback':这个动作的访问控制函数。
这里面还需要注意的是,向动作传递的参数也是一个数组,包含以下键:
'type' :参数的类型,Rules的类型验证会用到它。
'label' :参数的标签。
'description':参数的描述。
'wrapped':对象是否封装。这里的封装,就是是否使用entity_metadata_wrapper给封装一下。我开始的时候,不知道这个参数的含义,后来了解了entity_metadata_wrapper,才明白了这里的用法。
'save' :是否保存当前对象。
Rules里面有一个很重要的概念,就是数据类型,可能这个数据类型,是由Entity API提供的,因为两个模块都是Fago开发的。所以结合的非常好。我们来看一下Rules核心提供的数据类型:date、duration、integer、decimal、text、token、boolean、uri、list、entity、struct。
我们前面用到的'breadcrumb2',就属于这里的entity,这个类型由Entity API帮助我们提供了。当我们添加动作“Add a variable”(添加一个变量)的时候,就会看到所有可用的类型了:
对于实体,应该是该实体类型的机读名字。我们在做Rules开发的时候,会经常用到这些数据类型。如果想要实现自己的数据类型的话,可以使用hook_rules_data_info来定义。
接着,向breadcrumb2.rules.inc文件中追加以下代码:
/**
* Breadcrumb2 rules integration access callback.
*/
function breadcrumb2_rules_integration_access($type, $name) {
return TRUE;
}
我们这里直接返回了TRUE,在更复杂的情况下,这里需要编写逻辑代码的。不过我们这里的动作,只要用户设置了,就不需要访问控制检查,否则的话,会出现各种问题。是否可以将这个函数省去,直接返回TRUE。这个我没有测试过。这里面的代码结构,来源于OG模块的Rules集成,因为Drupal7下面,OG2.x里面的Rules集成,也是老葛从1.x中迁移过来的,当时的迁移还是费了很大的功夫的。
我们通过'base'键,指定了函数'breadcrumb2_rules_append_breadcrumb_trail',我们来看一下具体的代码,向breadcrumb2.rules.inc文件中追加以下代码:
/**
* Action: Append breadcrumb trail to breadcrumb object.
*/
function breadcrumb2_rules_append_breadcrumb_trail(EntityDrupalWrapper $breadcrumb, $title, $url) {
if(empty($title) || empty($url)){
return;
}
$link = array(
'title' => $title,
'url' => $url,
);
$links = $breadcrumb->link->value();
$links[] = $link;
$breadcrumb->link->set($links);
//drupal_set_message('title:'.$title.'url:'.$url);
}
这里面的逻辑比较简单,就是将$title, $url合成为$link数组,追加到$breadcrumb->link上去。开始的时候,我是没有编写上面的if语句的。后来出于更周全的考虑,当$title或 $url为空时,我们直接返回了,就不再追加这个链接了。我们这里假定,一个链接,必须要有一个$title和一个 $url。
现在我们回到,刚才创建的规则“breadcrumb for node create”的编辑页面,此时添加一个新的动作,在动作的可选项中,并没有我们新增的动作。这个时候我们需要清除缓存。当做Rules的开发时,有时候,缓存需要多清除两次。好了现在就可以添加我们的动作了。
我们选中动作“Append breadcrumb trail”(追加面包屑链接),这里使用trail这个英文单词,是因为另外的一个面包屑模块也这么用。选中后,点击继续按钮,就可以配置我们的这个动作了。
只需要我们指定参数,Rules就会自动的为我们创建好对应的配置表单,比较智能吧。由于我们这里只有breadcrumb这个变量是面包屑对象,所以第一个参数,系统为我们自动设置了。我们在TITLE里面输入“[node:type-name]s”,在URL里面输入“[node:type]s”,注意这里的[node:type-name]和[node:type]都是替换符号,我以前经常把它们称作令牌(token)。现在创建一个page页面,就会自动创建一个“首页 >> Pages”这样的面包屑了。
一个新的问题出现了,如果当我们更新一个节点时,也使用rules更新我们的面包屑,此时由于我们通过rules设置的面包屑并不带有bid,所以默认只会新建。而实际上,一个路径下面,应该只有一个面包屑的,也就是说path这个属性是唯一的。但是在使用rules创建面包屑的时候,我们没有检查这种唯一性。为了避免,创建很多同一路径的面包屑,我们为面包屑对象实现自己的save方法。如下所示:
/**
* The class used for breadcrumb entities
*/
class Breadcrumb extends Entity {
...
protected function defaultUri() {
return array('path' => 'breadcrumb/' . $this->bid);
}
public function save() {
if (empty($this->bid) && (!empty($this->path))) {
$existing_breadcrumb = breadcrumb2_load_by_path($this->path);
if(!empty($existing_breadcrumb)){
$this->bid = $existing_breadcrumb->bid;
$this->is_new = FALSE;
}
}
parent::save();
}
}
粗体部分,就是我们这里新增的,在这里,我们在bid为空的情况下,尝试根据path加载面包屑对象,如果加载了面包屑对象,我们使用该面包屑的bid作为我们的bid,并将is_new设置为FALSE,表示这里是更新,而不是新建。
我们前面实现了一个动作,通过追加面包屑链接,我们可以构建完整的面包屑对象了。做到这里,就基本上可以满足大部分实际需要了,只不过配置的过程比较麻烦一些,可能需要为每个内容类型分别配置面包屑。而我们的目标是建立基于分类的面包屑,而且构建起来,通用一些,这样模块的易用性就更高一些。所以我们这里借鉴一下Custom breadcrumbs里面的部分代码,从一个节点上面智能的获取第一个分类术语,然后以它为基础,构建面包屑。
首先,向breadcrumb2_rules_action_info追加一下代码:
/**
* Implements hook_rules_action_info().
*/
function breadcrumb2_rules_action_info() {
$items = array();
…..
$items['breadcrumb2_fetch_lightest_term_from_node'] = array(
'label' => t('Fetch lightest term from node'),
'group' => t('Breadcrumb2'),
'parameter' => array(
'node' => array(
'type' => 'node',
'label' => t('Node'),
'wrapped' => TRUE,
'save' => FALSE,
),
),
'base' => 'breadcrumb2_rules_fetch_lightest_term_from_node',
'access callback' => 'breadcrumb2_rules_integration_access',
'provides' => array(
'lightest_term' => array(
'type' => 'taxonomy_term',
'label' => t('First term'),
),
),
);
return $items;
}
这里,我们定义了一个新的动作,从节点上面获取第一个分类术语,参数只有一个node,动作的逻辑处理函数为breadcrumb2_rules_fetch_lightest_term_from_node;访问控制回调函数还是原来定义的。注意,这里面,我们使用了'provides',用来向rules提供一个新的可用的变量,这个变量的名字为lightest_term,数据类型为taxonomy_term,标签为'First term'(第一个分类术语)。如果动作需要返回一个变量的话,就需要用到这里的'provides',我这是第一次用这个参数。
接下来,我们看一下breadcrumb2_rules_fetch_lightest_term_from_node:
/**
* Action: Fetch lightest term from node.
*/
function breadcrumb2_rules_fetch_lightest_term_from_node(EntityDrupalWrapper $node) {
$term = breadcrumb2_node_get_lightest_term($node->value());
if(!empty($term)){
$term = taxonomy_term_load($term->tid);
}
return array('lightest_term' => $term);
}
这个动作里面的逻辑也非常简单,当然我们这里面,把重活都交给了帮助函数breadcrumb2_node_get_lightest_term。这个函数怎么来的呢?是从custom_breadcrumbs模块中复制过来的,我们只是把函数名的前缀,改成了我们自己的而已。
/********************Helper Function *************************************/
/**
* Returns the lightest term for a given node.
* Copy from custom_breadcrumbs_taxonomy module.
*
* If the term has parents, then the lightest parent's weight is used for the
* term weight. And if the parent has multiple child terms at different depths,
* the deepest child term will be returned. If the child terms have the same
* depth, then the lightest child term is returned.
*
* @param $node
* The node object.
*
* @return
* The taxonomy term object.
*/
function breadcrumb2_node_get_lightest_term($node) {
$terms = breadcrumb2_node_get_terms($node);
if (!empty($terms)) {
if (count($terms) > 1) {
foreach ($terms as $term) {
// Only consider terms in the lightest vocabulary.
if (!isset($vid)) {
$vid = $term->vid;
}
elseif ($term->vid != $vid) {
continue;
}
// If the term has parents, the weight of the term is the weight of the lightest parent.
$parents = taxonomy_get_parents_all($term->tid);
$depth = count($parents);
if ($depth > 0) {
$parent = array_pop($parents);
$weight = $parent->weight;
}
else {
$weight = $term->weight;
}
if ((isset($lweight) && ($weight < $lweight)) || !isset($lweight)) {
$lterm = $term;
$lweight = $weight;
$ldepth = $depth;
}
elseif (isset($lweight) && ($weight == $lweight)) {
// If the node has multiple child terms with the same parent, choose the child with the greatest depth.
if ($depth > $ldepth) {
$lterm = $term;
$ldepth = $depth;
}
elseif ($depth == $ldepth) {
// If the terms have the same depth, pick the term with the lightest weight.
$lterm = ($lterm->weight < $term->weight) ? $lterm : $term;
}
}
}
return $lterm;
}
else {
return array_pop($terms);
}
}
}
/**
* Copy from custom_breadcrumbs_taxonomy module.
* Finds all terms associated with a node.
* This is a D7 Replacement for Drupal 6 taxonomy_node_get_terms.
*/
function breadcrumb2_node_get_terms($node, $key = 'tid') {
static $terms;
if (isset($node->nid) && isset($node->vid)) {
if (!isset($terms[$node->vid][$key])) {
$query = db_select('taxonomy_index', 'r');
$t_alias = $query->join('taxonomy_term_data', 't', 'r.tid = t.tid');
$v_alias = $query->join('taxonomy_vocabulary', 'v', 't.vid = v.vid');
$query->fields( $t_alias );
$query->condition("r.nid", $node->nid);
$query->orderBy('v.weight');
$query->orderBy('t.weight');
$query->orderBy('t.name');
$result = $query->execute();
$terms[$node->vid][$key] = array();
foreach ($result as $term) {
$terms[$node->vid][$key][$term->$key] = $term;
}
}
return $terms[$node->vid][$key];
}
return array();
}
breadcrumb2_node_get_terms负责获取一个节点上面的所有分类术语,breadcrumb2_node_get_lightest_term从这些分类术语当中,按照特定的规则,找出第一个分类术语。在Drupal6里面,有个函数taxonomy_node_get_terms,可以用来获取节点上面的所有术语,但是这个函数在Drupal7里面没有了。另外需要注意的是,我们是从表taxonomy_index中获取数据的,在这个数据库表中,维护了节点分类术语之间的对应关系。我以前使用custom breadcrumbs模块的时候,修改过它们这里的代码,所以就把它们偷过来了。用孔乙己的话说,对程序员来说,这叫借,不叫偷。
好了,现在清除缓存,添加动作,就可以看到这个动作了。选中这个动作,进入配置页面,系统智能的为我们的参数选择了node:
其它都不用修改,注意这里提供的变量,与前面代码的对应关系。保存这里的设置,我们现在就可以使用变量“First term (lightest_term)”了。现在,我们就可以再追加一个面包屑链接了。这是追加的链接:
现在,我们创建一个词汇表(product_category),并添加以下分类术语:
接着创建一个内容类型product,并为其添加一个分类字段,引用的词汇表就是刚刚创建的。接着,我们创建一个产品,分类术语选择A11,这个时候系统会总动的帮助我们创建一个面包屑对象。面包屑的显示效果:
如果这里,能够把A11 的父术语A和A1也都显示出来,效果就完美了。注意,现在面包屑链接中的路径,还没有在内部定义,根据实际需要可以调整。
如果你对Rules比较熟悉的话,现在我们就可以点击“Add loop”这个链接,添加一个循环,我们根据lightest_term,就可以自动的获取到它所有的父术语,Rules核心就自带了这个功能。
我们这里把“Current list item”改为“Parent term”, 把“list_item”改为“parent_term”。上面的“lightest-term:parents-all”是我自己选出来的,也是rules自己提供了的:
保存以后,我们可以把刚才追加面包屑链接的动作,拖到循环的里面,也可以将其删除,在循环里面,单独的再创建一个同样的动作。我们这里将它拖了过来:
现在编辑这个动作。这是修改后的样子:
保存。现在再创建一个测试的产品节点,分类术语还选择A11。生成的面包屑是这个样子的:
怎么说呢,就差那么一点点了,分类术语的顺序在这里弄反了。我们只需要将“lightest-term:parents-all”的顺序反过来,就可以了,不过遗憾的是,Rules核心并没有提供这样的功能。好吧,我们就自己创建一个了。我们现在添加一个新的动作:
/**
* Implements hook_rules_action_info().
*/
function breadcrumb2_rules_action_info() {
$items = array();
……
$items['breadcrumb2_taxonomy_get_parents_all'] = array(
'label' => t('Taxonomy get parents all'),
'group' => t('Breadcrumb2'),
'parameter' => array(
'taxonomy_term' => array(
'type' => 'taxonomy_term',
'label' => t('Taxonomy term'),
'wrapped' => TRUE,
'save' => FALSE,
),
),
'base' => 'breadcrumb2_rules_taxonomy_get_parents_all',
'access callback' => 'breadcrumb2_rules_integration_access',
'provides' => array(
'parent_terms' => array(
'type' => 'list<taxonomy_term>',
'label' => t('Parent terms'),
),
),
);
return $items;
}
中间的代码省略了,粗体为新增的。注意,这里面,我们返回的变量是一个数组,这里使用了list,类型设置为了'list<taxonomy_term>',我根据经验,猜测应该这样写,后来经过测试,写对了。
我们接着定义,动作的逻辑处理函数:
/**
* Action: Taxonomy get parents all.
*/
function breadcrumb2_rules_taxonomy_get_parents_all(EntityDrupalWrapper $taxonomy_term) {
$parent_terms = taxonomy_get_parents_all($taxonomy_term->tid->value());
$parent_terms = array_reverse($parent_terms);
$return = array();
foreach($parent_terms as $parent_term){
$return[$parent_term->tid] = taxonomy_term_load($parent_term->tid);
}
//$return = array();
return array('parent_terms' => $return);
}
我们使用taxonomy_get_parents_all获取术语的所有父术语,包括术语本身,然后使用array_reverse将整个数组反向排列。注意这里面,由于taxonomy_get_parents_all返回的分类术语,不是完整的分类术语对象,所以这里又使用了taxonomy_term_load重新加载了一遍分类术语。开始的时候,代码里面是没有这个循环的,后台在测试的过程中,发现分类术语对象本身有问题。就添加了这个循环,并使用taxonomy_term_load明确加载分类术语对象。如果你不选,可以把这个循环代码注释掉,测试一下。
现在,清除缓存,添加动作“Taxonomy get parents all”,这里采用默认配置就可以了,系统会自动的为我们选择“lightest-term”。保存后,将我们的动作,拖放到loop的上面:
接着点击“保存变更”按钮,现在我们就可以修改这个循环,将List的数据选择,从“lightest-term:parents-all”改为“parent-terms”了。修改后,保存这个循环。
现在再创建一个产品节点,继续选择A11,生成的面包屑为:
这就是我们想要的。