Think in Drupal 第4集

第1章 实体(Entity)API 5
实体 6
Bundle(包) 7
不是所有的东西都是实体 7
Drupal7核心实体API介绍 8
Entity API模块 9
定义自己的实体类型 15
EntityFieldQuery 46
总结 53
第2章 Views 集成 54
Field Validation集成 54
Views的集成 58
Field Collection Views代码分析 75
总结 81
第3章 Rules的集成 82
Rules中的基本术语 82
Entity API提供了默认的Rules集成 83
定义自己的动作 86
将rules规则导出到代码里面 101
为Rules自定义条件 107
实现hook_rules_file_info 110
总结 111
第4章 集成上下文链接 112
接管theme_breadcrumb函数 112
添加上下文链接 115
实现预处理函数 121
通过hook_contextual_links_view_alter修改上下文链接 122
Breadcrumb2已有问题的修正 126
总结 131
第5章 Drupal主题系统探索 132
无法在当前主题下面覆写breadcrumb.tpl.php 132
添加模板建议 138
template_preprocess_breadcrumb? 139
为模板新增变量$classes 140
drupal_static 143
不修改Drupal核心代码 146
第6章 Drupal核心模板文件 159
html.tpl.php 159
page.tpl.php 161
region.tpl.php 165
node.tpl.php 166
block.tpl.php 168
field.tpl.php 170
常见问题 171
第7章 Batch API(批处理) 172
使用Batch API批量修改各种会员价格 172
批量删除模块batchdelete 180
VBO 185
第8章 最佳开发实践 189
编码规范 189
编写安全的代码 200
维护一个Drupal模块 211
第9章 Field Validation的历程 218
最初的解决办法 218
Field validation的Alpha1版 221
Field Validation Beta1版的改进 243
从Beta2到Beta6 245
第10章 Ctools插件开发 250
Ctools导入导出API 250
Ctools 插件开发 261
心得体会 276
后记 282

Drupal版本:

第8章 最佳开发实践

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

在本章中,我们主要探讨Drupal开发中的一些最佳实践,遵守这些实践,能够帮助我们提高代码的质量,提高代码的安全系数,同时有利于开发者之间相互交流。我们首先学习Drupal的编码规范,以及为模块创建文档;接着学习如何编写安全的代码;介绍了常用的版本控制,并展示了如何在drupal.org上维护一个模块。


Drupal版本:

10 字符串连接

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

  在点号两边分别使用空格:

<?php 
  $string = 'Foo' . $bar;
  $string = $bar . 'foo';
  $string = bar() . 'foo';
  $string = 'foo' . 'bar';
?>

 


Drupal版本:

11 常量

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

PHP常量应该全部大写,并使用下划线来分隔字词:

 

/**

/**

 * Node is not published.

 */

define('NODE_NOT_PUBLISHED', 0);

常量的名字也应该使用它们的模块名作为前缀,这样就可以避免常量之间的命名冲突。例如,上面的NODE_NOT_PUBLISHED,此时就没有使用NOT_PUBLISHED


Drupal版本:

12 全局变量

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

在你自己的模块中,最好不要定义全局变量。当然我们可以使用Drupal自带的全局变量,比如$user, $language.比如(摘自node.module中的node_object_prepare函数)

global $user;

$node->uid = $user->uid;

$node->created = REQUEST_TIME;


Drupal版本:

14 PHP注释

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

Drupal实现了大多数的Doxygen注释规范。所有的文档必须使用下面的语法:

 

/**

 * Documentation here.

 */

 

除了第一行以外,其它各行在星号(*)前面必须要有一个空格。

 

注意 Doxygen是一个能够友好支持PHP的文档生成器。它从代码中提取PHP注释并生成适合用户阅读的文档。更多信息,可参看http://www.doxygen.org

 

当为一个函数添加说明文档时,对应文档必须紧挨着放在函数前,中间不能存在空行。

 

Drupal能够理解下面所列的Doxygen结构;我们接下来会对其中的大多数作简单的介绍,不过有关它们的更多信息,请参看Doxygen的官方站点。

 

• @mainpage

• @file

• @defgroup

• @ingroup

• @addtogroup (as a synonym of @ingroup)

• @param

• @return

• @link

• @see

• @{

• @}

 

遵循这些标准的好处是,你可以使用第3方的API模块,为你的模块自动生成文档。API模块实现了Doxygen文档生成器规范的一个子集,并专门针对Drupal代码的文档生成进行了优化。访问http://api.drupal.org,你就可以看到这个模块的一个实例,除了官方的这个站点以外,还有http://drupalcontrib.org/,里面包含了常用第三方模块的文档。而关于API模块的更多信息,可参看http://drupal.org/project/api


Drupal版本:

15 文档例子

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

我们以node.module文件为例,来学习一下模块文档的编写。我们按照从上到下的顺序,对不同的类型的文档做出对应的解释。

在模块的第3行(在<?php开始标签下面),包含了一段文档,它用来介绍这个模块是做什么的。文档的内容如下所示:

 

/**

 * @file

 * The core that allows content to be submitted to the site. Modules and

 * scripts may programmatically submit nodes using the usual form API pattern.

 */

     上面的意思是说,这个模块是用来向站点提交内容的。通过使用表单API的方式,第三方模块也可以通过程序的方式提交节点。

    在node.pages.in文件的开头,也存在类似的文档:

/**

 * @file

 * Page callbacks for adding, editing, deleting, and revisions management for content.

 */

    上面的意思是说,node.pages.in文件,里面包含了各种页面回调函数,比如节点添加、编辑、删除,以及修订本管理对应的页面。

    通过上面的两个例子可以看出,@file指令就是对当前文件的用途做出说明。


Drupal版本:

16 为常量编写文档

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

PHP常量应该大写,可以使用下划线分隔单词。当定义PHP常量时,最好能够解释一下它们是用来做什么的,如下面的代码片段所展示的这样:

 

/**

 * Node is not published.

 */

define('NODE_NOT_PUBLISHED', 0);

    这里定义了一个NODE_NOT_PUBLISHED常量,值为0。上面的注释用来说明常量的含义:节点未发表。


Drupal版本:

17 为钩子实现编写文档

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

当一个函数是一个钩子实现时,此时不需要为钩子编写文档。简单的说明一下实现了哪个钩子就可以了,例如:

 

/**

 * Implements hook_help().

 */

function node_help($path, $arg) {

   …

}

以及:

/**

 * Implements hook_theme().

 */

function node_theme() {

   …

}

    换成中文的形式,我通常这样写:

/**

 * 实现钩子 hook_help().


Drupal版本:

18 为函数编写文档

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

函数文档应该使用下面的语法:

 

/**

 * Extract the type name.

 *

 * @param $node

 *   Either a string or object, containing the node type information.

 *

 * @return

 *   Node type of the passed-in data.

 */

function _node_extract_type($node) {

  return is_object($node) ? $node->type : $node;

}

    在文档的开头,我们首先描述一下这个函数是用来做什么的。如果你是用英语写文档的话,注意这里使用祈使语句。

 

    这里我们用到了两个文档指令, @param用来给出函数的参数,一个函数文档可以包含多个@param@return用来表示函数的返回值,通常只有一个。

   此外,我们还会用到其它一些文档指令,比如@ingroup

/**

 * Saves a node.

 *

 * @ingroup actions

 */

function node_save_action($node) {

  …

}

    它可以将一组函数关联在一起。node.module中有多个函数用到了@ingroup actions

/**

 * Sets the promote property of a node to 0.

 *

 * @ingroup actions

 */

function node_unpromote_action($node, $context = array()) {

  …

}

    在http://api.drupal.org/api/drupal/includes--actions.inc/group/actions/7里面,我们可以查看actions组所包含的所有函数。

    node.module里面所包含的文档指令,并不止这些,我们在这里就不逐一加以说明了。

 

    对于页面回调函数,我们应该注明它是页面回调,并给出函数的简单说明。

 

/**

 * Menu callback -- ask for confirmation of node deletion

 */

function node_delete_confirm($form, &$form_state, $node) {

  …

}


Drupal版本:

19 通过程序来检查你的代码

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

有两种主要的方式,可用来检查你的编码风格是否符合Drupal的编码标准:一种方式是使用一个Perl脚本,Drupal根目录下的scripts文件夹下面,有个名为code-style.pl脚本,它可用来检查你的Drupal代码;另一种方式是使用一个第3方模块coder

Coder模块的项目地址为http://drupal.org/project/coder,使用这个模块,我们便可以评估一个模块的代码。它有助于让我们熟悉Drupal的编码规范,提高我们的开发效率,帮我们节省不少时间。

    Coder模块的开发者提供了一个在线版本,http://upgrade.boombatower.com,通过在线提交你的模块,它就可以帮你完成代码的评估,Drupal6模块到Drupal7的初步升级。最初这个站点是免费的,现在改为了收费。

 

下载最新的coder版本,同时下载它依赖的grammar_parsergrammar_parser_liblibraries的最新开发版,将它们放到sites/all/modules/standard/下,接着在admin/modules页面启用CoderCoder ReviewCoder Upgrade模块,这和安装其它模块一样。

    现在导航到管理 » 配置 » 开发 » Coder”,点击“Run reviews”按钮,接着访问默认标签,我们就可以看到所有模块的代码评估了。以Field Validation模块为例,如图所示:

图片1.png 

    它会列出模块代码中的所有不符合规范的地方。

 

coder模块的scripts\coder_format文件夹下,提供了一个coder_format.php脚本。使用这个脚本,可以通过命令行的方式完成代码评估,这个脚本还会帮助我们修正代码格式错误。这个文件夹下面,同时还自带了coder_format.reg文件,方便在windows下面安装。


Drupal版本:

20 编写安全的代码

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

曾经有位学员打电话问我,说他的Drupal站点挂机了,他说他的站点可能受到了黑客攻击。问我能不能帮他查查,我告诉他,Drupal很稳定,通常不会出现这种情况。后来,原因出来了,原来是他网站所在服务器上面的另一套程序被人攻击了,导致了他的站点也受到了牵连。是的,这不是Drupal的错。我维护过很多Drupal站点,基本上没有见过被黑掉的Drupal站点。我前段时间,帮助我的一位大学老师维护他的个人站点,ASP的,小团队写的,里面挂满了木马,我不得不逐个文件夹进行核查。

是的,Drupal核心很安全了,因为Drupal核心程序得到了上百万个站点的检验了。但是这并不意味着说,你新上马的这个Drupal站点也是安全的。因为你的站点上面,安装了各种各样的第三方模块,以及自己定制的模块,这些代码与Drupal核心相比,存在安全漏洞的可能性更高一点。如果我们在编写自己的模块时,能够遵守代码安全相关的最佳实践,优先采用Drupal自身提供的API,我们就可以避免常见的安全漏洞。安全问题有很多,比较常见的有,跨站点脚本攻击(XSS)、CSRF、 SQL注入攻击、信息泄露、哈希长度扩展攻击。我们接下来介绍,Drupal提供了哪些技术,能够帮助我们避免这些安全漏洞。


Drupal版本:

21 跨站点脚本攻击(XSS)

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

跨站点脚本(XSS)指利用网站漏洞从用户那里恶意盗取信息。用户在浏览网站、使用即时通讯软件、甚至在阅读电子邮件时,通常会点击其中的链接。攻击者通过在链接中插入恶意代码,就能够盗取用户信息。攻击者通常会用十六进制(或其他编码方式)将链接编码,以免用户怀疑它的合法性。网站在接收到包含恶意代码的请求之后会产成一个包含恶意代码的页面,而这个页面看起来就像是那个网站应当生成的合法页面一样。许多流行的留言本和论坛程序允许用户发表包含HTMLjavascript的帖子。假设用户甲发表了一篇包含恶意脚本的帖子,那么用户乙在浏览这篇帖子时,恶意脚本就会执行,盗取用户乙的session信息。

能使用XSSHTML标签有<script>,<object>,<applet>,<embed>,<form>.用于XSS的常见语言有JavaScript,VBScript,HTML,Perl,C++,ActiveX,Flash.

 

现在的网站,都在强调与用户之间的互动,网站的很多内容,信息,是由普通用户生成的。对于大多数的用户来说,他们会按照我们的预期填写数据,然后提交给我们。但是也有很小的那么一部分,他们不按照规则形式。我们在方便前者的同时,不得不对后者做出防备。

 

假定你允许用户向你的网站输入HTML,期望他们这么输入

<em>大家好!</em> 此处省略10000...

 

但是他们输入了

<script src=”http://evil.example.com/xss.js”></script>

 

    当用户访问这个页面时,就有可能触发这个恶意的JS代码。对于受害者来说,会造成多种后果,可能会造成帐户信息被盗,访问各种虚假广告链,Cookie信息失窃等等。

    对于XSS,最简单的办法就是过滤用户输入,屏蔽用户输入的部分标签。我们来看看Drupal中,我们怎么解决这个问题。

 

 

Drupal提供了多种输入格式,用来过滤内容输出。以节点为例,用户创建了一篇文章,Drupal将文章正文原封不动的保存到数据库中,当生成web页面输出时,就会使用输入格式包含的过滤器对文本进行过滤,这样就可以很好的过滤恶意的HTML代码。为不同角色的用户,选用不同的输入格式。这是我们在构建一个站点时,从安全方面应该考虑的。

 

Drupal也提供了多个API函数,用来过滤用户的输入。我们在开发模块的时候,需要处理用户输入时,应该考虑使用这些函数,比如check_plaincheck_markup check_url filter_xssfilter_xss_admin。对于纯文本来说,我们使用check_plaincheck_url;对于富文本来说,我们使用check_markupfilter_xssfilter_xss_admin


Drupal版本:

22 使用check_plain()

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

check_plain做两件事情,一个是把HTML标签转义,二是检查字符串是不是UTF-8的。这两点都能帮我们防止XSS攻击。

 

我们举个例子,来看这段代码:

 

$output ="";

$user_input = '<script src=”http://evil.example.com/xss.js”></script>';

$output = check_plain($user_input);

return  $output;

 

用户的输入就是前面我们所给的一段恶意脚本,返回的输出已经经过了转义:

图片1.png 

我们看到此时的<script>标签已经是灰色的了,而不是蓝色的了。我们可以在页面的正文中看到对应的文本,此时的输入已经没有任何危害了。对应的页面内容如下所示:

图片2.png 

此时,如果我们把这个html页面保存起来,然后再用写字板打开对应的htm文件,我们会看到真实的输出:

<script src=”http://evil.example.com/xss.js”></script>

 

    此时我们已经把“<”作了转义,我们通过浏览器看到的则是用户的实际输入。

 

    如果我们的代码这样写:

$output ="";

$user_input = '<script src=”http://evil.example.com/xss.js”></script>';

//$output = check_plain($user_input);

$output = $user_input;

return  $output;

    我们通过firebug看到的输出则是:

图片3.png 

    对应的页面则没有内容输出。

 

   Drupal自带的一些函数,内置了对check_plain的支持,比如:

t():占位符比如'%name''@name'将被处理成为纯文本,如果想在这里使用html的,则使用'!name'

l():链接的标题,处理成为纯文本。如果想使用html的话,需要将html参数设置为TRUE

菜单项的标题,和面包屑的标题,自动使用check_plain

drupal_placeholder($text):占位符文本内置支持check_plain

区块描述内置支持check_plain

theme_username输出用户名时,内置支持check_plain

表单元素的#default_value属性自动支持check_plain

 

    如果Drupal已经内置支持了check_plain,我们就不需要重复使用它了。比如:

$form['good'] = array(
  '#type' => 'textfield',
  '#default_value' => $user_input,
); 

这些写就可以了。如果加上check_plain,就多余了。 

$form['bad'] = array(
  '#type' => 'textfield',
  '#default_value' => check_plain($user_input), //这里使用check_plain就多余了。
); 

 

    有些地方,则需要自己明确的调用check_plain。比如页面标题,区块标题,Watchdog消息,表单元素的#description#title属性、#value属性、#options属性。

比如:

drupal_set_title($user_input); // 存在安全隐患, drupal_set_title(check_plain($user_input));  // 正确。

//正确写法

$form['good'] = array(

  '#type' => 'textfield',

  '#title' => check_plain($user_input),

);

//错误写法

$form['bad'] = array(

  '#type' => 'textfield',

  '#title' => $user_input,

);


Drupal版本:

23 使用filter_xss()

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

函数filter_xss()的签名如下所示:

filter_xss($string, $allowed_tags = array('a', 'em', 'strong', 'cite', 'code',

'ul', 'ol', 'li', 'dl', 'dt', 'dd'))

filter_xss($string, $allowed_tags = array('a', 'em', 'strong', 'cite', 'blockquote', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd'))

从这个函数的名字,我们就可以看出,它是专门用来防止XSS攻击的。在Drupal7中,这个函数是放在common.inc文件中的,在以前的Drupal6中则放在了filter.module文件中。

 

这个函数做四件事情:

1.删除可能欺骗浏览器的字符、结构体。

 

2. 确保所有HTML实体格式良好。

 

3. 确保HTML标签和标签属性的形式良好

 

4. 它确保没有HTML标签包含带有未允许协议的URL

 

5. 它确保所有的HTML标签,都不包含不允许的协议。比如javascript:。而允许的协议有'ftp', 'http', 'https', 'irc', 'mailto', 'news', 'nntp', 'rtsp', 'sftp', 'ssh', 'tel', 'telnet', 'webcal'。你可以通过在settings.php中设置filter_allowed_protocols变量来修改这一列表。filter_xss()过滤不允许协议,是通过filter_xss_bad_protocol drupal_strip_dangerous_protocols函数实现的。

 


下面filter_xss()例子很多,modules/field/modules/list/list.module,文件中的list_field_formatter_view函数中,为了避免潜在的安全隐患,使用了field_filter_xss,代码如下

 

case 'list_key':

foreach ($items as $delta => $item) {

$element[$delta] = array('#markup' => field_filter_xss($item['value']));

}

break;

 

    这里的field_filter_xssfilter_xss作了简单的封装,设置了自己允许的标签:

function field_filter_xss($string) {

  return filter_xss($string, _field_filter_xss_allowed_tags());

}

 

对于与后台管理界面相关的内容过滤Drupal提供了函数filter_xss_admin()。它对filter_xss()做了简单的封装,只不过使用的可用标签更多一点,除了<script> 和 <style>标签以外,它包含了所有的可用标签。这是因为管理员比普通用户更加可信一点。使用它的一个例子是在主题中展示站点的宗旨(mission):

if (drupal_is_front_page()) {

$mission = filter_xss_admin(theme_get_setting('mission'));

}


Drupal版本:

24 使用check_url和valid_url

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

有时候我们需要处理用户提交的URL。我们首先需要检查用户提供的值是个有效的URL。此时我们可以使用函数check_url(),这个函数首先过滤掉恶意的协议,接着使用check_plain()来处理URL,代码如下

 

function check_url($uri) {

  return check_plain(drupal_strip_dangerous_protocols($uri));

}

此时由于我们使用了check_plain,所以返回的值不能用作drupal_attributes()里面的属性值。如果我们需要传递给drupal_attributes,那么可以直接调用drupal_strip_dangerous_protocols

如果我们只是想验证一下URL的有效性,此时可以使用valid_url()。它是按照RFC 3986标准来验证URL的。如果URL通过了测试,那么它返回TRUE,否则返回FALSE。需要注意的是,通过语法检查的URL也不一定安全。

如果你想输出一个URL时,可以使用函数url(),它在处理url时调用了drupal_encode_path(),从而实现对Drupal路径的编码。drupal_encode_path()PHPrawurlencode()函数做了封装。为了好看,没有对斜杠进行转义。

function drupal_encode_path($path) {

  return str_replace('%2F', '/', rawurlencode($path));

}

Drupal就是这样,很多时候不使用PHP自身的API函数,而是根据自己的需要,在上面做一些封装处理。这种情况很多,除了上面的drupal_encode_path,常见的还有drupal_substr,对substr做了封装,drupal_strlenstrlen做了封装。


Drupal版本:

25 SQL注入攻击

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

 SQL注入攻击是黑客对数据库进行攻击的常用手段之一。随着B/S模式应用开发的发展,使用这种模式编写应用程序的程序员也越来越多。黑客通过把SQL命令插入到Web表单提交的查询字符串,最终达到欺骗服务器并执行恶意的SQL命令

/**

 * Implements hook_menu().

 */

function sql_injection_menu() {

  $items = array();

$items['sql/injection'] = array(

    'title' => 'SQL注入实例',

    'description' => 'SQL注入实例.',

    'page callback' => 'sql_injection_page_callback',

    'access callback' => TRUE,

  );

return $items;

}

 

/**

 * 页面回调函数.

 */

  $result = db_query("SELECT n.nid, n.title FROM

function sql_injection_page_callback(){

  //假定这个type是用户通过表单输入的,为了方便,这里直接赋值了。

  $type = 'page';

//$output ="123";

 {node} n WHERE n.type = '$type'");

  $items = array();

foreach($result as $record){   

  $items[] = l($record->title, "node/{$record->nid}");

}

return theme('item_list', array('items' => $items));

}

访问http://localhost/thinkindrupal/sql/injection,一切正常。我们得到想要的基本页面列表,如图所示。

图片1.png 

图 简单的page页面标题列表

 

 

 

    尽管这段程序也能正常工作,但是却存在安全隐患。将变量$type直接放到SQL中,这样用户就有可能发动SQL注入攻击。让我们将$type设置为:

$type = "page' UNION SELECT s.sid, s.sid FROM {sessions} s WHERE s.uid = 1 #";

    假定这就是用户的输入,此时我们将得到:

 

图片2.png 

图 在db_query()中不使用占位符引起的SQL注入

 

    看到什么了?像密码一样的信息,是的,我们把用户1的会话信息也显示了出来。出问题了把。当然,这危害还比较小,黑客有可能会使用SQL注入,获取更多的信息。比如注册用户的各种个人信息。

 

我们来对这段代码进行改进,其实使用Drupal7中的占位符,就可以避免SQL注入。代码如下:

$result = db_query("SELECT n.nid, n.title FROM {node} n WHERE n.type = :type",array(':type' => $type));

如果用户输入的是:

$type = "page' UNION SELECT s.sid, s.sid FROM {sessions} s WHERE s.uid = 1 #";

此时结果为空。我们已经有效的防止了SQL注入攻击。

我们也可以采用动态查询的形式,代码如下所示:

$result = db_select('node', 'n')

          ->fields('n',array('nid','title'))

            ->condition('n.type',$type)  

            ->execute();

这也同样可以避免SQL注入攻击。


Drupal版本:

26 跨站点请求伪造(CSRF)

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

一个网站用户B可能正在浏览聊天论坛,而同时另一个用户A也在此论坛中,并且后者刚刚发布了一个具有B银行链接的图片消息。设想一下,A编写了一个在B的银行站点上进行取款的form提交的链接,并将此链接作为图片的src属性。如果B的银行在cookie中保存他的授权信息,并且此cookie没有过期,那么当B的浏览器尝试装载图片时,就会提交这个取款form和他的cookie,这样在没经B同意的情况下便授权了这次事务。

Drupal5.2以前,是存在CSRF安全漏洞,http://drupal.org/node/162360,在此以后,只要遵守Drupal表单API规范,便可以杜绝此类攻击。

 

Drupal表单API遵循HTTP/1.1规范,GET方法用于数据检索,除此以外不对数据作任何改动。如果需要对服务器进行改动,在Drupal中应该使用POST

 

其次,表单API带有令牌和标识ID,从而确保从POST请求中提交的表单数据来自于Drupal生成的表单。当你编写模块时,对于你的表单,一定要为其使用表单API,这样你就能够自动的获得保护。在你的模块中,对表单输入结果进行的任何操作,都应该放到该表单的提交函数中。

我们在实践中,需要避免两种情况:

1, 直接使用$_POST,并且使用html方式构建表单(绕过Drupal的表单API)。

2, 使用菜单回调直接处理修改数据的动作。

 

        对于前者,我们采用表单API既可,对于后者,我们也需要采用表单API,此时使用confirm_form(),在执行数据操作前,先确认一下。


Drupal版本:

27 哈希长度扩展攻击

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

哈希算法有很多,我们以MD5为例。首先MD5算法会对消息进行分组,每组64字节,不足64字节的部分用padding补齐。padding的规则是,在最末一个字节之后补充0x80,其余的部分填充为0x00padding最后的8字节用来表示需要哈希的消息长度。在对消息进行分组以及padding后,MD5算法开始依次对每组消息进行压缩,经过64轮数学变换。上一次压缩的结果,将作为下一次压缩的输入。

    长度扩展攻击的理论基础,就是将已知的压缩后的结果,直接拿过来作为新的压缩输入。在这个过程中,只需要上一次压缩后的结果,而不需要知道原来的消息内容是什么。

    为了避免潜在的安全隐患,在Drupal7中,我们不应该在代码中使用md5() sha1()函数,而应该使用更加安全的drupal_hash_base64()drupal_hmac_base64()

function drupal_hash_base64($data) {

  $hash = base64_encode(hash('sha256', $data, TRUE));

  // Modify the hash so it's safe to use in URLs.

  return strtr($hash, array('+' => '-', '/' => '_', '=' => ''));

}

    drupal_hash_base64使用了sha256算法,并使用base64进行编码,最后对哈希值作了字符替换,确保在URL中的安全性。


Drupal版本:

28 信息泄露

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

  信息泄露破坏了系统的保密性,它指信息被透漏给非授权的用户。这又分为多种情况。以前面sql注入的代码为例。

$result = db_select('node', 'n')

          ->fields('n',array('nid','title'))

            ->condition('n.type',$type)  

            ->execute();

    用户可以看到该类型下面的所有节点信息,当然这里面有些是需要做出限制的,比如未发表的节点,比如启用了节点访问控制模块后,用户无权访问的特定节点。此时我们需要对代码进行进一步的改进。

$result = db_select('node', 'n')

  ->fields('n',array('nid','title'))

  ->condition('n.type',$type)

          ->condition('n.status',1)

          ->addTag('node_access')    

  ->execute();

    这样用户便只能看到已发表的文章,同时节点访问控制模块也可以对我们的SQL进行修改,加上权限检查。


Drupal版本:

29 权限和页面回调

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

在我们编写模块时,需要注意菜单项中'access callback'access arguments’键的使用。在前面SQL注入的例子中,我们是这样的写的:

function sql_injection_menu() {

  $items = array();

$items['sql/injection'] = array(

    'title' => 'SQL注入实例',

    'description' => 'SQL注入实例.',

    'page callback' => 'sql_injection_page_callback',

    'access callback' => TRUE,

  );

return $items;

}

此时,任何人都可以访问这个页面。我们可以对它做出改进。比如我们可以这样设置

'access callback' => 'user_access',

'access arguments' => array('access content'),

这样只有具有“access content”访问内容)权限的用户才可以访问这个页面, “access content”是个很宽松的权限。我们也可以使用hook_permission()来定义你自己的权限,并使用它们来保护你的菜单回调函数。当然我们也可以自己定义一个访问控制回调函数,将其设置为'access callback'。有关这方面的更多信息,可参看菜单系统一章。


Drupal版本:

30 文件安全

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

在以前,如果你把Drupal站点安装在了IIS上,由于Drupal没有提供IIS下面的配置文件,假定你又不熟悉IIS的配置的话,那么有可能其它用户能将你的module文件下载到本地。通过查看页面源代码,就能看到JS的路径信息,如果JS没有压缩,就可以推测出来module文件的所在,直接访问.module文件,便可以将其下载到本地。

Drupal自带的.htaccess文件包含了以下代码,可以保护后台程序文件不被他人下载:

# Protect files and directories from prying eyes.

<FilesMatch "\.(engine|inc|info|install|module|profile|po

|sh|.*sql|theme|tpl(\.php)?|xtmpl)$|^(code-style\.pl

|Entries.*|Repository|Root|Tag|Template)$">

Order allow,deny

</FilesMatch>

 

Order指令设为了allow,deny,这意味着默认的行为是拒绝。对于匹配上述规则的文件的请求,都会被拒绝访问。如果直接我们访问:http://localhost/thinkindrupal/sites/all/modules/custom/sql_injection/sql_injection.module。此时会得到一个拒绝访问页面。

图片1.png 

 

如果我们将“Order allow,deny”修改为“Order deny,allow”,此时再访问这个页面,我们竟可以下载这个文件了。

图片2.png 

 

对于IIS服务器,Drupal7自带的web.config里面也包含和.htaccess对应的规则:

<rule name="Protect files and directories from prying eyes" stopProcessing="true">

<match url="\.(engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\.php)?|xtmpl)$|^(\..*|Entries.*|Repository|Root|Tag|Template)$" />

<action type="CustomResponse" statusCode="403" subStatusCode="0" statusReason="Forbidden" statusDescription="Access is forbidden." />

</rule>


 


Drupal版本:

31 维护一个Drupal模块

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

如果你创建了一个Drupal模块,想把它贡献到drupal.org的话,你首先需要申请一个账号,这个账号可以向GIT提交代码,这里是Drupal项目的Git,也就是以前的CSV账号。通常情况下,这个申请都是比较困难的,因为需要得到GIT管理员的批准,他们会对代码里面的问题提出很多的意见,让你修改,你可以按照他们的要求,来修改,直到账号批准下来。对于很多中文开发者来说,这个过程比较麻烦。还有一个办法就是,找一个拥有GIT账号的人,帮你创建这个项目,然后把代码提交上来,这个时候你可以申请成为项目的维护者,当你成为了一个项目的维护者的时候,自然就拥有了代码提交的权限。我就是通过后一种途径获得的CSV账号,现在升级成为GIT账号了。当然,你还可以创建一个沙盒项目,通过这种方式维护你的代码,但是沙盒项目里面有很多的限制。

当我们创建一个新的项目的时候,需要看看,是不是已经有了这样的项目,如果已经有了这样的项目,最好的办法是去完善已有的项目,而不是做重复的工作。我们这里以要编写的面包屑模块为例,其实已经有很多面包屑模块,这个时候,如果我们有新的想法,此时可以申请成为已有模块的维护者,但是如果没有人批准的话,我们再去创建一个啊。比如现在已经有了一个breadcrumb模块了,如果我们再去创建一个entity_breadcrumb或者breadcrumb2的话,drupal.org上面,就会有人冒出来,提出这样的问题,这个模块与什么什么模块有哪些区别啊? 如果没有区别的话,合并了算了。我在breadcrumb模块的问题列表里面申请成为维护者,但是没有回应,这个时候,我就可以创建一个新的模块了,再有人来问,也有了理由答复他们。

我们把要创建的模块命名为breadcrumb2,现在我们来添加一个这样的项目,如果你已经拥有GIT账号了,此时可以访问http://drupal.org/project/user,这里有你已有的项目列表,我维护的模块有点多了,在这里的截图太大了。

图片1.png 

点击最下面的“Add a new project”链接,进入页面http://drupal.org/node/add/project-project,这个页面就是一个普通的节点添加页面,我们在这里输入相关的信息即可:

图片2.png 

这是创建好的样子:

图片3.png 

项目描述里面,咱用的可是英文啊,不过这些英文都是从profile2模块里面复制过来的,改了一下里面的名字而已。中间的对号的右边是提示信息,提示我们Git的资源库正在准备创建中,只需要等待几秒钟就可以了。

现在,我们点击页面标签里面的“Version control”标签,进入页面http://drupal.org/project/breadcrumb2/git-instructions。这个时候,我们的Git资源库还没有建立起来,我们会看到以下信息:

图片4.png 

这里面列出来了Git命令,这些命令对于像我这样不熟悉Git的用户,就很方便了。我用的是Windows下面的Git客户端,大家可以到http://code.google.com/p/msysgit/下载。安装好以后,会有一个Git Bash快捷键,点击这个快捷键,就会出来Git的命令行式的对话框。

图片5.png 

我把维护的项目都放到E盘的ghr/test目录了,需要使用命令先导航到这个目录。输入以下命令即可:

cd e:/ghr/test

接下来,我们按照页面上面的提示,依次执行相应的命令即可。这里分别是:

mkdir breadcrumb2

 cd breadcrumb2

 git init

 echo "name = Breadcrumb2" > breadcrumb2.info

 git add breadcrumb2.info

 git commit -m "Initial commit."

 git remote add origin g089h515r806@git.drupal.org:project/breadcrumb2.git

 git push origin master

有些命令,比如mkdirgit add breadcrumb2.info,我们也可以通过Window的图形界面完成,能够通过图形界面完成的,我都采用图形界面。省事。

当输入最后一个命令“git push origin master”时,会提示输入你在git.drupal.org上面的密码,输入密码后,才会将本地的代码提交上去。

执行完这些命令以后,刷新,此时这个页面的内容会有变化,此时显示出来更多的Git命令:

图片6.png 

这些命令上面的提示都是英文的,我们这里分别介绍一下。

 

1,如果是第一次下载这些资源库里面的内容的话,也就是将Git 上面的代码复制到本地,使用以下命令:

git clone --recursive --branch master g089h515r806@git.drupal.org:project/breadcrumb2.git

 cd breadcrumb2

这里的master是当前的分支,我们一般是不使用这个分支的,而是使用7.x-1.x7.x-2.x8.x-1.x这样的分支。在下面我们会介绍如何创建一个新的分支。如果本地已经有了现成的代码,就不需要使用这段命令了。

 

2,将你本地的资源库,与你的drupal.org帐户关联起来,这里的关联使用的是电子邮件。命令如下:

git config user.email g089h515r806@gmail.com

当然,这里的电子邮件地址不一定使用真实的,也可以使用这样的形式g089h515r806@174740.no-reply.drupal.org。能够在Git上面将你标识出来即可。

 

3,检查资源库的状态,用来检查当前的代码与Git上面的代码是否同步:

git status

 

4,切换到一个不同的分支:

git branch -a

git checkout [branchname]

这里的第一行命令,用来列出所有可用的分支,也就是你拥有权限访问的分支,第二行命令用来切换。

 

5,提交本地的变更:

git add -A

git commit -m "Issue #[issue number] by [comma-separated usernames]: [Short summary of the change]."

如果这个变更是由别人提交的补丁,此时最好注明作者,也就注明谁做的贡献,我们可以在说明里面列出来,也可以使用下面的命令:

git commit --author="deviantintegral <deviantintegral@71291.no-reply.drupal.org>" -m "Issue #[issue number] by [comma-separated usernames]: [Short summary of the change]."

这里面的作者选项,可以在作者所在的页面http://drupal.org/user/71291,里面的“Git attribution”找到。deviantintegralField Validation模块提交过补丁,我在将代码提交上去的时候,将这个提交的作者设置为了deviantintegral。这样可以调动更多地人的积极性。

 

6,将你本地的修改,提交到Drupal.org上面的资源库中:

git push -u origin master

这个命令通常和上面的两个命令一起用的。

 

7,更新最新的代码,使本地的代码与Drupal.org上面的资源库保持同步:

git pull origin master

这里是以线上的代码为标准的,本地的和线上的保持一致。通常在为模块创建补丁的时候,使用这个命令,来做准备工作。

 

8,创建一个补丁文件:

git diff >  [description]-[issue-number]-[comment-number].patch

中括号里面的文字需要替换为你自己的。

 

9,应用一个补丁,你需要把别人的补丁下载到本地的对应目录,然后执行以下命令:

git apply -v [patchname.patch]

 

打好补丁以后,需要将这个补丁删除:

rm  [patchname.patch]

 

10,撤销没有提交的变更,首先将变更保存到一个文件里面:

git checkout [filename]

然后,撤销所有的变更:

git reset –hard

 

11,创建一个新的开发分支,并将其传到drupal.org上:

git checkout -b 7.x-1.x

git push -u origin 7.x-1.x

12,为稳定版打标签:

git checkout  7.x-1.x

git tag 7.x-1.0

git push origin 7.x-1.0

如果我们当前已经处在了7.x-1.x,此时就不需要执行第一行命令了。除了7.x-1.0以外,还可以用7.x-1.0-alpha1, 7.x-1.0-beta1这样的名字。

 

当我们创建一个新的项目后,记住,不要使用master分支,而是创建一个专门的分支,这里我们为Breadcrumb2执行命令11里面所列的命令。平时维护项目的时候,用到哪些命令,现查现用即可。另外需要注意的是,访问这个页面的时候,上面有个下拉选择框:

图片7.png 

这里可以选择master7.x-1.x7.x-2.x这样的开发分支,选择后,下面的命令提示也会有相应的变化。

 

如果我们编写的模块,对很多人有用,慢慢地,用的人就会多起来,如果模块的代码质量不好,功能有限,很快就会无人问津了。模块本身是否成功,很大程度上,取决于模块的维护者。


Drupal版本:

5 流程控制语句

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

流程控制语句是程序中用来控制执行流程的指令,比如条件语句和循环语句。条件语句有ifelse elseif、和switch语句。循环语句有whiledo-whileforforeach

 

流程控制语句在关键字(ifelseif、 whilefor, 等等)和开括号之间应有一个空格,从而将其与函数调用(也使用圆括号,但是没有空格)区分开来。“{”应该与关键字位于同一行(而不是自成一行),这一点对于很多熟悉PHP的程序员来说,开始的时候可能很不适应,但这就是Drupal的规范,它与PHP的规范还是有区别的。“}”应该自成一行。

    下面的这段代码摘自node.modulenode_block_list_alter函数:

 

if (!empty($node)) {

// This is a node or node edit page.

if (!isset($block_node_types[$block->module][$block->delta][$node->type])) {

// This block should not be displayed for this node type.

unset($blocks[$key]);

continue;

}

}

elseif (isset($node_add_arg) && isset($node_types[$node_add_arg])) {

// This is a node creation page

if (!isset($block_node_types[$block->module][$block->delta][$node_add_arg])) {

// This block should not be displayed for this node type.

unset($blocks[$key]);

continue;

}

}

else {

// This is not a node page, remove the block.

unset($blocks[$key]);

continue;

}

花括号“{}”一般总是使用的,即便是不需要的时候,为了增强代码的可读性,并降低出错的可能性,也应使用“{}”。下面的正确代码摘自于代码摘自node.modulenode_mark函数。

 

错误的

if (!$user->uid)

  return MARK_READ;

正确的

if (!$user->uid) {

return MARK_READ;

}

切换语句的格式应该这样(注意break;”语句在默认情况下不是必需的),摘自于代码摘自node.modulenode_block_view函数

switch ($delta) {

case 'syndicate':

$block['subject'] = t('Syndicate');

$block['content'] = theme('feed_icon', array('url' => 'rss.xml', 'title' => t('Syndicate')));

break;

 

case 'recent':

if (user_access('access content')) {

$block['subject'] = t('Recent content');

if ($nodes = node_get_recent(variable_get('node_recent_block_count', 10))) {

$block['content'] = theme('node_recent_block', array(

'nodes' => $nodes,

));

} else {

$block['content'] = t('No content available.');

}

}

break;

}

当一个情况执行完以后,打算继续执行下一情况时,此时可以省略“break;”语句。


Drupal版本:

6 函数调用

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

在函数调用中, 在操作符(=, <, >,等等)的两边应该各有一个空格,而在函数名和函数的开括号之间则没有空格。在函数的开括号和它的第一个参数之间也没有空格。中间的函数参数使用逗号和空格分隔,在最后一个参数和闭括号之间没有空格。在上面的例子中,区块内容的赋值语句就说明了这几点:

正确的

$block['content'] = theme('feed_icon', array('url' => 'rss.xml', 'title' => t('Syndicate')));

 

如果这样写就错误了

$block['content']=theme ('feed_icon',array('url'=>'rss.xml','title'=>t('Syndicate')));

 

 

这个规则也存在例外的情况。在一个包含多个相关赋值语句的代码块中,如果能够提高可读性,那么可以在赋值操作符周围插入多个空格(摘自node.moduletemplate_preprocess_node函数):

 

$variables['date']      = format_date($node->created);

$variables['name']      = theme('username', array('account' => $node));

 

$uri = entity_uri('node', $node);

$variables['node_url']  = url($uri['path'], $uri['options']);

$variables['title']     = check_plain($node->title);


Drupal版本:

7 函数声明

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

在函数的名字和它的开括号之间没有空格。在编写函数时,如果它的有些参数需要使用默认值,那么需要把这些参数列在后面。还有,如果你的函数生成了任何有用的数据,那么你需要返回该数据,以供调用者使用。下面给出了一些函数声明的例子摘自node.module

 

错误的

function node_is_page ($node) {

  $page_node = menu_get_object();

  $value = (!empty($page_node) ? $page_node->nid == $node->nid : FALSE);

}

 

function node_title_list ($title = NULL, $result) {

}

 

正确的

function node_is_page($node) {

  $page_node = menu_get_object();

  return (!empty($page_node) ? $page_node->nid == $node->nid : FALSE);

}

 

function node_title_list($result, $title = NULL) {

  …

}


Drupal版本:

8 函数名字

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

Drupal中,函数的名字都是小写的,为了避免命名空间冲突,通常以模块的名字或者它们所属系统的名字作为前缀。下划线用来分隔函数名字的描述性部分。在模块名的后面,通常应该紧跟一个动词,接着是动词作用的对象:modulename_verb_object()。在下面的第一个例子中,函数名字没有正确的使用模块前缀,并且动词和它的对象颠倒了。在接下来的例子中,很明显,修正了这些错误。

 

错误的

function save_node_action ($node) {

...

}

 

正确的

function node_save_action ($node) {

...

}

私有函数与其它函数一样,遵守相同的习惯,不过它在函数名字前面加了一个下划线。

function _node_index_node($node) {

  …

}


Drupal版本:

9 数组

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

对于数组,也是使用空格对它的每个元素和每个赋值操作符进行分隔的。如果数组区块跨越了80个字符,那么每个元素都应独立成行。为了提高可读性和可维护性,最好将每个元素全部独立成行。这样你就可以方便的添加、删除数组元素。

 

错误的

function node_uri($node) {

  return array('path' => 'node/' . $node->nid);

}

正确的

function node_uri($node) {

  return array(

    'path' => 'node/' . $node->nid,

  );

}

注意 数组中最后一个元素的后面有一个逗号,这不是一个PHP错误。最后的这个逗号,在PHP中是可有可不有的。这里加了逗号,是为了避免犯错,如果最后没有逗号,假如开发者在数组列表的最后又添加了一个元素,此时就会引起语法错误。这个规范是推荐使用的,不过它不是必须的。

 

在创建内部的Drupal数组时,比如菜单项或者表单定义,总是将每个元素单独成行:

 

$items['node/add'] = array(

'title' => 'Add content',

'page callback' => 'node_add_page',

'access callback' => '_node_add_access',

'file' => 'node.pages.inc',

);


Drupal版本:

1编码规范

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

Drupal社区认为,它的核心代码必须遵守一个标准的规范,从而提高代码的可读性,也使得初学者更容易学习DrupalDrupal官方也鼓励第3方模块的开发者采用这些规范。实际上,如果你没有遵守编码规范,你的模块仍然能够正常工作,但是模块中的代码,不利于同行之间的交流。我们首先学习一下具体的规范,接着介绍了一些工具,用来自动检查代码的质量(甚至可以为你纠正代码)。


Drupal版本:

2行缩进

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

Drupal代码的缩进,使用的是两个空格,而不是tab键。如果你习惯了使用tab键,那么你可以在你的编辑器中,设置一个首选项,将tab键设置为两个空格,这样你就可以继续使用Tab键,并且也能达到缩进两个空格的目的。


Drupal版本:

3运算符

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

对于所有的二元运算符,比如+-=!===>,应该在运算符的前面和后面增加一个空格,从而增加可读性。例如:

正确:

     $arg[0] == 'node' && $arg[1] == 'add' && $arg[2]

错误:

      $arg[0]=='node'&&$arg[1]=='add'&&$arg[2]

     对于一元运算符,则不应该包含空格。比如:

$i++;

     对于三元运算符,符号两边应该有空格:

return is_object($node) ? $node->type : $node;


Drupal版本:

4PHP开始和结束标签

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

包含代码的文件,比如.module.inc文件,会使用PHP代码开始标签,如下所示:

 

<?php

...

 

Drupal中不能使用开始标签的简写形式“<?”

结束标签“?>”,在PHP中不是必须的,但是在Drupal的模块代码中,它是被禁止使用的。如果使用了PHP的结束标签,那么可能会带来一些潜在的问题。不过这也有例外情况,在Drupal的模板文件中,为了退出PHP并且回到HTML中,此时会使用结束标签,例如,在themes/bartik/templates/node.tpl.php中,就同时用到了PHP的开始、结束标签:

 

  <?php print render($title_prefix); ?>

  <?php if (!$page): ?>

    <h2<?php print $title_attributes; ?>>

      <a href="<?php print $node_url; ?>"><?php print $title; ?></a>

    </h2>

  <?php endif; ?>

  <?php print render($title_suffix); ?>


Drupal版本:

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

第1章 实体(Entity)API

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

节点系统,在Drupal里面的历史悠久,是很早很早以前,就有了的一种机制。慢慢的,围绕着节点系统,出现了Flexinode,我们看这个模块的创建日期,200428日,快9年了,Flexinode允许用户创建新的内容类型,并为内容类型添加字段,随着技术的进步, Flexinode的实现机制跟不上了发展的需要;两年后,也就是2006年,出现了CCK模块,此时Drupal的版本还是4.7;随后,CCK取代了Flexinode,发展成为了Drupal5Drupal6下面的标准实现;并最终进入了Drupal7的内核,当然进入Drupal7后,名字改为了Field

    在Drupal5、 Drupal6下面,随着CCK的日趋流行,它给节点系统带来了极大的灵活性,在Drupal 5、 Drupal6两个版本下,CCK是仅支持节点系统的。在Drupal社区当中,一种声音愈来愈强,这就是将评论、区块、用户Profile以及很多其它系统,转为节点系统的声音。读过Drupal专业开发指南中文版的用户,在里面的节点系统一章,讲过一个问题,不是一切东西都是节点,里面讲述了为什么不把评论处理成为节点,很多人都知道这个道理,但是把所有系统,向节点系统靠拢的趋势,一直存在着,并且影响越来越广。为什么会这样?因为CCK模块的存在,使得节点系统非常的灵活。

1.png

     在这种趋势的影响下,出现了Node as BlockNode commentContent Profile等流行模块,它们将区块、评论、profile建立在了节点系统之上,这样就可以方便的利用CCK了。随着,这些模块的流行,人们也逐渐的意识到了,这种方式的局限性。有哪些局限性呢?在节点系统中,除了CCK字段以外,它还有自己属性,比如作者、创建日期、修改日期、标题、nid、置顶、推到首页等等,很多这样的属性,这些属性对于其它系统可能是多余的无用的,所以人们还需要编写模块来隐藏这些属性,或者自动设置某些属性,比如对于标题,就存在一个Automatic Nodetitles,帮助用户隐藏多余的标题。


Drupal版本:

1 实体

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

    在CCK进入Drupal7内核的时候,很多人进一步思考的CCK的优势,并且认识到,很多其它的系统,比如用户系统、分类系统、评论系统,也都存在利用CCK的需求。因此,在Drupal7下,对CCK模块做了进一步的扩充,并将其更名为Field模块,使得用户可以向节点、评论、分类术语、用户上面添加字段。而这种扩充的结果,就是引入了实体(Entity)这样的一个概念,实体是伴随着Field进入Drupal内核,而衍生抽象出来的一个概念。什么是实体呢?这种概念性质的东西,不是一句话就理解透的,我们看看例子:1.png


Drupal7下,节点、评论、用户、分类都是实体的一种。对于实体,在Drupal7下面,我们可以这样理解,具有相同属性,并且可以为它添加字段的一种东西。当然,有些实体,是不能够向它们上面添加字段的,这种情况也是存在的。当我们定义一种实体的时候,可以为它定义一组公用的属性,然后基于Field模块,可以为其添加各种字段。我们可以看到,评论,它是一种实体,所有的评论对象都具有公用的一组属性,由于可以往实体上面添加字段,所以我们这里,就不存在把评论系统建立在节点系统之上的问题了,我们这里既利用了Field模块的灵活性,也不用额外的处理并不属于该类实体的属性。这是一个进步。2.png

Drupal版本:

10 总结

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

通过本章的学习,我们学到了以下内容:

  (1),什么是实体,实体在Drupal7中的重要地位

  (2),如何基于Entity API自定义一个实体类型

  (3),EntityFieldQuery的介绍、使用


Drupal版本:

2 Bundle(包)

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

伴随着,实体的出现,还出现了另一个概念,Bundle,中文可以翻译为。什么是Bundle呢,我们可把它看作,一种实体下面的一个具体实现。以节点为例:1.png

   

Drupal7核心内置的两种内容类型: ArticlePage,就是一种Bundle。我们可以把Bundle看作,是Drupal7下面,对内容类型的一种概念上的扩展。对于分类来讲,一个词汇表,就是一个Bundle。在Profile2下面,我们可以为不同的角色定义不同的Profile,比如学生所用的Profile、教师所用的Profile,这些也是一种Bundle

在同一种实体下面,不同Bundle之间,它们的属性是通用的,但是可以有不同的默认配置;它们的字段,则可以分别添加,不同的Bundle,可以为其添加不同的字段,当然两个Bundle,我们为其添加了相同的字段,这也是允许的。


Drupal版本:

3 不是所有的东西都是实体

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

现在,节点、分类术语、评论、用户都统一到了实体上来了,这是不是意味着说,所有的东西都是实体呢?当然不是。在Drupal7的核心系统里面,区块系统就不是实体,菜单系统也不是实体,因此,我们可以说,不是所有的东西都是实体。但是这些东西,正在被转为实体,比如区块,现在有一个第三方模块bean,全称为“Block Entities Aren't Nodes” “区块实体不是节点,此外,在Drupal8的核心开发里面,菜单连接,也正在被转为实体。此外,比如webform模块,一直都是非常流行的,但是到了Drupal7下面,出现了Entity form模块,与之竞争,将来webform是否会被Entity form模块所取代,这个现在还不好判断。比如在UbercartCommerce里面,订单,都已经被处理成为了实体,而在Commerce里面,产品、客户信息都已被处理成为实体。还有我们提到过的,Field Collection模块,字段实体的出现,更是扩展了实体的应用范围。

因此,我们可以这样说,不是所有的东西,都是实体;但是所有的东西,都可以转为实体,只要你去转,都可以转过来。


Drupal版本:

4 Drupal7核心实体API介绍

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

Drupal7中,核心的实体系统,存放在includes下面的entity.inc文件中,这里面,包含一个DrupalEntityControllerInterface接口、DrupalDefaultEntityController类、EntityFieldQueryException类、EntityFieldQuery类、EntityMalformedException类。所有的实体,必须实现DrupalEntityControllerInterface接口,对于我们常见的大多数实体,只需要继承DrupalDefaultEntityController类即可,如果需要实现的实体不是存放在数据库中的,此时则需要单独的实现DrupalEntityControllerInterface接口。EntityFieldQueryEntity FieldSQL查询做了封装,当然,封装的不是很彻底,有很多的局限性,对于我们常见的查询,尤其涉及到字段的,用起来非常方便。我们在本章会对EntityFieldQuery做专门的介绍。

commons.inc文件中,定义了一组与实体相关的API函数:

函数名

用途描述

entity_create_stub_entity

帮助函数,用来组装一个带有初始ids的对象结构。

entity_extract_ids

帮助函数,用来从一个实体中提取idvidbundle等信息。

entity_form_field_validate

把字段API验证附加到实体表单上。

entity_form_submit_build_entity

对于简单的实体表单,这个函数可以用来将表单提交的值复制到实体属性上面来。

entity_get_controller

获取某一实体类型的实体控制器类。

entity_get_info

获取某一实体类型的实体信息数组。

entity_info_cache_clear

重置有关实体类型的缓存信息。

entity_label

返回实体的标签。

entity_language

返回实体的语言。

entity_load

从数据库中加载实体。

entity_load_unchanged

从数据库中加载未被修改了的实体。

entity_prepare_view

用来触发钩子hook_entity_prepare_view()

entity_uri

返回实体的URI元素。

我们看到,Drupal7里面,与实体相关的API函数放在了commons.inc文件中,这是一个权宜之计。在Drupal8里面,这些相关的函数都被放到了entity.inc文件中去了。

Drupal核心,包含了与实体相关的钩子函数,这些钩子函数,允许我们在实体生命周期的各个不同阶段,与之交互。我们来看一下这些钩子函数。我们可以在system模块里面的system.api.php文件中,找到这些钩子函数。

钩子

描述

hook_entity_delete

实体删除时,用来交互的钩子。

hook_entity_info

这个钩子可以用来定义一个或者多个实体类型。

hook_entity_info_alter

用来修改hook_entity_info里面的定义信息。

hook_entity_insert

实体插入时,用来交互的钩子。

hook_entity_load

实体加载时,用来交互的钩子。

hook_entity_prepare_view

准备显示实体时,用来交互的钩子。

hook_entity_presave

在保存实体前,用来交互的钩子。

hook_entity_query_alter

用来修改或者执行一个EntityFieldQuery

hook_entity_update

实体更新时,用来交互的钩子。

hook_entity_view

在实体正在组装时,呈现(render)之前,用来交互的钩子。

hook_entity_view_alter

用来修改ENTITY_view()的结果。

hook_entity_view_mode_alter

用来修改正被显示的实体的查看模式。

 


Drupal版本:

5 Entity API模块

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

我们需要注意,模块的名字就叫Entity API,简称entity,这个名字起的够霸道的,模块的作者也很牛,就是fagoDrupal核心Entity系统的维护者。在Drupal7将要发布的时候,Fago就认识到了Drupal7里面,对实体的支持,还很不完善,他还专门写了一篇文章,指出未来的方向是实体,为此他开发了Entity API模块,作为Drupal核心实体系统的补充,Entity API模块里面的很多概念,被Fago搬到了Drupal8的内核里面了。Entity API模块做了多方面的扩展,我们这里简单了解一下。


Drupal版本:

7 定义自己的实体类型

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

其实2011年的时候,自己就想编写一个模块,定义一个实体类型,最初的想法是把区块改造为实体,我在官方网站上面搜索,发现已经有了这样的一个模块了,bean,就是这个时候冒出来的。后来想把Contact模块,重新实现为实体的形式,不过发现了有个Entity Forms模块,可以做这件事情。再往后,就想不出来哪些东西可以改造为实体了。一直到搭建网上书店时,解决面包屑问题的时候,有了将面包屑改造为实体的想法,并且在现有的模块当中,没有这样的技术实现。

在这一章中,大家跟我一道来学习一下,如何采用实体的形式来实现Drupal里面的面包屑功能。面包屑这个实体,有一点点小小的特殊,它的bundle只有一个,还是面包屑,类似于用户,实体下面的bundle只有一个。

从我的角度,推荐几个模块,供大家阅读、学习、使用,推荐的有Profile2模块,这个模块是基于Entity API模块的,很具有代表性,而且我们在实际当中,也经常用到这个模块,还有,这个模块进入了Drupal8的内核了。其次推荐Examples里面的entity_example模块,推荐model模块,这是一个基于Entity API的更简单实用的例子,只不过模块本身没有太大价值。

现有的面包屑模块很多,这里罗列一下,custom_breadcrumbsmenu_breadcrumbpath_breadcrumbsbreadcrumbcrumbshanseltaxonomy_breadcrumb,其中path_breadcrumbscrumbs是后起之秀。我们这里把我们的模块的名字叫做breadcrumb2,参考一下Profile2,我在breadcrumb项目的问题列表中申请成为维护者,但是没有人回应,现在看起来需要另起炉灶了。

在我们创建模块之前,请大家把Profile2model两个模块的代码阅读一遍,为什么呢?因为我们的很多代码都是从这里直接复制过来的。作者也是第一次创建一个实体类型,很多时候也是摸着石头过河。


Drupal版本:

7.1 Info 文件

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

   我们创建一个breadcrumb2.info文件,输入以下内容:

name = Breadcrumb2

description = Supports configurable breadcrumbs.

core = 7.x

files[] = breadcrumb2.admin.inc

files[] = breadcrumb2.info.inc

files[] = breadcrumb2.test

dependencies[] = link

dependencies[] = rules

dependencies[] = field_validation

configure = admin/structure/breadcrumbs

这里面的键值我这里就不讲解了,我们这个模块依赖于rulesentity模块,由于rules本身依赖于entity,所以我们这里只需要依赖于rules即可。我们需要添加一个链接字段,面包屑导航里面包含的就是多个链接,所以我们这里依赖于link模块。由于我们将path,也就是内部路径,处理成为了字段的形式,所以我们需要加一些验证,也就用到了field_validation模块了。


Drupal版本:

7.10 为面包屑添加字段

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

导航到admin/structure/breadcrumbs/fields,添加两个字段“Path”Breadcrumb Link。这是添加好的样子:

图片3.png 

对于“Path”字段,我们将其设置为必填的,其它采用默认配置。对于Breadcrumb Link,选中了“Optional URL”,将“Link Title”设置为了“Required Title”,将“Number of values”设置为了“unlimited”(不限)。我们后面根据需要可能会调整这里的配置。


Drupal版本:

7.11 添加实体

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

我们首先定义菜单项:

  $items['breadcrumb/add'] = array(

    'title' => 'Add breadcrumb',

    'page callback' => 'breadcrumb2_add',

    'access arguments' => array('administer breadcrumbs'),

    'file' => 'breadcrumb2.admin.inc',

  );

接着,向breadcrumb2.admin.inc添加回调函数breadcrumb2_add

/**

 * Returns a breadcrumb submission form.

 */

function breadcrumb2_add(){

  $breadcrumb = entity_get_controller('breadcrumb2')->create();

  drupal_set_title(t('Create breadcrumb'));

  $output = drupal_get_form('breadcrumb2_form', $breadcrumb);

 

  return $output;

}

在这个函数里面,我们首先使用entity_get_controller创建了一个初始化的面包屑,然后设置了标题,最后调用drupal_get_form,调用的表单IDbreadcrumb2_form,同时我们把$breadcrumb传递给了它。我们这样做的目的,是希望breadcrumb2_form同时能够适应于编辑表单。我们来看看breadcrumb2_form的定义:

/**

 * The breadcrumb edit form.

 */

function breadcrumb2_form($form, &$form_state, $breadcrumb) {

  // Save the breadcrumb for later, in case we need it.

  $form['#breadcrumb'] = $breadcrumb;

  $form_state['breadcrumb'] = $breadcrumb;

  

  $form['bid'] = array(

    '#type' => 'value',

    '#value' => isset($breadcrumb->bid) ? $breadcrumb->bid : NULL,

  );

  

  // Add the field related form elements.

  field_attach_form('breadcrumb2', $breadcrumb, $form, $form_state);

 

  $form['actions'] = array('#type' => 'actions');

  $form['actions']['submit'] = array(

    '#type' => 'submit',

    '#value' => t('Save'),

    '#weight' => 40,

  );

  if (!empty($breadcrumb->bid)) {

    $form['actions']['delete'] = array(

      '#type' => 'submit',

      '#value' => t('Delete breadcrumb'),

      '#weight' => 45,

      '#limit_validation_errors' => array(),

      '#submit' => array('breadcrumb2_form_submit_delete')

    );

  }

  $form['#validate'][] = 'breadcrumb2_form_validate';

  $form['#submit'][] = 'breadcrumb2_form_submit';

  return $form;

}

首先,我们将$breadcrumb保存到了$form$form_state里面,这里保存原始的面包屑对象。接着,我们添加了'bid'元素,它的类型为value,为什么这样做呢?因为有些模块会使用entity_extract_ids来提取实体的ID,在Drupal7里面,如果我们不预先保存一个实体ID的话,entity_extract_ids就会提取不出来,Drupal8已经改进了这个问题,我遇到过这个问题,所以在这里加了了'bid'。接着是使用field_attach_form,把实体上面的字段也添加进来,由于字段的添加,是动态的,我们也不知道具体有几个字段,通过field_attach_form,我们就可以把这个工作委托给Field模块了,它能够帮我们做这件事情。再往下是提交、删除按钮,我们把这两者放到了actions里面了,actionsDrupal7里面的一个新的表单元素类型;对于删除按钮,我们为它指定了一个单独的提交函数breadcrumb2_form_submit_delete,同时为它设置了'#limit_validation_errors',通过这个设置,在删除面包屑的时候,即便是存在验证错误的话,也可以正常提交。最后是为表单设置$form['#validate']$form['#submit'],我们在第一集里面学过,即便不设置这两行代码,表单系统也会自动使用这两个函数,为什么明确的设置呢?因为我看到其它实体的添加表单都是这样明确设置的,包括节点的添加表单,或许这样做的好处的,代码的可读性更强。

接下来是表单验证函数:

function breadcrumb2_form_validate($form, &$form_state) {

  $breadcrumb = $form_state['breadcrumb'];

  

  // Notify field widgets to validate their data.

  field_attach_form_validate('breadcrumb2', $breadcrumb, $form, $form_state);

}

代码比较简单,我们模块本身没有多少验证工作,所以这里直接将验证工作委托给了Field模块,这里使用的是field_attach_form_validate

再往下,是提交函数,逻辑也比较简单:

/**

 * Breadcrumb form submit handler.

 */

function breadcrumb2_form_submit($form, &$form_state) {

 

  $breadcrumb = &$form_state['breadcrumb'];

  // Notify field widgets.

  field_attach_submit('breadcrumb2', $breadcrumb, $form, $form_state);

  

  // Save the breadcrumb

  breadcrumb2_save($breadcrumb);

 

  drupal_set_message(t('Breadcrumb saved.'));

  $form_state['redirect'] = 'breadcrumb/' . $breadcrumb->bid;

 

}

我们首先使用field_attach_submit,将提交委托给了Field模块,接着我们调用我们的API函数breadcrumb2_save,保存实体。最后设置一个消息,并重定向。

在这部分,需要注意的是field_attach_formfield_attach_form_validatefield_attach_submit的使用。这里我有个疑问,不知道为什么不把field_attach_submit叫做field_attach_form_submit

编写完这些代码以后,可以测试一下了,如果你按照这里所列的代码,跟着做的,现在访问breadcrumb/add页面,我们可以看到一个表单页面,这里面包含前面我们添加的两个字段。一切正常。输入一些测试数据,提交,我们遇到了第一个问题:

图片4.png 

这是一个PHP错误。我在网上搜索了这个问题,很多人也遇到了同样的问题,但是没有找到答案。我们在开发的过程中,是存在一些问题,这些问题起初我也没有注意到,我也希望一次能够搞定所有的问题,但是错误总是不经意的出现。如果你能够,在现有的代码基础上,把所有的问题都解决掉,那么证明你完全掌握了如何定义一个实体类型。我也是在解决这些问题的过程中,才明白了里面的很多细微的地方。


Drupal版本:

7.12 调试并解决已有代码的问题

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

通过Google搜索,找不到答案。有很多同样的问题,但是里面没有我们想要的答案。这个时候,我们需要打开common.inc文件,找到7750行,Drupal核心的版本不一样,这里显示的行数也不一致。但是代码是一样的:

/**

 * Get the entity controller class for an entity type.

 */

function entity_get_controller($entity_type) {

  $controllers = &drupal_static(__FUNCTION__, array());

  if (!isset($controllers[$entity_type])) {

    $type_info = entity_get_info($entity_type);

//print debug($type_info);

    $class = $type_info['controller class'];

//drupal_set_message('123456');

    $controllers[$entity_type] = new $class($entity_type);

  }

  return $controllers[$entity_type];

}

当然,核心里面是没有print debugdrupal_set_message这两句的,这是我调试的时候添加了,我想查看一下$type_info里面包含哪些信息。发现这里打印出来的信息,和我们在前面定义的有出入。我首先想到的是,是不是'controller class'没有设置正确?我们需要自己定义一个控制器类?

问题1

通过print debug,我还是发现了一个错误,在breadcrumb2_entity_info里面:

      'entity keys' => array(

        'id' => 'pid',

      ),

漏网之鱼,复制别人的代码的时候,经常会遇到这种情况,将它修正过来:

      'entity keys' => array(

        'id' => 'bid',

      ),

问题2

'bundle keys'注视掉,我们只有一个bundle,没有‘type’这个概念。   

/*

      'bundle keys' => array(

        'bundle' => 'type',

      ),

  */

 

问题3

前面的两个问题,是我重读breadcrumb2_entity_info代码以后,所做的两个修正;但是问题仍然存在。我通过进一步的测试,发现验证是正常的,问题出在了表单的提交函数里面。我把breadcrumb2_form_submit里面的代码全部注销掉,问题没有了,当然我们的面包屑实体也没有保存起来。进一步的细化,发现breadcrumb2_save($breadcrumb);出了问题。

接着检查了breadcrumb2_save的代码,里面的逻辑也非常简单,只有这么简单的一行:

return $breadcrumb->save();

Breadcrumb里面,我们并没有定义save方法,这个方法应该是它的父类Entity定义的。所以回到Entity API模块里面查找Entity类的对应代码。此时我发现,这个类存放到了entity\includes下面的entity.inc文件中了。注意,这个文件夹下面,包含4inc文件,分别为entity.controller.incentity.incentity.property.incentity.ui.incentity.wrapper.inc。我们打开entity.inc文件,找到save方法:

  /**

   * Permanently saves the entity.

   *

   * @see entity_save()

   */

  public function save() {

    return entity_get_controller($this->entityType)->save($this);

  }

对这里的代码,做以下修改:

  public function save() {

    drupal_set_message($this->entityType);

    //return entity_get_controller($this->entityType)->save($this);

  }

我们去掉了实际的保存逻辑代码,加上了一个drupal_set_message,我想看看此时传递过来的实体类型到底是什么。

再次测试,发现$this->entityType的值竟然为“breadcrumb”,而实际应该为“breadcrumb2”,如果我们的模块名字为breadcrumb就好了,就不会遇到这个问题了。找到原因以后,很快就定位到了错误的代码地方,Breadcrumb的构造方法,原来为:

  public function __construct($values = array()) {

    parent::__construct($values, 'breadcrumb');

  } 

我们将它修正为:

  public function __construct($values = array()) {

    parent::__construct($values, 'breadcrumb2');

  }

一字之差,就让我们调试了半天,复制粘贴别人的代码的时候,经常会遇到这样的问题,要改的地方没有改过来,或者改错了。

 

问题4

原来的错误消失了,新的错误出现了:

图片4.png 

这个错误也非常严重,但是相比前面的错误,至少这个错误还能把页面完整显示出来。刚才发现Breadcrumb类定义的有问题。现在根据错误提示,“entity_id”有问题,也就是我们的bid设置的有问题,前面已经修正了一个地方了。我重读了Profile2模块里面的Profile类的定义,发现里面的代码中,包括很多属性,其中就包括pid。人家是这样定义的:

class Profile extends Entity {

 

  /**

   * The profile id.

   *

   * @var integer

   */

  public $pid;

。。。。

    当时我定义Breadcrumb类的时候,不知道这些属性的含义,另外在类的成员函数里面也没有用到这些属性,所以一股脑的把它们删除了,现在我们为Breadcrumb添加一个属性:

  /**

   * The breadcrumb id.

   *

   * @var integer

   */

  public $bid;

 

问题5

修改后,我们清空缓存,再次测试,原来的错误消失了,又出现了新的问题:

图片5.png 

我直接打开includes\database\query.inc文件,找到716行,看源代码:

  public function preExecute() {

    // Confirm that the user did not try to specify an identical

    // field and default field.

    if (array_intersect($this->insertFields, $this->defaultFields)) {

      throw new FieldsOverlapException('You may not specify the same field to have a value and a schema-default value.');

    }

 

    if (!empty($this->fromQuery)) {

      // We have to assume that the used aliases match the insert fields.

      // Regular fields are added to the query before expressions, maintain the

      // same order for the insert fields.

      // This behavior can be overridden by calling fields() manually as only the

      // first call to fields() does have an effect.

      $this->fields(array_merge(array_keys($this->fromQuery->getFields()), array_keys($this->fromQuery->getExpressions())));

    }

 

    // Don't execute query without fields.

    if (count($this->insertFields) + count($this->defaultFields) == 0) {

      throw new NoFieldsException('There are no fields available to insert with.');

    }

 

    // If no values have been added, silently ignore this query. This can happen

    // if values are added conditionally, so we don't want to throw an

    // exception.

    if (!isset($this->insertValues[0]) && count($this->insertFields) > 0 && empty($this->fromQuery)) {

      return FALSE;

    }

    return TRUE;

  }

这里的黑体部分,就是我们出错的地方,此时我突然想到了一个问题,我们的breadcrumb数据库表,里面只有一个bid字段,并且这个字段是自增的字段。我们向数据库里面插入数据的时候,是不需要设置这个字段的,除此以外,我们没有别的字段了。我要哭了。在没有遇到这个问题以前,我是没有思考过这个问题的。我查看了多个实体类型的定义,发现主表里面,都是有属性的,即便是Field Collection里面也有一个属性field_name。现在为我们的breadcrumb数据库表,添加什么样的属性呢?最初我考虑的是createdchanged,但是这两个属性对于我们来说,没有多大用处,最后我觉得,使用path,把它定义为这里的属性,不用Field模块来管理它了,当然也就不使用Field validation负责它的验证了,我们自己编写它的验证逻辑。这次改动有点多哟,好事多磨。

首先是schema里面的定义,粗体表示新增的

      'bid' => array(

        'type' => 'serial',

        'not null' => TRUE,

        'description' => t("'Primary Key: Unique breadcrumb item ID."),

      ),

      'path' => array(

        'description' => t('URL where breadcrumb should be shown.'),

        'type' => 'varchar',

        'length' => 256,

        'not null' => TRUE,

      ),

接着为类Breadcrumb,添加一个属性$path: 

class Breadcrumb extends Entity {

  /**

   * The breadcrumb id.

   *

   * @var integer

   */

  public $bid;

 

   /**

   * The internal path where breadcrumb should be shown.

   *

   * @var string

   */

  public $path;

 

这下,我们的breadcrumb2.info.inc文件有用了:

class Breadcrumb2MetadataController extends EntityDefaultMetadataController {

 

  public function entityPropertyInfo() {

    $info = parent::entityPropertyInfo();

    $properties = &$info[$this->type]['properties'];


    $properties['path'] = array(

      'label' => t('path'),

      'description' => t('The internal path where breadcrumb should be shown.'),

      'setter callback' => 'entity_property_verbatim_set',

      'setter permission' => 'administer breadcrumbs',

      'schema field' => 'path',

    );


    return $info;

  }

}

当然,我们的面包屑表单里面,也需要加上这个元素:

function breadcrumb2_form($form, &$form_state, $breadcrumb) {

  // Save the breadcrumb for later, in case we need it.

  $form['#breadcrumb'] = $breadcrumb;

  $form_state['breadcrumb'] = $breadcrumb;

  

  $form['bid'] = array(

    '#type' => 'value',

    '#value' => isset($breadcrumb->bid) ? $breadcrumb->bid : NULL,

  );

  $form['path'] = array(

    '#type' => 'textfield',

    '#title' => t('Path'),

    '#maxlength' => 60,

    '#default_value' => !empty($breadcrumb->path) ? $breadcrumb->path : '',

    '#weight' => -10,

  );

做完这些修改以后,卸载breadcrumb2模块,然后重新安装,然后删除Path字段,重新测试。成功了,只不过重定向回来的时候,路径还不存在,导致了空白问题。我通过phpmyadmin检查对应的数据,基本上都正确了,就剩下一个小问题,就是path属性没有保存下来,里面一直问题。这个问题解决起来也并不复杂:

function breadcrumb2_form_submit($form, &$form_state) {

 

  $breadcrumb = &$form_state['breadcrumb'];

  

  $breadcrumb->bid = $form_state['values']['bid'];

  $breadcrumb->path = $form_state['values']['path'];

  ….

粗体部分为新增的代码,这样就正常了。我是在解决这些问题的时候,更加深刻的掌握了创建一个实体类型的技术。原本打算重新写作这部分资料,后来还是觉得把过程中遇到的错误,也原原本本的呈现出来。这是Think in Drupal的一个风格,就是我们把开发中、配置中的错误也完全呈现出来,告诉大家的解决这些问题的步骤。


Drupal版本:

7.13查看面包屑实体

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

 首先定义菜单项:

  $items['breadcrumb/%breadcrumb2'] = array(

    'title' => 'Breadcrumb',

    'page callback' => 'breadcrumb2_page_view',

    'page arguments' => array(1),

    'access arguments' => array('administer breadcrumbs'),

    'file' => 'breadcrumb2.admin.inc',

  );

  $items['breadcrumb/%breadcrumb2/view'] = array(

    'title' => 'View',

    'type' => MENU_DEFAULT_LOCAL_TASK,

    'weight' => -10,

  );

   这里面我们使用了通配符%breadcrumb2,当我们传递过来一个bid以后,系统会自动的调用breadcrumb2_load函数,将bid转换为相应的面包屑对象。

接下来,我们看breadcrumb2_page_view的具体实现,向breadcrumb2.admin.inc里面添加以下代码:

/**

 * Breadcrumb view page.

 */

function breadcrumb2_page_view($breadcrumb, $view_mode ='full'){

  return $breadcrumb->view('full');

}

清除缓存,现在访问breadcrumb/1,已经可以显示出来了,但是面包屑的属性path没有显示出来。

此时有多种解决办法,一种是实现自己的控制器类BreadcrumbController,在里面实现自己的方法:

public function buildContent($entity, $view_mode = 'full', $langcode = NULL, $content = array())

    另一种,是在breadcrumb2_page_view里面,我们不调用$breadcrumb->view,然后自己去构建我们想要的内容。

function breadcrumb2_page_view($breadcrumb, $view_mode = 'full'){

 // return $breadcrumb->view($view_mode);

  $breadcrumb->content = array();

  

  if($view_mode = 'full'){

    $breadcrumb->content['path'] = array(

      '#markup' => filter_xss($breadcrumb->path),

      'weight' => -5,

    );

  }

  

  //Build fields content

  field_attach_prepare_view('breadcrumb2', array($breadcrumb->bid => $breadcrumb), $view_mode);

  entity_prepare_view('breadcrumb2', array($breadcrumb->bid => $breadcrumb));

  $breadcrumb->content += field_attach_view('breadcrumb2', $breadcrumb, $view_mode);

  return $breadcrumb->content;

}

   这种方式是我们自己调用field的集成。当然,如果我们熟悉$breadcrumb->view返回的数组结构的话,也可以这样编写代码:

$build = $breadcrumb->view($view_mode);

   if($view_mode = 'full'){

    $build['breadcrumb2'][$breadcrumb->bid]['path'] = array(

      '#markup' => filter_xss($breadcrumb->path),

      'weight' => -5,

    );

  }

return $build;

第三种办法,就是自己实现hook_ breadcrumb2_view这个钩子,这个钩子是从哪里定义的?这是Entity API模块帮助我们定义,只需要自己去实现即可,在module文件中添加以下代码,效果是一样的:

/**

 * Implement hook_breadcrumb2_view().

 */

function breadcrumb2_breadcrumb2_view($breadcrumb, $view_mode, $langcode){

  if($view_mode = 'full'){

    $breadcrumb->content['path'] = array(

      '#markup' => filter_xss($entity->path),

      'weight' => -5,

    );

  }

}

从这里面,我们可以看到Entity API帮助我们做了很多工作,省了不少事。


Drupal版本:

7.14 编辑面包屑实体

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

菜单项代码:

  $items['breadcrumb/%breadcrumb2/edit'] = array(

    'page callback' => 'breadcrumb2_page_edit',

    'page arguments' => array(1),

    'access arguments' => array('administer breadcrumbs'),

    'weight' => 0,

    'title' => 'Edit',

    'type' => MENU_LOCAL_TASK,

    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,

    'file' => 'breadcrumb2.admin.inc',

  );

这里在菜单项里面,用到了'context',它的含义可以参看第一集。接着我们向breadcrumb2.admin.inc添加breadcrumb2_page_edit函数:

/**

 * Breadcrumb edit page.

 */

function breadcrumb2_page_edit($breadcrumb){

  return drupal_get_form('breadcrumb2_form', $breadcrumb);

}

由于我们在前面创建面包屑的时候,编写breadcrumb2_form表单时,同时考虑了编辑时会用到,所以我们这里的代码就简洁了很多。将添加表单、编辑表单合二为一,这是一个好习惯。


Drupal版本:

7.15 删除面包屑实体

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

首先我们实现删除按钮的提交处理函数breadcrumb2_form_submit_delete

/**

 * Breadcrumb form submit handler for the delete button.

 */

function breadcrumb2_form_submit_delete($form, &$form_state) {

  $breadcrumb = $form_state['breadcrumb'];

  $form_state['redirect'] = 'breadcrumb/' . $breadcrumb->bid . '/delete';

}

在这里,当删除一个面包屑时,我们直接将它重定向到了对应的删除页面。对应路径的菜单项是这样定义的:

  $items['breadcrumb/%breadcrumb2/delete'] = array(

    'page callback' => 'drupal_get_form',

    'page arguments' => array('breadcrumb2_delete_confirm_form', 1),

    'access arguments' => array('administer breadcrumbs'),

    'weight' => 1,

    'title' => 'Delete',

    'type' => MENU_LOCAL_TASK,

    'context' => MENU_CONTEXT_INLINE,

    'file' => 'breadcrumb2.admin.inc',

  );

    breadcrumb2_delete_confirm_form是一个确认表单,我们把它的定义添加到breadcrumb2.admin.inc里面:

/**

 * Confirm form for deleting a profile.

 */

function breadcrumb2_delete_confirm_form($form, &$form_state, $breadcrumb) {

  $form_state += array('breadcrumb' => $breadcrumb);

  $confirm_question = t('Are you sure you want to delete breadcrumb for path %path?', array('%path' => $breadcrumb->path));

  return confirm_form($form, $confirm_question, 'breadcrumb/' . $breadcrumb->bid);

}

 

确认表单,采用了confirm_form,让Drupal系统来生成这个表单。最后我们来看一下,确认表单提交后的代码:

function breadcrumb2_delete_confirm_form_submit($form, &$form_state) {

  $breadcrumb = $form_state['breadcrumb'];

  $breadcrumb->delete();

  drupal_set_message(t('Deleted breadcrumb for path %path.', array('%path' => $breadcrumb->path)));

  $form_state['redirect'] = 'admin/structure/breadcrumbs';

}

这里面的代码,主要借鉴了Profile2里面的profile2_page.inc文件里面的代码。


Drupal版本:

7.16 安装时为实体创建字段

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

我们这里是手动的创建Breadcrumb link字段,我们希望在安装这个Breadcrumb2模块的时候,自动的帮我们创建这个字段。这个字段,对我们来说是必须的。首先,我们把现有的字段的信息导出来。这里我们使用Features模块。

这是Features导出来的代码:

/**

 * Implements hook_field_default_fields().

 */

function breadcrumb_link_field_default_fields() {

  $fields = array();

 

  // Exported field: 'breadcrumb2-breadcrumb2-field_breadcrumb_link'

  $fields['breadcrumb2-breadcrumb2-field_breadcrumb_link'] = array(

    'field_config' => array(

      'active' => '1',

      'cardinality' => '-1',

      'deleted' => '0',

      'entity_types' => array(),

      'field_name' => 'field_breadcrumb_link',

      'foreign keys' => array(),

      'indexes' => array(),

      'module' => 'link',

      'settings' => array(

        'attributes' => array(

          'class' => '',

          'rel' => '',

          'target' => 'default',

        ),

        'display' => array(

          'url_cutoff' => 80,

        ),

        'enable_tokens' => 1,

        'title' => 'optional',

        'title_maxlength' => 128,

        'title_value' => '',

        'url' => 0,

      ),

      'translatable' => '0',

      'type' => 'link_field',

    ),

    'field_instance' => array(

      'bundle' => 'breadcrumb2',

      'default_value' => NULL,

      'deleted' => '0',

      'description' => '',

      'display' => array(

        'default' => array(

          'label' => 'above',

          'module' => 'link',

          'settings' => array(),

          'type' => 'link_default',

          'weight' => 1,

        ),

      ),

      'entity_type' => 'breadcrumb2',

      'field_name' => 'field_breadcrumb_link',

      'label' => 'Breadcrumb Link',

      'required' => 0,

      'settings' => array(

        'attributes' => array(

          'class' => '',

          'configurable_title' => 0,

          'rel' => '',

          'target' => 'default',

          'title' => '',

        ),

        'display' => array(

          'url_cutoff' => '80',

        ),

        'enable_tokens' => 1,

        'title' => 'required',

        'title_maxlength' => '128',

        'title_value' => '',

        'url' => 'optional',

        'user_register_form' => FALSE,

        'validate_url' => 1,

      ),

      'widget' => array(

        'active' => 0,

        'module' => 'link',

        'settings' => array(),

        'type' => 'link_field',

        'weight' => '2',

      ),

    ),

  );

 

  // Translatables

  // Included for use with string extractors like potx.

  t('Breadcrumb Link');

 

  return $fields;

}

我把它改造了一下,放到了install文件里面了,我们这里并没有打算依赖于Features模块:

/**

 * Implements hook_install().

 */

function breadcrumb2_install() {

   // Add or remove the link field, as needed.

  $field = field_info_field('link');

  if (empty($field)) {

$field = array(

  'cardinality' => '-1',

      'entity_types' => array('breadcrumb2'),

      'field_name' => 'link',

      'module' => 'link',

      'type' => 'link_field',

    );

    $field = field_create_field($field);

  }

  $instance = field_info_instance('breadcrumb2', 'link', 'breadcrumb2');

  if (empty($instance)) {

    $instance = array(

      'bundle' => 'breadcrumb2',

      'default_value' => NULL,

      'deleted' => '0',

      'description' => '',

      'display' => array(

        'default' => array(

          'label' => 'above',

          'module' => 'link',

          'settings' => array(),

          'type' => 'link_default',

          'weight' => 1,

        ),

      ),

      'entity_type' => 'breadcrumb2',

      'field_name' => 'link',

      'label' => 'Breadcrumb Link',

      'required' => 0,

      'settings' => array(

        'attributes' => array(

          'class' => '',

          'configurable_title' => 0,

          'rel' => '',

          'target' => 'default',

          'title' => '',

        ),

        'display' => array(

          'url_cutoff' => '80',

        ),

        'enable_tokens' => 1,

        'title' => 'required',

        'title_maxlength' => '128',

        'title_value' => '',

        'url' => 'optional',

        'user_register_form' => FALSE,

        'validate_url' => 1,

      ),

      'widget' => array(

        'active' => 0,

        'module' => 'link',

        'settings' => array(),

        'type' => 'link_field',

        'weight' => '2',

      ),

    );

    $instance = field_create_instance($instance);

  }

}

    在卸载我们的模块时,我们还需要负责将其删除:

/**

 * Implements hook_uninstall().

 */

function breadcrumb2_uninstall() {

  $instance = field_info_instance('breadcrumb2', 'link', 'breadcrumb2');

  if (!empty($instance)) {

    field_delete_instance($instance);

  }

 

  $field = field_info_field('link');

  if ($field) {

    field_delete_field('link');

  }  

}

   注意这里面的field_info_fieldfield_create_fieldfield_info_instancefield_create_instancefield_delete_instancefield_delete_fieldDrupal API函数的用法。它们用来获取字段信息、创建一个字段、获取字段实例信息、创建一个字段实例、删除一个字段实例、删除一个字段。

我刚开始的时候,这样写的代码:

field_delete_field($field);

结果在卸载模块的时候,总是报错,卸载不了。后来才修正过来。所以用的时候要小心。要明白这个函数里面各个参数的具体含义。


Drupal版本:

7.17 面包屑本身功能实现

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

我们这个模块,是用来设置页面上面的面包屑的,开发了这么多,我们看看如果通过我们的面包屑实体来设置面包屑。向module文件添加以下代码:

/**

 * Implements hook_page_alter().

 */

function breadcrumb2_page_alter() {

  // See if current page has path breadcrumbs.

  $breadcrumbs = drupal_get_breadcrumb();

  $current_path = current_path();

  $breadcrumb2 = breadcrumb2_load_by_path($current_path);

  if(empty($breadcrumb2)){

    return;

  }

  //$breadcrumb2 = breadcrumb2_load(1);

  $wrapper = entity_metadata_wrapper('breadcrumb2', $breadcrumb2);

  $breadcrumb_links = $wrapper->link->value();

  foreach($breadcrumb_links as $breadcrumb_link){

    $breadcrumbs[]= l($breadcrumb_link['title'], $breadcrumb_link['url']);

  }

  //print debug($breadcrumb_links);

  // Set breadcrumbs for current page if it exists.

  if ($breadcrumbs) {

    drupal_set_breadcrumb($breadcrumbs);

  }

}

这里面我们实现了hook_page_alter钩子,这个方式借鉴于path_breadcrumb模块,当然,我们也可以通过hook_preprocess_page来设置, crumbs模块里面有这种方式的实现。我们来看看这个钩子函数里面的代码,首先,我们根据当前路径,获取到面包屑实体$breadcrumb2;这里面我们对$breadcrumb2使用entity_metadata_wrapper做了封装,这样我们就可以方便的读取到实体里面字段link的数组了,这样做的好处是,不用考虑语言问题;之后对$breadcrumb_links数组进行循环,将里面的链接追加到当前面包屑数组$breadcrumbs里面;最后使用drupal_set_breadcrumb设置面包屑。

接下来,来看一下函数breadcrumb2_load_by_path的实现代码:

/**

 * Fetch a breadcrumb object. 

 *

 * @param $path

 *   Internal path.

 * @return

 *   A fully-loaded $breadcrumb object or FALSE if it cannot be loaded.

 *

 * @see breadcrumb2_load_multiple()

 */

function breadcrumb2_load_by_path($path) {

  $breadcrumbs = breadcrumb2_load_multiple(FALSE, array('path' => $path));

  return reset($breadcrumbs);

}

我们直接将其委托给了breadcrumb2_load_multiple,把array('path' => $path)作为条件参数传递了过去。如果返回了面包屑对象数组,我们就使用reset将数组中的第一个对象返回。这里的逻辑非常简单,和breadcrumb2_load函数类似。不过越是简单的东西,有时候越难理解。我原来的想法是这样的,使用EntityFieldQuery,把我们的$path作为属性参数传递给它,这样EntityFieldQuery将会读取来一个包含bids的数组,我们把bids数组传递给breadcrumb2_load_multiple,获得一组面包屑对象,然后再使用reset获取第一个对象元素。

Entity API帮我们做了这些工作,为了更好的理解,我们反向追踪一下代码。

图片4.png


通过阅读里面的代码,我们最终找到了,SQL语句是在DrupalDefaultEntityController里面的buildQuery方法中动态构建的。有兴趣的可以看一下里面的代码。

到这里我们的面包屑实体类型就创建完毕了。当然,模块还没有写完。我们接着会写Views的集成、Rules的集成。只有当集成了Rules以后,我们这种构建面包屑的方式,才能


Drupal版本:

7.2 为实体定义Schema

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

和开发普通的模块一样,当需要存储数据的时候,我们需要定义自己的数据库表,这个时候,就需要定义Schema了。这个我们以前也讲过了,来看这里的定义。首先创建breadcrumb2.install文件,在里面输入以下内容:

<?php

 

/**

 * @file

 * Install, update and uninstall functions for the breadcrumb2 module.

 */

 

/**

 * Implements hook_schema().

 */

function breadcrumb2_schema() {

  $schema['breadcrumb'] = array(

    'description' => 'Stores breadcrumb items.',

    'fields' => array(

      'bid' => array(

        'type' => 'serial',

        'not null' => TRUE,

        'description' => t("'Primary Key: Unique breadcrumb item ID."),

      ),

    ),

    'primary key' => array('bid'),

  );

  return $schema;

}

最上面的文件的描述说明,那段文字是直接从profile2.install里面复制过来的,下面的breadcrumb2_schema也是从那边复制过来的,只不过我们根据自己的需要做了修改。面包屑,这个实体比较简单,本来我想在这里定义一个属性path的,但是一想,自己需要为它创建一个表单元素,负责它的编辑、验证、存储,感觉有点麻烦,所以直接将它交给Field系统了。所以数据库表breadcrumb的结构非常简单,就一个bid主键,用来关联字段的,其它什么属性也没有定义。简单就好。

我们采用这样的策略,首先,定义好实体,然后向实体上面添加两个字段pathlink,之后将这两个字段的定义使用Features导出来,然后把里面的定义代码复制到我们的模块里面,这样我们在安装的时候,就直接为我们的面包屑实体创建好字段了。一步一步来。


Drupal版本:

7.3 实现hook_entity_info

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

接下来,我们创建breadcrumb2.module文件,首先输入以下代码:

<?php

 

/**

 * @file

 * Support for configurable breadcrumbs.

 */

   接着,实现hook_entity_info()

/**

 * Implement hook_entity_info().

 */

function breadcrumb2_entity_info() {

  $return = array(

    'breadcrumb2' => array(

      'label' => t('Breadcrumb'),

      'plural label' => t('Breadcrumbs'),

      'description' => t('Breadcrumb2 entity breadcrumbs.'),

      'entity class' => 'Breadcrumb',

      'controller class' => 'EntityAPIController',

      'base table' => 'breadcrumb',

      'fieldable' => TRUE,

      'view modes' => array(

        'full' => array(

          'label' => t('Breadcrumb'),

          'custom settings' => FALSE,

        ),

      ),

      'entity keys' => array(

        'id' => 'pid',

      ),

      'bundles' => array(

        'breadcrumb2' => array(

          'label' => t('Breadcrumb'),

          'admin' => array(

            'path' => 'admin/structure/breadcrumbs',

            'access arguments' => array('administer breadcrumbs'),

          ),

        ),

      ),

      'bundle keys' => array(

        'bundle' => 'type',

      ),

      'uri callback' => 'entity_class_uri',

      'access callback' => 'breadcrumb2_access',

      'module' => 'breadcrumb2',

      'metadata controller class' => 'Breadcrumb2MetadataController'

    ),

  );

  return $return;

}

首先,这个钩子的实现,我们主要参考了profile2_entity_info的实现,这里是以它为基础做的修改。其次,由于我们这里只有一个bundle,并且不允许创建其它的bundle,这个和user实体非常类似,所以中间的部分代码,我们借鉴的是user_entity_info的实现,我们来看一下user模块的实现:

function user_entity_info() {

  $return = array(

    'user' => array(

      'label' => t('User'),

      'controller class' => 'UserController',

      'base table' => 'users',

      'uri callback' => 'user_uri',

      'label callback' => 'format_username',

      'fieldable' => TRUE,

      // $user->language is only the preferred user language for the user

      // interface textual elements. As it is not necessarily related to the

      // language assigned to fields, we do not define it as the entity language

      // key.

      'entity keys' => array(

        'id' => 'uid',

      ),

      'bundles' => array(

        'user' => array(

          'label' => t('User'),

          'admin' => array(

            'path' => 'admin/config/people/accounts',

            'access arguments' => array('administer users'),

          ),

        ),

      ),

      'view modes' => array(

        'full' => array(

          'label' => t('User account'),

          'custom settings' => FALSE,

        ),

      ),

    ),

  );

  return $return;

}

我们的'entity keys''bundles''view modes',都是从user_entity_info借鉴过来的。所谓借鉴,就是将它们的代码复制过来,然后改成我们自己的。比葫芦画瓢。比如'entity keys',最初这个是从profile2中借鉴过来的,profile2_entity_info中这样定义的:

      'entity keys' => array(

        'id' => 'pid',

        'bundle' => 'type',

        'label' => 'label',

      ),

我们知道,pidprofile里面的主键,我们将它修改为bid,就成了最初的样子:

      'entity keys' => array(

        'id' => 'bid',

        'bundle' => 'type',

        'label' => 'label',

      ),

 

然后再借鉴一下user里面的实现,这个时候,我们会发现里面的'bundle''label'没有什么用,把它们删除,就成了现在的样子:

      'entity keys' => array(

        'id' => 'pid',

      ),

Drupal里面的很多钩子,尤其是这种带有info后缀的钩子,里面通常是一个大的数组,遇到这样的钩子,我们学习的路径,最好是找个类似的实现作为参考,当然我们还需要阅读这个钩子的文档,弄清楚里面的键值的具体含义,不过大部分键的含义,从字面上很容易理解出来,比如这里的'label''plural label''description''entity class''controller class''base table''fieldable'等。


Drupal版本:

7.4 实现Hook_menu

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

我们在hook_entity_info里面,有这样的设置“'path' => 'admin/structure/breadcrumbs'”

这里所用的路径,我们需要在hook_menu里面定义一下,现在我们就来实现这个路径。向module文件中添加以下代码:

/**

 * Implements hook_menu().

 */

function breadcrumb2_menu() {

  $items['admin/structure/breadcrumbs'] = array(

    'title' => 'Breadcrumbs',

    'description' => 'Manage breadcrumbs.',

    'page callback' => 'breadcrumb2_overview_breadcrumbs',

    'access arguments' => array('administer breadcrumbs'),

    'file' => 'breadcrumb2.admin.inc',

  );

  $items['admin/structure/breadcrumbs/list'] = array(

    'title' => 'List',

    'type' => MENU_DEFAULT_LOCAL_TASK,

    'weight' => -10,

  );

}

注意,我们必须把admin/structure/breadcrumbs/list指定为MENU_DEFAULT_LOCAL_TASK,这样Field模块可以在admin/structure/breadcrumbs路径后面添加两个子标签,管理字段、管理显示。

接着创建breadcrumb2.admin.inc文件,向里面添加我们的回调函数breadcrumb2_overview_breadcrumbs

<?php

 

/**

 * @file

 * Breadcrumb administration and module settings UI.

 *

 */

 

/**

 * Displays the breadcrumb admin overview page.

 */

function breadcrumb2_overview_breadcrumbs(){

  $build['#markup'] = t('Breadcrumb2 overview breadcrumbs');

  return $build;

}

这里面还没有逻辑代码。我们只是想快速的实现一个骨架,然后再逐步的完善里面具体的细节。


Drupal版本:

7.5 权限与访问控制

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

在我们的菜单项里面,用到了权限'administer breadcrumbs',现在就让我们定义这个权限,向module文件中添加以下代码:

/**

 * Implements hook_permission().

 */

function breadcrumb2_permission() {

  $permissions = array(

    'administer breadcrumbs' => array(

      'title' => t('Administer breadcrumbs'),

      'description' => t('Edit and view all entity breadcrumbs.'),

    ),  

  );

  return $permissions;  

}

面包屑也有增删查改,但是这些工作都是由管理员来做的,所以我们这里比较省事,就定义了一个'administer breadcrumbs'权限,其它的都省去了。

hook_entity_info里面,有这样的定义'access callback' => 'breadcrumb2_access',这里的breadcrumb2_access,就是一个访问控制回调函数,来看一下我们的实现:

 

/**

 * Determines whether the given user has access to a breadcrumb.

 *

 * @param $op

 *   The operation being performed. One of 'view', 'update', 'create', 'delete'

 *   or just 'edit' (being the same as 'create' or 'update').

 * @param $breadcrumb

 *   Optionally a breadcrumb to check access for. If nothing is

 *   given, access for all breadcrumbs is determined.

 * @param $account

 *   The user to check for. Leave it to NULL to check for the global user.

 * @return boolean

 *   Whether access is allowed or not.

 */

function breadcrumb2_access($op, $breadcrumb = NULL, $account = NULL) {

  if (user_access('administer breadcrumbs', $account)) {

    return TRUE;

  }

  return FALSE;

}

对于比较复杂的实体,比如profile2,这个访问回调里面还会定义一个新的钩子hook_profile2_access

 

function profile2_access($op, $profile = NULL, $account = NULL) {

  if (user_access('administer profiles', $account)) {

    return TRUE;

  }

  if ($op == 'create' || $op == 'update') {

    $op = 'edit';

  }

  // Allow modules to grant / deny access.

  $access = module_invoke_all('profile2_access', $op, $profile, $account);

 

  // Only grant access if at least one module granted access and no one denied

  // access.

  if (in_array(FALSE, $access, TRUE)) {

    return FALSE;

  }

  elseif (in_array(TRUE, $access, TRUE)) {

    return TRUE;

  }

  return FALSE;

}

node_access里面,也定义了类似的钩子:

$access = module_invoke_all('node_access', $node, $op, $account);

 

当然,我们这里不需要定义一个hook_breadcrumb2_access,所以我们的就比较简单。


Drupal版本:

7.6 加载、删除、创建、保存实体

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

对于定义的实体类型,我们最好为其实现增删改查等API函数,方便别处调用:

{entity_type}_load(),

{entity_type}_create(),

{entity_type}_save(),

{entity_type}_delete_multiple(),

Entity API模块,为我们提供了entity_create()、 entity_save()、 entity_delete()等函数,直接使用Entity API里面的即可,我们还有必要定义自己的么?最好还是定义一下,也就是有这个必要。我们来看看我们的实现:

/**

 * Fetch a breadcrumb object. 

 *

 * @param $bid

 *   Integer specifying the breadcrumb id.

 * @param $reset

 *   A boolean indicating that the internal cache should be reset.

 * @return

 *   A fully-loaded $breadcrumb object or FALSE if it cannot be loaded.

 *

 * @see breadcrumb2_load_multiple()

 */

function breadcrumb2_load($bid, $reset = FALSE) {

  $breadcrumbs = breadcrumb2_load_multiple(array($bid), array(), $reset);

  return reset($breadcrumbs);

}

 

 

/**

 * Load multiple breadcrumbs based on certain conditions.

 *

 * @param $bids

 *   An array of breadcrumb IDs.

 * @param $conditions

 *   An array of conditions to match against the {breadcrumb} table.

 * @param $reset

 *   A boolean indicating that the internal cache should be reset.

 * @return

 *   An array of breadcrumb objects, indexed by bid.

 *

 * @see entity_load()

 * @see breadcrumb2_load()

 */

function breadcrumb2_load_multiple($bids = array(), $conditions = array(), $reset = FALSE) {

  return entity_load('breadcrumb2', $bids, $conditions, $reset);

}

 

/**

 * Deletes a breadcrumb.

 */

function breadcrumb2_delete(Breadcrumb $breadcrumb) {

  $breadcrumb->delete();

}

 

 

/**

 * Delete multiple breadcrumbs.

 *

 * @param $bids

 *   An array of breadcrumb IDs.

 */

function breadcrumb2_delete_multiple(array $bids) {

  entity_get_controller('breadcrumb2')->delete($bids);

}

 

 

/**

 * Create a breadcrumb object.

 */

function breadcrumb2_create($values = array()) {

  return new Breadcrumb($values);

}

 

 

/**

 * Saves a breadcrumb to the database.

 *

 * @param $breadcrumb

 *   The breadcrumb object.

 */

function breadcrumb2_save(Breadcrumb $breadcrumb) {

  return $breadcrumb->save();

}

这都是模式化的代码,对应的实现,可以参考profile2,也可以参考model模块。


Drupal版本:

7.7 定义实体类

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

我们在breadcrumb2_entity_info里面,将'entity class'指定为了Breadcrumb,现在我们来看看这个类的定义。向breadcrumb2.module里面添加以下代码:

/**

 * The class used for breadcrumb entities

 */

class Breadcrumb extends Entity {

  

  public function __construct($values = array()) {

    parent::__construct($values, 'breadcrumb');

  }

 

  protected function defaultLabel() {

    return $this->path;

  }

 

  protected function defaultUri() {

    return array('path' => 'breadcrumb/' . $this->bid);

  }

}

  类似的代码可以参看model里面的Model类的实现。

   

  我们将'controller class'设置为了EntityAPIController,当然,我们也可以有自己的实现,比如将其设置为BreadcrumbController。此时需要定义这个控制器类,我们向module文件追加以下代码:

 

/**

 * The Controller for Breadcrumb entities

 */

class BreadcrumbController extends EntityAPIController {

  public function __construct($entityType) {

    parent::__construct($entityType);

  }

 

  /**

   * Create a breadcrumb - we first set up the values that are specific

   * to our breadcrumb schema but then also go through the EntityAPIController

   * function.

   * 

   * @param $type

   *   The machine-readable type of the breadcrumb.

   *

   * @return

   *   A breadcrumb object with all default fields initialized.

   */

  public function create(array $values = array()) {

    // Add values that are specific to our Breadcrumb

    $values += array( 

      'bid' => '',

    );

    

    $breadcrumb = parent::create($values);

    return $breadcrumb;

  }

  

这里也是模仿的model模块的实现。


Drupal版本:

7.8 元数据控制器类

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

我们将'metadata controller class'设置为了Breadcrumb2MetadataController,对于这个类型,我们将它放到了breadcrumb2.info.inc文件中了。来看一下它的代码:

 

<?php

 

/**

 * @file

 * Provides Entity metadata integration.

 */

 

/**

 * Extend the defaults.

 */

class Breadcrumb2MetadataController extends EntityDefaultMetadataController {

 

  public function entityPropertyInfo() {

    $info = parent::entityPropertyInfo();

    $properties = &$info[$this->type]['properties'];

 

    return $info;

  }

}

我们这里面,这个类是空类,里面其实没有自己的实现。我们是模仿profile2的,它的这个元数据控制器类,是这样定义的:

class Profile2MetadataController extends EntityDefaultMetadataController {

 

  public function entityPropertyInfo() {

    $info = parent::entityPropertyInfo();

    $properties = &$info[$this->type]['properties'];

 

    $properties['label'] = array(

      'label' => t('Label'),

      'description' => t('The profile label.'),

      'setter callback' => 'entity_property_verbatim_set',

      'setter permission' => 'administer profiles',

      'schema field' => 'label',

    );

    return $info;

  }

}

 

/**

 * Implements hook_entity_property_info_alter().

 */

function profile2_entity_property_info_alter(&$info) {

我们的面包屑里面,除了bid,不包含其它属性,同时只有一个bundle,我们这里可能就不需要定义这个元数据控制器类,只需要使用Entity API提供的默认的EntityDefaultMetadataController即可。

另外需要注意的是,在这个info.inc文件里面可以定义实现钩子hook_entity_property_info_alter。如果这个钩子实现可以放在这里的话,那么也可以在这里实现hook_entity_property_info,这是我的猜测。这样做的目的也是为了减小module文件的大小。


Drupal版本:

7.9 调试代码

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

一切工作准备就绪,让我们启用Breadcrumb2模块,我们需要预先安装好它所依赖的模块。都准备好了。启用好模块以后,没有报错,接着我们访问admin/structure,在这里我们并没有找到我们的面包屑:

图片1.png 

而管理字段、管理显示两个标签却显示了出来,点击这两个标签,发现它们的路径分别为admin/structure/breadcrumbs/fieldsadmin/structure/breadcrumbs/display。功能和我们在其它地方看到的一样。admin/structure/breadcrumbs哪里去了?

我们回过头来检查代码,发现我们在breadcrumb2_menu少写下面一句代码:

return $items;

一个很低级的错误。我们把它补上。我最初开发的时候,除了这个问题,还遇到了找不到Breadcrumb2MetadataController类的问题。因为我没有创建breadcrumb2.info.inc文件,也没有添加Breadcrumb2MetadataController的实现。我是在发现admin/structure/breadcrumbs不存在这个问题以后,清空缓存后,整个网站无法访问了,并且提示Breadcrumb2MetadataController不存在。这个时候,我才添加的breadcrumb2.info.inc文件。

现在清除缓存,admin/structure/breadcrumbs显示出来了:

图片2.png


Drupal版本:

8 EntityFieldQuery

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

在没有使用EntitiFieldQuery之前,我一直都是采用db_select的形式,这个我们在前面已经讲过了。我们先来看一个实际的例子,这段代码主要是我写的:

<?php

/**

 * @file

 * Field validation unique validator.

 *

 */

$plugin = array(

  'label' => t('Unique values'),

  'description' => t('Verifies that all values are unique in current entity or bundle.'),

  'handler' => array(

    'class' => 'field_validation_unique_validator',

  ),

);

 

class field_validation_unique_validator extends field_validation_validator {

 

  /**

   * Validate field. 

   */

  public function validate() {

    $flag = TRUE;

    $scope = $this->rule->settings['data'];

    $count = 0;

    foreach ($this->items as $delta1 => $item1) {

      if ($this->delta != $delta1) {

        if ($this->value == $item1[$this->rule->col]) {

          $flag = FALSE;

          break;

        }

      }

    }

    if ($flag) {

      $query = new EntityFieldQuery();

      if ($scope == 'global') {

      }

      elseif ($scope == 'entity') {

        $query->entityCondition('entity_type', $this->rule->entity_type);

      }

      elseif ($scope == 'bundle') {

        $query->entityCondition('entity_type', $this->rule->entity_type);

        $query->entityCondition('bundle', $this->rule->bundle);

      }

 

      list($id, $vid, $bundle) = entity_extract_ids($this->rule->entity_type, $this->entity);

      if ($this->rule->entity_type == 'user' && arg(0) =='user' && arg(2) =='edit' && empty($id)) {

        $id = arg(1);

      }

      if ($this->rule->entity_type == 'field_collection_item' && arg(0) == 'field-collection' && arg(3) =='edit' && empty($id)) {

        $id = arg(2); 

      }

      if ($this->rule->entity_type == 'profile2' && empty($id)) {

        $arg_index = 1;

        if (module_exists('profile2_page')) {

          $profile_type = profile2_type_load($this->entity->type);

          $path = profile2_page_get_base_path($profile_type);

          $arg_index = count(explode('/', $path));

        }

        $uid = arg($arg_index);

        if (arg($arg_index + 1) == 'edit' && is_numeric($uid) && $account = user_load($uid)) {

          if ($profile = profile2_load_by_user($account, $this->entity->type)) {

            $id = $profile->pid;

          }

        }

      }

      if (!empty($id)) {

        $query->entityCondition('entity_id', $id, '!=');

      }

      //Always bypass all access checkings.

      $query->addMetaData('account', user_load(1));

      $query->fieldCondition($this->rule->field_name, $this->rule->col, $this->value);

 

      // Store a copy of our matched entities for our use in tokens later.

      $matched_entities = $query->execute();

 

      $count = $query

        ->count()

        ->execute();

      if ($count) {

        $flag = FALSE;

 

      }

 

    }

 

    if (!$flag) {

      $token = array(

        '[count]' => $count,

      );

 

      // Find the first entity that failed this unique condition so we can

      // add a token referencing it. First, we have some special handling for

      // field collection entities so we can find the entity title of

      // whatever the specific field is connected to.

      $entity_types = array_keys($matched_entities);

      $entity_type = reset($entity_types);

      $matched_entity = reset($matched_entities);

      $first_match = reset($matched_entity);

      $entity_info = entity_get_info($entity_type);

      $entity_key_id = $entity_info['entity keys']['id'];

      $entitys = entity_load($entity_type, array($first_match->{$entity_key_id}));

      $entity = reset($entitys);

 

      if ($entity_type == 'field_collection_item') {

        $host_type = $entity->hostEntityType();

        $host_entity = $entity->hostEntity();

        $label = entity_label($host_type, $host_entity);

        $uri = entity_uri($host_type, $host_entity);

      }

      else {

        $label = entity_label($entity_type, $entity);

        $uri = entity_uri($entity_type, $entity);

      }

 

      $token['[existing-entity-label]'] = $label;

      $token['[existing-entity-link]'] = l($label, $uri['path'], $uri['options']);

 

      $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']['data'] = array(

      '#title' => t('Scope of unique'),

      '#description' => t("Specify the scope of unique values, support: global, entity, bundle."),

      '#type' => 'select',

      '#options' => array(

        'global' => t('Global'),

        'entity' => t('Entity'),

        'bundle' => t('Bundle'),

      ),  

      '#default_value' => isset($default_settings['data']) ? $default_settings['data'] : '',

    );

    parent::settings_form($form, $form_state);

  }

  

  /**

   * Provide token help info for error message.

   */

  public function token_help() {

    $token_help = parent::token_help();

    $token_help += array(

      '[count]' => t('Count of duplicate'),

      '[existing-entity-label]' => t('The label of the first entity that contains matching data.'),

      '[existing-entity-link]' => t('A link to the first entity that contains matching data.'),

    );

    return $token_help;

  }

}

这里面涵盖了EntitiFieldQuery的常见用法,首先,我们使用下面的语句,初始化查询语句:

$query = new EntityFieldQuery();


Drupal版本:

8.1条件语句

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

接着,可以为它添加一些条件语句,比如使用entityCondition添加实体本身相关的条件语句:

      $query->entityCondition('entity_type', $this->rule->entity_type);

      $query->entityCondition('bundle', $this->rule->bundle);

      $query->entityCondition('entity_id', $id, '!=');

注意,这里面entity_id,也属于entityCondition的范畴。函数的结构定义是这样的:

public function entityCondition($name, $value, $operator = NULL)

$name,这里的允许值有'entity_type', 'bundle', 'revision_id' 'entity_id',注意有些实体,比如评论、分类术语,它们不能使用'bundle'$value就是当前值,$operator是操作符,默认使用“=”,可用的操作符有'=', '<>', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS', 'IN', 'NOT IN', 'BETWEEN'。注意,后面三个操作符都是作用于数组的。

 

也可以使用fieldCondition添加字段相关的条件语句:

$query->fieldCondition($this->rule->field_name, $this->rule->col, $this->value);

这是我们这个例子里面用到的。fieldCondition的结构定义是这样的:

public function fieldCondition($field, $column = NULL, $value = NULL, $operator = NULL, $delta_group = NULL, $language_group = NULL)

    这里的参数比较多,常用的就是前面四个,后面的我从来没有用过。

 

property_validation模块里面的property_validation_unique_validator,还有属性相关的条件语句:

$query->propertyCondition($this->rule->property_name, $this->value) ;

这个成员函数的结构是这样的:

public function propertyCondition($column, $value, $operator = NULL)

$column就是属性名字,$value就是当前值, $operator就是操作符。

 

获取查询的结果

加了这么多条件语句以后,使用execute()方法就可以获取查询的结果:

$matched_entities = $query->execute();

当然,我们需要注意,返回的结果的数据结构,通常对于返回的结果,进行这样的处理:

        foreach ($matched_entities as $entity_type => $entities) {

          foreach ($entities as $entity_id => $entity) {

            // 这里进行操作

          }

        }

如果我们这里已经知道了实体的类型,则可以这样处理:

    $result = $query->execute();

    $entities = entity_load($my_type, array_keys($result[$my_type]));

由于EntitiFieldQuery里面获取到的实体对象,仅仅是一个壳子,所以,我们还需要使用entity_load来加载它。我们在例子中,为了获取到第一个匹配到的实体,这样处理的:

      $entity_types = array_keys($matched_entities);

      $entity_type = reset($entity_types);

      $matched_entity = reset($matched_entities);

      $first_match = reset($matched_entity);

      $entity_info = entity_get_info($entity_type);

      $entity_key_id = $entity_info['entity keys']['id'];

      $entitys = entity_load($entity_type, array($first_match->{$entity_key_id}));

      $entity = reset($entitys);

这里使用多次使用reset,用来获取一个数组里面的第一个元素,注意,不能够将array_keys($matched_entities)直接传递给reset,如果这样做的话,会提示有语法问题,但是结果仍然是正确的。

使用Count()获取结果记录总数

我们这里的例子是这样的:

 $count = $query->count()->execute();

另外,这里面需要说明的是,这些成员函数之间,是可以链式调用的。


Drupal版本:

9 使用addMetaData向查询添加元数据信息

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

我们这里要实现的是,查询不受权限的影响,这里通过addMetaData方法,添加一个用户1,这样就可以以用户1的身份来执行查询语句了,这样的好处就是可以跳过所有的权限检查。这是addMetaData的一个用法。

$query->addMetaData('account', user_load(1));

实际当中其它常用的方法

   排序方法,有三个可用,分别为:

public function entityOrderBy($name, $direction = 'ASC') {}

public function fieldOrderBy($field, $column, $direction = 'ASC') {}

public function propertyOrderBy($column, $direction = 'ASC') {}

   排序的方向默认是按照升序'ASC'进行的,如果需要降序的话,则可以使用"DESC"

 

  范围查询,可以使用: 

 public function range($start = NULL, $length = NULL) {}

 

   为查询启用分页功能,则可以使用:

  public function pager($limit = 10, $element = NULL) {}


Drupal版本:

6完善的API函数

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

这个模块提供了更多的API函数,比如entity_create()、 entity_save()、 entity_delete()、 entity_view()、 entity_access(),这些函数可以用来方便的创建、保存、删除、查看实体,最后一个entity_access用来控制访问权限的。除了这几个以外,还有entity_id()、 entity_export()、 entity_import()entity_get_property_info()entity_id()是用来获取id的,entity_export负责导出,entity_import负责导入;entity_get_property_info是用来做什么的?它是用来获取实体上面的属性信息的,注意这里的属性,和Drupal核心里面的属性是不一样的概念,在Drupal7核心里面,属性是属性,字段是字段,而在Entity API模块里面,属性、字段都是属性。

Drupal版本:

6.1 hook_entity_property_info

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

Entity API模块定义了一个新的钩子,hook_entity_property_info(),通过这个钩子函数,就可以定义实体包含哪些属性信息了,这个信息里面还包括,属性的数据类型、获取、设置属性的回调函数。注意,很多常用的模块,都用到了这里定义的属性,比如Rules模块。而前面的entity_get_property_info,则是用来获取hook_entity_property_info()里面的定义信息的。这个钩子函数比较抽象,我们可以看一个例子,在sites\all\modules\entity\modules下面可以找到,Drupal核心中各个子系统的钩子实现。下面这个是用户系统的实现:

/**

 * Implements hook_entity_property_info() on top of user module.

 *

 * @see entity_entity_property_info()

 */

function entity_metadata_user_entity_property_info() {

  $info = array();

  // Add meta-data about the user properties.

  $properties = &$info['user']['properties'];

 

  $properties['uid'] = array(

    'label' => t("User ID"),

    'type' => 'integer',

    'description' => t("The unique ID of the user account."),

    'schema field' => 'uid',

  );

  $properties['name'] = array(

    'label' => t("Name"),

    'description' => t("The login name of the user account."),

    'getter callback' => 'entity_metadata_user_get_properties',

    'setter callback' => 'entity_property_verbatim_set',

    'sanitize' => 'filter_xss',

    'required' => TRUE,

    'access callback' => 'entity_metadata_user_properties_access',

    'schema field' => 'name',

  );

下面的这段则是节点系统的实现:

  $properties['created'] = array(

    'label' => t("Date created"),

    'type' => 'date',

    'description' => t("The date the node was posted."),

    'setter callback' => 'entity_property_verbatim_set',

    'setter permission' => 'administer nodes',

    'schema field' => 'created',

  );

  $properties['changed'] = array(

    'label' => t("Date changed"),

    'type' => 'date',

    'schema field' => 'changed',

    'description' => t("The date the node was most recently updated."),

  );

  $properties['author'] = array(

    'label' => t("Author"),

    'type' => 'user',

    'description' => t("The author of the node."),

    'setter callback' => 'entity_property_verbatim_set',

    'setter permission' => 'administer nodes',

    'required' => TRUE,

    'schema field' => 'uid',

  );

除了这些实体系统以外,字段系统也都被包含了进来,不过字段系统的代码比较抽象:

/**

 * Implements hook_entity_property_info() on top of field module.

 *

 * @see entity_field_info_alter()

 * @see entity_entity_property_info()

 */

function entity_metadata_field_entity_property_info() {

  $info = array();

  // Loop over all field instances and add them as property.

  foreach (field_info_fields() as $field_name => $field) {

    $field += array('bundles' => array());

    if ($field_type = field_info_field_types($field['type'])) {

      // Add in our default callback as the first one.

      $field_type += array('property_callbacks' => array());

      array_unshift($field_type['property_callbacks'], 'entity_metadata_field_default_property_callback');

 

      foreach ($field['bundles'] as $entity_type => $bundles) {

        foreach ($bundles as $bundle) {

          $instance = field_info_instance($entity_type, $field_name, $bundle);

 

          if ($instance && empty($instance['deleted'])) {

            foreach ($field_type['property_callbacks'] as $callback) {

              $callback($info, $entity_type, $field, $instance, $field_type);

            }

          }

        }

      }

    }

  }

  return $info;

}

 

/**

 * Callback to add in property info defaults per field instance.

 * @see entity_metadata_field_entity_property_info().

 */

function entity_metadata_field_default_property_callback(&$info, $entity_type, $field, $instance, $field_type) {

  if (!empty($field_type['property_type'])) {

    if ($field['cardinality'] != 1) {

      $field_type['property_type'] = 'list<' . $field_type['property_type'] . '>';

    }

    // Add in instance specific property info, if given and apply defaults.

    $name = $field['field_name'];

    $property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$name];

    $instance += array('property info' => array());

    $property = $instance['property info'] + array(

      'label' => $instance['label'],

      'type' => $field_type['property_type'],

      'description' => t('Field "@name".', array('@name' => $name)),

      'getter callback' => 'entity_metadata_field_property_get',

      'setter callback' => 'entity_metadata_field_property_set',

      'access callback' => 'entity_metadata_field_access_callback',

      'query callback' => 'entity_metadata_field_query',

      'translatable' => !empty($field['translatable']),

      // Specify that this property stems from a field.

      'field' => TRUE,

      'required' => !empty($instance['required']),

    );

    // For field types of the list module add in the options list callback.

    if (strpos($field['type'], 'list') === 0) {

      $property['options list'] = 'entity_metadata_field_options_list';

    }

  }

}

Drupal核心系统,默认是不支持这个钩子的,但是Entity API模块把核心系统里面的所有涉及到的部分,都实现出来了。这里是包括所有的Drupal核心的字段类型的。我们在这里,前期不需要完全明白这些代码的含义,大致看一下,知道有这么回事就可以了,我们只需要记住,这里面,在属性数组信息里面,包含哪些常用的键就可以了。'label''description''getter callback''setter callback''sanitize''required''access callback''schema field'。有兴趣的话,可以沿着我们提示,把sites\all\modules\entity\modules下面,所有inc文件里面的代码都阅读一遍。注意,verbatim的中文意思为完全)照字面的(地),逐字的(地),里面的代码会遇到这个单词。


Drupal版本:

6.2 entity_metadata_wrapper

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

Entity API模块,还提供了一个元数据封装器函数,entity_metadata_wrapper,通过这个函数的封装,我们可以更加方便的访问实体及其属性。这个函数给我们带来了哪些便利呢?我们通过代码示例来了解一下。

 

比如,获取节点作者的电子邮件地址,可以使用下面的代码:

$wrapper = entity_metadata_wrapper('node', $node);

$wrapper->author->mail->value();

我们在这里看到,封装过后的这个对象,在它上面,可以采用链式调用,此外它能够直接的建立关联关系。对于上面的功能,如果不使用封装器的话,我们通常采用这样的代码:

$uid = $node->uid;

$user = user_load($uid);

$mail = $user->mail;

这里,我们的代码也并不复杂,看不出来entity_metadata_wrapper的优越性。据文档里面说,$wrapper->author->mail->value();这段代码,可以省去user_load这个操作,直接通过关联关系获取到用户的电子邮件地址,这样可能会有一点小的性能提升。但是这也是有代价的,代价就是调用了entity_metadata_wrapper通过这个例子,我们可以看到这个函数的基本用法。我们在前面让大家阅读代码的原因,就是为这里做准备的,为什么用的是$wrapper->author?而没有用$wrapper->uid,也没有用$wrapper->user?因为在hook_entity_property_info里面定义的是author

 

为了更新用户的电子邮件地址,我们可以使用下面的代码:

$wrapper->author->mail->set('test@test.com');

或者

$wrapper->author->mail = 'test@test.com';

 

换成我们熟悉的代码,则是:

$user->mail =  'test@test.com';

user_save($user);

 

为了获取电子邮件的属性信息,可以使用下面的代码:

$mail_info = $wrapper->author->mail->info();

 

获取过滤了的节点标题:

$wrapper->title->value(array('sanitize' => TRUE));

 

获取原始数据:

$wrapper->body->value->raw();

 

上面的这几个例子都比较简单,我们看一个复杂的:

$wrapper->author->profile->field_name->value();

$wrapper->author->profile->field_name->set('新名字');

在这里,从节点,到节点的作者,再到作者的profile,再到profile上面的字段field_name,上面的一句,用来获取field_name字段的值,下面一句用来为该字段设置一个新值。如果我们自己写代码的话,需要加载用户对象,需要加载profile2对象,访问一个字段的值时,需要知道使用哪个语言来访问,代码写起来就比较复杂了。但是,在这里,一行代码就搞定了,非常方便。最后,我们再多看几个示例用法:

$wrapper->language('de')->body->summary->value();

 

$wrapper->author->mail->access('edit') ? TRUE : FALSE;

$wrapper->author->roles->optionsList();

 

$wrapper->field_files[0]->description = 'The first file';

 

$wrapper->save();

$node = $wrapper->value();


Drupal版本:

6.3 简化了实体类型的定义

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

这个模块还提供了一个Entity对象,如果我们要定义一个新的实体类型时,只需要继承这个对象就可以了,这个对象帮助我们实现了完整的CRUD操作。

Entity API里面,所有的bundle,这里说的是Bundle本身,也都被处理成为了实体,这是一种可以导入、导出的实体。在Drupal核心中,内容类型的添加、编辑,是没有采用实体的形式的。Entity API模块,在这里走的更远,Bundle本身也处理成为了实体。我们看Profile2模块,在profile2_entity_info里面,可以找到这段代码:

  $return['profile2_type'] = array(

    'label' => t('Profile type'),

    'plural label' => t('Profile types'),

    'description' => t('Profiles types of Profile2 user profiles.'),

    'entity class' => 'ProfileType',

    'controller class' => 'EntityAPIControllerExportable',

    'base table' => 'profile_type',

    'fieldable' => FALSE,

    'bundle of' => 'profile2',

    'exportable' => TRUE,

    'entity keys' => array(

      'id' => 'id',

      'name' => 'type',

      'label' => 'label',

    ),

    'access callback' => 'profile2_type_access',

    'module' => 'profile2',

    // Enable the entity API's admin UI.

    'admin ui' => array(

      'path' => 'admin/structure/profiles',

      'file' => 'profile2.admin.inc',

      'controller class' => 'Profile2TypeUIController',

    ),

  );

通过这段代码,把Profile类型也处理成为实体了。对于这种包括Bundle类型的实体,Entity API还提供了一个管理界面,我们只需要对这个默认的管理界面进行扩展,就可以了。这里使用EntityDefaultUIController来管理默认的界面,Profile2TypeUIController则继承了EntityDefaultUIController

如果我们基于Entity API来定义实体类型的话,此时与ViewsRules的集成都比较友好。Entity API模块提供了Views的基本集成,而Rules模块则是Fago的另一个杰作,本身就是基于Entity API模块的。


Drupal版本:

第2章 Views 集成

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

一个好的Drupal模块,通常能够充分的利用已有模块的各种功能,而不是重复的发明轮子。当我们打算实现这个面包屑模块的时候,有一个目标就是尽量的简单易用、代码精炼,充分的利用常用的第3方模块,能够帮助我们实现这个目标。我们这里主要集成ViewsRules,顺带集成Field Validation。前面两个都是社区内,排名非常靠前的模块,Field Validation就是老葛写的,集成一下自己写的模块,对于推广使用Field Validation也是很有帮助的。


Drupal版本:

1 Field Validation集成

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

Field validation的集成相对简单一点,而且我本人也非常的熟悉。从Field validation2.1开始,里面自带一个子模块,叫做property validation,我们这里集成的其实是property validation。一个实体通常由两部分组成,字段(Field)和属性(property),注意在Entity API里面将两者都统一为了属性;Field validation负责字段验证,property validation负责属性验证,将两者统一起来,是我的一个目标。我们这里主要想为path这个属性,添加一些验证,比如保证它是唯一的,保证这个路径在内部确实存在,不加这些验证,也能够正常工作。


Drupal版本:

10 在info文件里面注册我们的类

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

清空缓存,向Views里面添加新的字段,此时就会看到我们这里定义的字段了:

2.jpg 

选中,上面的删除、编辑链接,点击添加并配置字段按钮,但是我们却得到了这样的页面:

2.jpg 

我多次清除缓存,都不起作用,包括到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文件里面注册一下。

将配置好的Views导出到代码里面


Drupal版本:

11 将配置好的Views导出到代码里面

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

    由于前面,我们新增了编辑、删除两个链接,所以我们可以将字段“Breadcrumb: Breadcrumb ID (Breadcrumb ID)”“Global: Custom text (Edit)”删除掉了。同时,将每页显示的10条,改为显示30条;设置无结果行为,无结果时显示信息“No breadcrumbs available.”;然后保存视图。

    我们在实际的项目中,也经常需要将配置好的Views导出成代码的形式点击这里的导出链接即可图片1.png

 

系统会为我们自动生成好代码。如下图所示:

图片2.png

 

我们将这里的代码复制下来,然后打开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;


Drupal版本:

12 导出的views放到breadcrumb2.views_default.inc中

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

前面讲到,我们在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就可以这样放,我的就不可以。这个问题,我们就不深究了。


Drupal版本:

13 Field Collection Views代码分析

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

我们这章,就主要讲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_infohook_field_formatter_viewhook_field_formatter_settings_formhook_field_formatter_settings_summary,这四个钩子函数,是定制格式器时,需要实现的四个钩子函数。

field_collection_views_field_formatter_view里面,注意粗体字部分,这里面的逻辑是,我们获取所有的field collection itemids,然后把它们作为参数传递给对应的视图(view)。views_embed_view,这个函数,我们已经非常熟悉了,注意这里的参数传递方式,我们这里使用了“+”号,来传递多个参数。这是实际当中,经常用的一个技巧。

最后,我们实现了hook_views_api,这里注意的是'api',我们这里用的是'3.0-alpha1',是Views的最初版本,如果把它改为'3.0',也是可以的。


Drupal版本:

14 实现hook_views_data

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

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 ']

而不是去拼凑我们的字段了。


Drupal版本:

15 实现hook_views_default_views

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

在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;

}



Drupal版本:

16 通过继承views_handler_field,定制自己的视图字段处理器

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

我们这里给出一个最简单的例子,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



Drupal版本:

17 总结

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

    在实际项目中,大部分都已经有了Views的集成,我们需要做的,也就是用的到的,可能就是这里所讲的这些,导出views,在已有的基础上添加一个字段什么的。以后,有机会,我们介绍更多的Views的集成。



Drupal版本:

2 配置property validation验证规则

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

集成property validation,和集成Field validation的过程其实是一样的。首先是我们在后台配置验证规则,然后将其导出成代码,这和后面所讲的将Views的代码导出,是一样的。

启用Property validation模块以后,导航到admin/structure/property_validation

图片1.png 

这是CtoolsExport 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支持的内部路径,没有将别名排除出去,不过这个影响不大。


Drupal版本:

3 导出property validation验证规则

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

我们回到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.';

    我们这里的导出,是基于CroolsExport插件功能的。可能读到这里的时候,很多人会问,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):

图片3.png 

     而在存储(Storage)里面,也显示的是“Overridden”了。如果我们恢复(Revert)一下,“Overridden”就变成“Default”了。这里的配置和Views里面是一致的。

Field validation模块,正在日趋流行,但是安装量还没有达到34万的规模,之所以集成它,主要为了大家展示一下Ctools导出插件的具体应用,另外老葛也希望,Breadcrumb2这个模块可以为Field validation模块带来更多的安装量。我们下面来看看Views的集成,这是项目中经常用的。


Drupal版本:

4 Views的集成

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

我们的面包屑模块,是基于Entity API的,Entity API提供了基本的Views集成,我们这里要做的是,在它的基础之上再添加一些集成,从而充分满足我们的需要。有兴趣的可以阅读一下entity\views下面的源代码。需要说明一下的是,这里的代码非常抽象,我看了以后,也只是大致了解了一下代码的结构。不过这并不影响,我们接下来的工作。


Drupal版本:

5 Entity API默认的Views集成

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

我们回到admin/structure/breadcrumbs,现在这个页面还是一个空白页面,我们这里想要显示的内容是,面包屑列表,并提供按照路径的查询功能。同时可以编辑面包屑,删除面包屑,还可以直接添加面包屑。

我们首先创建一个Views,导航到admin/structure/views,点击添加视图(Add new view),做以下配置:

图片4.png 

“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)”,按照降序排列,这样新增的面包屑放在前面。这是配置好的样子:

 

图片5.png

Drupal版本:

6 使用views_embed_view嵌入视图

作者:老葛,北京亚艾元软件有限责任公司,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,看一下效果,我这里预先定义了很多面包屑,这里给出一个简单的截图:

图片6.png 

下面的分页功能,是正常的,如果对这个页面,做比较多的测试的话,会发现这里面存在一个问题。在上面的搜索框里面,输出“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不起作用了。


Drupal版本:

7 在管理界面添加一个动作链接

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

访问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”链接了:

图片1.png 


Drupal版本:

8 在OverLay中添加面包屑

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

说到节点模块,我们还会主要到一个功能,当我们点击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中打开了:

图片2.png 

这是一个进步。


Drupal版本:

9 为Views提供更多的可用字段

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

我们在前面的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_breadcrumbedit_breadcrumbdelete_breadcrumboperations。用来方便的编辑、删除、查看、管理面包屑。

这里的$data就是一个大的数组,$data['breadcrumb']是基表,$data['breadcrumb']['edit_breadcrumb']是下面的字段,里面的titlehelp键就不用介绍了,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集成的更多代码,可以直接参看ViewsDrupal核心模块的支持,位于目录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


Drupal版本:

第3章 Rules的集成

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

实际上,到现在为止,我们模块的优势,还没有体现出来,只有实现了Rules的集成,才能体现出来,不然的话,一个一个的添加面包屑,是不现实的。

我们的想法,很简单,就是在创建节点的时候,通过Rules,创建对应的面包屑对象。这样当显示这个节点时,我们就可以调用这个面包屑对象了。我们首先实现一个基于分类的面包屑。面包屑的结构是这样的:

首页 » 图书 » 图书分类1 » 图书子分类1

首页 » 新闻 » 新闻分类1

基本是按照分类术语的层级结构进行了,这个结构和网上书店系统里面的结构一致。我们的目标,就是方便的实现这样的面包屑结构,并且允许用户通过Rules的后台进行进一步的配置。在我们继续进行之前,我们先来介绍一下Rules的基本术语。



Drupal版本:

1 Rules中的基本术语

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

Rules,我常常翻译成为规则,它包含三个组成部分:事件(Event)、条件(Condition)、动作(Action)。事件和动作是必须的,条件是可选的,但是大部分rules里面都是有条件的。

Event(事件),就是发生了什么。比如,新增了一个节点、更新了一个节点、删除了一个节点、查看一个节点,这些都是一个事件。Rules核心自带了很多事件,我们也可以创建自己的事件。

Condition(条件),这个类似于Panels的选择规则,说白了就是一组if语句,来检查一下是否满足当前的条件,如果满足,则执行相应的动则;否则就会跳过。举个例子,比如说一个节点的内容类型,它是不是Page类型的;再比如,这个节点是否推荐到了首页、节点是否被发表,等等,这些都是条件。在Rules的条件里面,支持关系运算“or”“and”,多个条件可以使用这两个符号连接,从而合成更复杂的条件。

Action(动作):就是将要执行的东西,比如发送一封电子邮件、显示一条提示消息、创建一个新的实体。在动作里面,支持循环(loop),在循环里面,可以继续添加动作。

我们来看一下,三者的关系图:

图片7.png

 



Drupal版本:

10 将rules规则导出到代码里面

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

我们实现了在新建节点时,使用rules自动创建一个面包屑对象。现在我们将这个规则克隆一下,将克隆后的名字改为“breadcrumb for node update”,并保存。接着,添加一个新的事件“After updating existing content”(在更新已有内容之后),删除原来的事件After saving new content(新建内容之后)。这样当系统更新节点时,我们也会自动的更新对应的面包屑对象。

接着,新建一个规则“breadcrumb for term create”,事件为After saving a new term,配置好的动作为:

图片1.png 

然后以它为基础,克隆一份,将克隆后的命名为“breadcrumb for term update”,添加事件After updating an existing term,删除原来的事件。保存即可。

这样,当创建一个分类术语时,Rules会自动为它创建一个面包屑对象,最后生成的面包屑和系统自带的是略有区别的,在我们的面包屑对象里面,最前面是词汇表的链接,同时包含了当前分类术语。其它都和系统默认的一致。

现在导航到admin/config/workflow/rules页面,点击规则“breadcrumb for node create”右边的export链接,rules会为我们生成代码形式。

图片2.png 

我们要做的工作就是,新建一个文件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”(恢复回去)。


Drupal版本:

11 为Rules自定义条件

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

    后来,我还想到了这样的问题,假如用户安装了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。接着,我们点击添加条件

图片1.png 

选中我们刚自定义的条件,点击继续按钮,进入条件的配置页面:

图片2.png 

条件的配置表单,也是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;

}


Drupal版本:

12 实现hook_rules_file_info

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

这个其实是一个很小的问题了,我在阅读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.incbreadcrumb2.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目录下面,比如这里面有:

图片1.png 

清除缓存,一切正常。我们有关Rules的集成,也就介绍到这里。



Drupal版本:

13 总结

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

通过本章的学习,我们实现了:

1),自定义一个rules动作

2),自定义一个rules条件

3),导出rules规则到代码里面,也就是通过代码提供默认的rules规则。

 



Drupal版本:

2 Entity API提供了默认的Rules集成

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

导航到admin/config/workflow/rules,这个rules的管理界面,需要启用Rules UI模块,才能访问这个页面。没有启用是看不到的。在这里我们点击“Add new rule”(添加一个新的规则)链接,进入页面admin/config/workflow/rules/reaction/add。在对应的事件里面,我们看到,已经有了有关面包屑实体的事件了。

图片1.png

这些事件包括,删除一个面包屑、创建一个新的面包屑之后、更新一个面包屑、在保存面包屑之前、查看面包屑。这些事件,都是Entity API默认提供的。

我们在这里,为Name(规则名字)输入“breadcrumb for node create”,事件选择“After saving new content”(在保存新内容后)。然后保存。这样进入Rules的配置页面admin/config/workflow/rules/reaction/manage/rules_breadcrumb_for_node_create

我们现在先不加条件,我们添加一个动作,这里的动作很多:

图片2.png 

由于,我们要做的事情是创建一个面包屑实体,Entity API模块提供了这样的一个动作“Create a new entity”(创建一个新的实体)。我们尽量利用已有模块提供的功能。所以这里选择这个动作创建一个新的实体

此时会出来一个新的对话框,让我们选择实体类型,我们这里选择Breadcrumb就可以了:

图片3.png 

选中之后,点击继续,此时会展开更多的配置选项:

图片4.png 

我们创建了一个新的面包屑实体,这个实体需要使用一个变量存储起来,这里就是配置变量的名字的。图中所示,是默认的变量标签和名字,我们将其分别改为“Breadcrumb”“breadcrumb”,并保存。

接下来,我们需要为这个面包屑设置属性和字段的值了,我们关心的有两个地方,一个是当前页面的路径,另一个就是面包屑链接,我们使用的link字段。先来看路径的设置。

我们需要再添加一个动作,这一次,我们选择使用“Set a data value”这个动作。选中以后是这样的:

图片5.png 

在这个数据选择器里面,我们需要指定要修改哪个数据。通过选择,我们得到“breadcrumb:path”,具体的选择过程,自己用鼠标点点就知道了。Rules的这种选择,非常方便。选中,以后,点击继续按钮。

图片6.png 

现在我们需要为breadcrumb:path指定一个新的值了,在这里面,我们输入“node/[node:nid]”,其中“[node:nid]”是一个替换符,展开下面的置换模式,就可以找到。我们这里把面包屑的路径设置为了当前被保存的节点的路径。

注意,我们动作添加到这里,如果现在去创建一个新的节点的话,系统就会创建一个面包屑对象。并且为我们指定对应的路径。我开始做到这里的时候,还添加了一个动作“Save entity”(保存实体)。用来将我们在前面创建的面包屑实体保存。但是后来经过反复的测试,突然发现,我们不需要“Save entity”这个动作,系统会自动的帮助我们保存面包屑对象。有兴趣的朋友,读到这里可以做一下对比。

此外,我还做过一个尝试,就是继续添加动作“Set a data value”这个动作,只不过这次要设置的数据,我们改为了“breadcrumb:link”,但是link本身是一个数组,不好直接输入。添加后是这个样子的:

图片7.png 

我们使用这个链接本身来设置这个链接,就是绕了一个圈,又回到了原点,什么都没有做。我开始的时候曾经尝试过,比如添加一个变量,将这个变量的类型,设置为“List”的形式,然后想一个一个向里面追加链接,这种办法,尝试了,也不起作用。最终我决定,定义自己的一个动作,专门用来向面包屑对象里面追加链接。


Drupal版本:

3 定义自己的动作

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

现在我们开始编写代码了,因为现有的功能已经满足不了我们的需求了。在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' :是否保存当前对象。


Drupal版本:

4 数据类型

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

Rules里面有一个很重要的概念,就是数据类型,可能这个数据类型,是由Entity API提供的,因为两个模块都是Fago开发的。所以结合的非常好。我们来看一下Rules核心提供的数据类型:datedurationintegerdecimaltexttokenbooleanurilistentitystruct

我们前面用到的'breadcrumb2',就属于这里的entity,这个类型由Entity API帮助我们提供了。当我们添加动作“Add a variable”(添加一个变量)的时候,就会看到所有可用的类型了:

图片1.png 

对于实体,应该是该实体类型的机读名字。我们在做Rules开发的时候,会经常用到这些数据类型。如果想要实现自己的数据类型的话,可以使用hook_rules_data_info来定义。


Drupal版本:

5 实现动作的访问控制回调函数

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

接着,向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中迁移过来的,当时的迁移还是费了很大的功夫的。



Drupal版本:

6 实现动作的逻辑处理函数

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

我们通过'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的开发时,有时候,缓存需要多清除两次。好了现在就可以添加我们的动作了。

图片1.png 

我们选中动作“Append breadcrumb trail”(追加面包屑链接),这里使用trail这个英文单词,是因为另外的一个面包屑模块也这么用。选中后,点击继续按钮,就可以配置我们的这个动作了。

图片2.png 

只需要我们指定参数,Rules就会自动的为我们创建好对应的配置表单,比较智能吧。由于我们这里只有breadcrumb这个变量是面包屑对象,所以第一个参数,系统为我们自动设置了。我们在TITLE里面输入“[node:type-name]s”,在URL里面输入“[node:type]s”,注意这里的[node:type-name][node:type]都是替换符号,我以前经常把它们称作令牌(token)。现在创建一个page页面,就会自动创建一个首页 >> Pages”这样的面包屑了。


Drupal版本:

7 按照路径更新面包屑

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

一个新的问题出现了,如果当我们更新一个节点时,也使用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,表示这里是更新,而不是新建。


Drupal版本:

8自定义动作:获取节点上面的第一个分类术语

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

我们前面实现了一个动作,通过追加面包屑链接,我们可以构建完整的面包屑对象了。做到这里,就基本上可以满足大部分实际需要了,只不过配置的过程比较麻烦一些,可能需要为每个内容类型分别配置面包屑。而我们的目标是建立基于分类的面包屑,而且构建起来,通用一些,这样模块的易用性就更高一些。所以我们这里借鉴一下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

图片1.png 

其它都不用修改,注意这里提供的变量,与前面代码的对应关系。保存这里的设置,我们现在就可以使用变量“First term (lightest_term)”了。现在,我们就可以再追加一个面包屑链接了。这是追加的链接:

图片2.png 

现在,我们创建一个词汇表(product_category),并添加以下分类术语:

图片3.png 

接着创建一个内容类型product,并为其添加一个分类字段,引用的词汇表就是刚刚创建的。接着,我们创建一个产品,分类术语选择A11,这个时候系统会总动的帮助我们创建一个面包屑对象。面包屑的显示效果:

图片4.png 

如果这里,能够把A11 的父术语AA1也都显示出来,效果就完美了。注意,现在面包屑链接中的路径,还没有在内部定义,根据实际需要可以调整。


Drupal版本:

9 实现以分类层级结构为基础的面包屑

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

如果你对Rules比较熟悉的话,现在我们就可以点击“Add loop”这个链接,添加一个循环,我们根据lightest_term,就可以自动的获取到它所有的父术语,Rules核心就自带了这个功能。

   图片1.png

我们这里把“Current list item”改为“Parent term”, 把“list_item”改为“parent_term”。上面的“lightest-term:parents-all”是我自己选出来的,也是rules自己提供了的:

图片2.png 

保存以后,我们可以把刚才追加面包屑链接的动作,拖到循环的里面,也可以将其删除,在循环里面,单独的再创建一个同样的动作。我们这里将它拖了过来:

图片3.png 

现在编辑这个动作。这是修改后的样子:

图片4.png 

保存。现在再创建一个测试的产品节点,分类术语还选择A11。生成的面包屑是这个样子的:

图片5.png 

怎么说呢,就差那么一点点了,分类术语的顺序在这里弄反了。我们只需要将“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的上面:

图片6.png 

接着点击保存变更按钮,现在我们就可以修改这个循环,将List的数据选择,从“lightest-term:parents-all”改为“parent-terms”了。修改后,保存这个循环。

现在再创建一个产品节点,继续选择A11,生成的面包屑为:

图片7.png 

这就是我们想要的。


Drupal版本:

第4章 集成上下文链接

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

其实我一直有一个这样的想法,当我决定为每个页面创建一个面包屑对象的时候,就有了这样的想法,在面包屑这片内容上面输出一个上下文链接,可以方便的添加、配置面包屑:

图片1.png 

 

更前卫一点的想法,是如何与Edit这个模块集成,就是让用户在这里能够在当前页面编辑面包屑。在当前页面编辑面包屑的这个想法,需要等待Spark这个安装包里面的技术成熟稳定以后,才会考虑实现。

我们来看看,上下文链接的形式。在breadcrumb2beta2版里面,已经实现了这个功能,有兴趣的可以直接下载对应的代码,我们这里重新按照我当时的实现步骤,为大家重现一下完整的过程。


Drupal版本:

1 接管theme_breadcrumb函数

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

我们是可以在主题层覆写面包屑的,但是有一个问题,我们这里定义的是自己的模块,我们想在自己的模块里面修改theme_breadcrumb函数。我们知道,我们是不能够直接修改Drupal核心代码的,所以将其接管过来是最佳的办法。这也是我们添加上下文链接的第一步。

我们首先需要知道,theme_breadcrumb是在什么地方定义的。说真的,真心受不了,Google不能用。搜索个theme_breadcrumb都出不来结果。好吧,我们直接登陆http://api.drupal.org/api,在里面搜索theme_breadcrumb。我们看到这个函数是在includes/theme.inc文件里面定义的。函数的代码也很简单:

 

function theme_breadcrumb($variables) {

  $breadcrumb = $variables['breadcrumb'];

 

  if (!empty($breadcrumb)) {

    // Provide a navigational heading to give context for breadcrumb links to

    // screen-reader users. Make the heading invisible with .element-invisible.

    $output = '<h2 class="element-invisible">' . t('You are here') . '</h2>';

 

    $output .= '<div class="breadcrumb">' . implode(' » ', $breadcrumb) . '</div>';

    return $output;

  }

}

很多资料上,讲主题函数的覆写的时候,通常都以这个作为例子。我记得以前的Drupal专业开发指南里面,都拿这个作为例子。

知道了这个函数的位置以后,我们还需要知道,哪个hook_theme钩子函数中,注册了这个主题函数。而theme_breadcrumb这个函数是在drupal_common_theme里面注册的,通过system_theme实现。对应代码如下:

   'breadcrumb' => array(

      'variables' => array('breadcrumb' => NULL),

    ),

想要修改主题的注册表信息,我首先想到的是hook_theme_alter,但是通过Google查找了一下,发现应该使用hook_theme_registry_alter。和我们平时的不一样。我们向breadcrumb2.module追加以下代码:

 

/**

 * Implements hook_theme_registry_alter().

 */

function breadcrumb2_theme_registry_alter(&$theme_registry) {

  if (isset($theme_registry['breadcrumb'])) {

    $path = drupal_get_path('module', 'breadcrumb2');

    $theme_registry['breadcrumb']['path'] = $path;

    $theme_registry['breadcrumb']['template'] = 'breadcrumb';

    $theme_registry['breadcrumb']['function'] = NULL;

  }

}

在这里,我们首先检查,有没有设置$theme_registry['breadcrumb'],如果已经设置了,我们对它进行修改,为它设置$theme_registry['breadcrumb']['template'],同时为它设置$theme_registry['breadcrumb']['path'],最后将$theme_registry['breadcrumb']['function']置为空。这里的'path''template''function'是三个键,如果设置了'template',就应该把'function'置为空,否则默认还是使用'function''function'表示这是一个主题函数,'template'表示这是一个模板文件。'path'用来设置模板所在的目录,我们把它修改为了'breadcrumb2'所在的目录了。

现在清除缓存,我们会得到这样的错误消息:

Warning: include(D:\xampp\htdocs\breadcrumb2/sites/all/modules/breadcrumb2/breadcrumb.tpl.php) [function.include]: failed to open stream: No such file or directory in theme_render_template() (line 1495 of D:\xampp\htdocs\breadcrumb2\includes\theme.inc). 

Warning: include() [function.include]: Failed opening 'D:\xampp\htdocs\breadcrumb2/sites/all/modules/breadcrumb2/breadcrumb.tpl.php' for inclusion (include_path='.;D:\xampp\php\PEAR') in theme_render_template() (line 1495 of D:\xampp\htdocs\breadcrumb2\includes\theme.inc).

意思是说,在breadcrumb2找不到breadcrumb.tpl.php,我们还没有创建嘛,这说明了,我们前面的代码起作用了,而且工作的很好。我们现在就创建breadcrumb.tpl.php文件,里面的代码如下:

<?php

 

/**

 * @file

 * Default theme implementation to display a breadcrumb.

 *

 * Available variables:

 * - $contextual_links: contextual links for breadcrumb.

 * - $breadcrumb: An array of breadcrumb link.

 * @see template_preprocess()

 * @see breadcrumb2_preprocess_breadcrumb()

 * @see template_process()

 *

 * @ingroup themeable

 */

?>

 123456

<?php if (!empty($breadcrumb)): ?>

<div class="breadcrumb-wrapper">

  <h2 class="element-invisible">   <?php print t('You are here'); ?>  </h2>

  <div class="breadcrumb"> <?php print implode(' » ', $breadcrumb); ?> </div>

</div>

<?php endif; ?>

中间的“123456”,是用来测试,是不是这个模板文件起作用,模板覆写的时候,我经常使用这个办法,在模板里面加点东西,用来判断它是否起作用。注释里面有个$contextual_links,这个我们在后面才会添加。

 

我们回到breadcrumb2_theme_registry_alter,在这里面,我们可以加上两行调试代码:

  print debug($theme_registry['breadcrumb']);

  print debug($theme_registry['page']);

   然后,再清除缓存,我们呢可以看到打印出来的消息:

array (

  'variables' => 

  array (

    'breadcrumb' => NULL,

  ),

  'type' => 'module',

  'theme path' => 'modules/system',

  'function' => 'theme_breadcrumb',

  'preprocess functions' => 

  array (

  ),

  'process functions' => 

  array (

  ),

)

array (

  'template' => 'page',

  'path' => 'themes/seven',

  'type' => 'theme_engine',

  'theme path' => 'themes/seven',

  'render element' => 'page',

  'preprocess functions' => 

  array (

    0 => 'template_preprocess',

    1 => 'template_preprocess_page',

    2 => 'contextual_preprocess',

    3 => 'overlay_preprocess_page',

    4 => 'shortcut_preprocess_page',

    5 => 'seven_preprocess_page',

  ),

  'process functions' => 

  array (

    0 => 'template_process',

    1 => 'template_process_page',

    2 => 'rdf_process',

  ),

)

通过将整个数组,打印出来,我们可以看到有多少个键可用。以及每个键的赋值情况。我前面的代码写好了以后,发现了一个问题,就是无法在主题层覆写breadcrumb.tpl.php文件。就是说,我们将breadcrumb.tpl.php复制到themes\bartik\templates下面,并稍微的调整一下里面的内容,比如把刚才的“123456”修改为“123”。清除缓存,发现起作用的还是我们模块里面的模板文件。这是在实际应用当中发现的一个问题。到现在还没有解决。本来打算,一边写作,一边写代码,后来灵感大爆发,直接把模块先写完了。我写到这的时候是20121224日,实际上,1211号就发布了beta4版了,很多功能都已经实现。

不过今天依然没有解决这个无法覆写的问题,不过我想,肯定会解决的。我通过Google搜索过,也有人遇到过同样的问题,但是没有解决。不过今天Google罢工了。我们继续前进,来看一下怎么添加上下文。


Drupal版本:

10 总结

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

    通过本章的学习,我们成功的实现了为breadcrumb.tpl.php文件添加上下文链接的功能,与此同时,我们还学习了contextual模块的机制,以及如何通过预处理函数为模板文件添加变量 。

 



Drupal版本:

2 添加上下文链接

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

我们先来分析一个问题,区块上面的上下文链接,怎么生成的。我们来看一下左边导航区块

图片1.png 

然后使用Firefox打开,使用firebug查看一下,上下文链接的源代码。

图片2.png

 

     在区块的标题和正文之间,插入了一个div

<div class="contextual-links-wrapper contextual-links-processed">

里面包含的就是我么看到的上下文链接。打开区块模块里面的block.tpl.php文件,我们发现标题和正文之间,使用的是这行代码:

<?php print render($title_suffix); ?>

我们把这行注释掉,上下文链接就没有了。我们在api.drupal.org上查找template_preprocess_block,这是区块的预处理函数,里面没有$title_suffix的相关定义。我们直接打开contextual.module文件,这个模块非常小,建议阅读一遍。在这里面,我们找到了contextual_preprocess,在这个预处理函数中,有$title_suffix的对应代码:

function contextual_preprocess(&$variables, $hook) {

  // Nothing to do here if the user is not permitted to access contextual links.

  if (!user_access('access contextual links')) {

    return;

  }

 

  $hooks = theme_get_registry(FALSE);

 

  // Determine the primary theme function argument.

  if (!empty($hooks[$hook]['variables'])) {

    $keys = array_keys($hooks[$hook]['variables']);

    $key = $keys[0];

  }

  elseif (!empty($hooks[$hook]['render element'])) {

    $key = $hooks[$hook]['render element'];

  }

  if (!empty($key) && isset($variables[$key])) {

    $element = $variables[$key];

  }

 

  if (isset($element) && is_array($element) && !empty($element['#contextual_links'])) {

    // Initialize the template variable as a renderable array.

    $variables['title_suffix']['contextual_links'] = array(

      '#type' => 'contextual_links',

      '#contextual_links' => $element['#contextual_links'],

      '#element' => $element,

    );

    // Mark this element as potentially having contextual links attached to it.

    $variables['classes_array'][] = 'contextual-links-region';

  }

}

而且,我们看到区块最外面的divclass里面的'contextual-links-region',就是这里设置的。此外,我们在这个模块里面,还会看到:

/**

 * Implements hook_library().

 */

function contextual_library() {

  $path = drupal_get_path('module', 'contextual');

  $libraries['contextual-links'] = array(

    'title' => 'Contextual links',

    'website' => 'http://drupal.org/node/473268',

    'version' => '1.0',

    'js' => array(

      $path . '/contextual.js' => array(),

    ),

    'css' => array(

      $path . '/contextual.css' => array(),

    ),

  );

  return $libraries;

}

 

/**

 * Implements hook_element_info().

 */

function contextual_element_info() {

  $types['contextual_links'] = array(

    '#pre_render' => array('contextual_pre_render_links'),

    '#theme' => 'links__contextual',

    '#links' => array(),

    '#prefix' => '<div class="contextual-links-wrapper">',

    '#suffix' => '</div>',

    '#attributes' => array(

      'class' => array('contextual-links'),

    ),

    '#attached' => array(

      'library' => array(

        array('contextual', 'contextual-links'),

      ),

    ),

  );

  return $types;

}

我们看到contextual_linksDrupal里面的一个元素,类似于表单元素一样,当呈现这个元素的时候,就会加载对应的JSCSS文件,分别为contextual.jscontextual.css。对应的代码为:

    '#attached' => array(

      'library' => array(

        array('contextual', 'contextual-links'),

      ),

    ),

 

由于我们熟悉表单元素,所以只要我们构建出来contextual_links元素的数组,就能将它呈现出来。这样我当时就决定,不通过title_suffix输出上下文链接了,直接通过输出contextual_links元素的形式,来实现我们的目标。我们直接在breadcrumb.tpl.php里面输入以下代码:

<?php 

   $contextual_links   =    array('#contextual_links' => array(

      //'breadcrumb2' => array('breadcrumb', array('1')),

        //'breadcrumb2' => array('breadcrumb', array('add')),

'menu' => array('admin/structure/menu/manage', array('navigation')),

      )); 

   $contextual_link = array(

      '#type' => 'contextual_links',

      '#contextual_links' => $contextual_links['#contextual_links'],

  '#element' => $contextual_links,

    );

  print drupal_render($contextual_link); 

 ?>

我们这里首先构建了$contextual_links,接着设置了一个类型为上下文链接的元素$contextual_link,最后使用drupal_render将它呈现了出来。这个时候,我们通过Firebug,其实就可以看到我们的输出了,但是显示不出来。我阅读了一下contextual.js的源代码,从中发现了原因:

(function ($) {

 

Drupal.contextualLinks = Drupal.contextualLinks || {};

 

/**

 * Attaches outline behavior for regions associated with contextual links.

 */

Drupal.behaviors.contextualLinks = {

  attach: function (context) {

    $('div.contextual-links-wrapper', context).once('contextual-links', function () {

      var $wrapper = $(this);

      var $region = $wrapper.closest('.contextual-links-region');

      var $links = $wrapper.find('ul.contextual-links');

      var $trigger = $('<a class="contextual-links-trigger" href="#" />').text(Drupal.t('Configure')).click(

        function () {

          $links.stop(true, true).slideToggle(100);

          $wrapper.toggleClass('contextual-links-active');

          return false;

        }

      );

      // Attach hover behavior to trigger and ul.contextual-links.

      $trigger.add($links).hover(

        function () { $region.addClass('contextual-links-region-active'); },

        function () { $region.removeClass('contextual-links-region-active'); }

      );

      // Hide the contextual links when user clicks a link or rolls out of the .contextual-links-region.

      $region.bind('mouseleave click', Drupal.contextualLinks.mouseleave);

      // Prepend the trigger.

      $wrapper.prepend($trigger);

    });

  }

};

 

/**

 * Disables outline for the region contextual links are associated with.

 */

Drupal.contextualLinks.mouseleave = function () {

  $(this)

    .find('.contextual-links-active').removeClass('contextual-links-active')

    .find('ul.contextual-links').hide();

};

 

})(jQuery);

我们看到,外面的'contextual-links-region',是需要的,我们没有加上这个class。我们将:

<div class="breadcrumb-wrapper">

修改为:

<div class="breadcrumb-wrapper contextual-links-region">

并将print drupal_render($contextual_link); 放到这个div的内部。这个时候,面包屑右边的上下文链接显示出来了。

图片3.png 

只不过显示的是别处的上下文链接。不过我们已经向前前进了一大步。接着可以尝试使用使用:'breadcrumb2' => array('breadcrumb', array('1')),来构建上下文链接,这次也出来了,只不过这些链接,是bid1的上下文链接。

可能有读者会问,我怎么知道的,这样构建上下文链接,这个我是参考block.module里面的代码:

$build[$key]['#contextual_links']['block'] = array('admin/structure/block/manage', array($block->module, $block->delta));

以及menu.module里面的代码:

$data['content']['#contextual_links']['menu'] = array('admin/structure/menu/manage', array($delta));

我们通过前台知道,就这两个模块有上下文链接。走到这里的时候,需要弄明白这个数组里面的两个参数的含义。这个时候,我还是阅读的源代码:

function contextual_pre_render_links($element) {

  // Retrieve contextual menu links.

  $items = array();

  foreach ($element['#contextual_links'] as $module => $args) {

    $items += menu_contextual_links($module, $args[0], $args[1]);

  }

 

  // Transform contextual links into parameters suitable for theme_link().

  $links = array();

  foreach ($items as $class => $item) {

    $class = drupal_html_class($class);

    $links[$class] = array(

      'title' => $item['title'],

      'href' => $item['href'],

    );

    // @todo theme_links() should *really* use the same parameters as l().

    $item['localized_options'] += array('query' => array());

    $item['localized_options']['query'] += drupal_get_destination();

    $links[$class] += $item['localized_options'];

  }

  $element['#links'] = $links;

 

  // Allow modules to alter the renderable contextual links element.

  drupal_alter('contextual_links_view', $element, $items);

 

  // If there are no links, tell drupal_render() to abort rendering.

  if (empty($element['#links'])) {

    $element['#printed'] = TRUE;

  }

 

  return $element;

}

通过上面的代码,我们可以看到,上下文链接,是通过函数menu_contextual_links生成的,通过查看这个函数的API文档,我们可以弄明白传递给它的三个参数的含义。也就对应于我们代码里面的:

      //'breadcrumb2' => array('breadcrumb', array('1')),

       //'breadcrumb2' => array('breadcrumb', array('add')),

  'menu' => array('admin/structure/menu/manage', array('navigation')),

然后,通过阅读contextual_pre_render_links,我还弄明白了一个问题,那就是上下文链接,本质上还是链接。这不是废话么。理解到这一点,是一个进步,当我们的面包屑对象不存在时,是无法构建出来上下文链接的。中间的这段代码:

'breadcrumb2' => array('breadcrumb', array('add')),

是不工作的。我们理解了,它本质上是一组链接,最后还是通过$element['#links']输出的,所以我们在构建不出来上下文链接的时候,可以直接构建出来链接,放进来。而这里面,还提供了hook_contextual_links_view_alter,通过实现这个钩子,就能够达到添加链接的目的了。


Drupal版本:

3实现预处理函数

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

我们前面的工作都是在模板里面直接进行的,我们把它转移到预处理函数里面。主题函数的预处理函数是有限制的,而我们这里的breadcrumb是一个模板文件了,所以可以为它在模块里面实现预处理函数。我们的实现如下。

/**

 * Implements MODULE_preprocess_HOOK().

 */

function breadcrumb2_preprocess_breadcrumb(&$variables) {

  if (user_access('administer breadcrumbs') && user_access('access contextual links')) {

    $current_path = current_path();

    $breadcrumb = breadcrumb2_load_by_path($current_path);

    if (!empty($breadcrumb)) {

      $contextual_links_element   =    array(

        '#contextual_links' => array(

          'breadcrumb2' => array('breadcrumb', array($breadcrumb->bid)),

        )

      ); 

}

else{

      $contextual_links_element   =    array(

        '#contextual_links' => array(

          'breadcrumb2' => array('breadcrumb', array('0')),

        )

      ); 

}

 

    $contextual_links = array(

      '#type' => 'contextual_links',

      '#contextual_links' => $contextual_links_element['#contextual_links'],

      '#element' => $contextual_links_element,

    );

    $variables['contextual_links'] = drupal_render($contextual_links);

  }

}

这是我最初的实现,这里对模板里面的代码,做了相应的调整。在预处理函数里面,我们添加了一个新的变量,这样在模板文件里面就可以直接输出这个变量的。逻辑代码通常放到预处理函数、处理函数里面,这样可以让模板文件更干净一些。如果是实际当中的项目,就另当别论了。我们这里最终要贡献到drupal.org的,所以应该把逻辑代码独立出来。

现在,我们需要把模板里面的输出,也做相应的调整,实际上这里,只需要把前面的:

<?php print drupal_render($contextual_link); ?>

修改为:

<?php print $contextual_links; ?>

即可,或者直接注释掉原来的,添加上这里的最新代码也可以。另外,模板里面的代码可以删除、或者注释掉了。我们这里是注释掉了。

预处理函数,也是钩子函数,所以实现了这个函数以后,特别是第一次实现一个钩子函数,我们都需要清除一下缓存。Drupal7能够缓存哪些模块实现了某个钩子。这与Drupal6相比,就是一个小小的进步。而我们需要做的就是,当新建一个钩子函数的时候,需要清除缓存,否则就会不工作。我们此时会看到这样的错误消息:

Notice: Undefined variable: contextual_links in include() (line 35 of D:\xampp\htdocs\breadcrumb2\sites\all\modules\breadcrumb2\breadcrumb.tpl.php).

查找钩子函数的实现,是一个耗费资源的操作,将其缓存起来,可以提升性能。有兴趣的读者,可以顺着module_invoke_all这个函数,逐层分析,肯定会找到对应的缓存代码的。我们这里清除缓存,这个错误消息就消失了。对于有面包屑对象的,上下文链接是这样的:

图片1.png 

如果这个页面没有对应的面包屑实体对象,则显示不出来这个上下文链接。


Drupal版本:

4 通过hook_contextual_links_view_alter修改上下文链接

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

我们前面讲过contextual_pre_render_links里面,提供了hook_contextual_links_view_alter这个钩子函数。在上下文,链接呈现前,通过这个钩子函数,可以修改上下文链接,也就是说,可以伪造模拟出来一个链接。可能很多人不明白,为什么。在前面,我们看到这样的代码:

$types['contextual_links'] = array(

    '#pre_render' => array('contextual_pre_render_links'),

….

这里为contextual_links元素,指定了一个预呈现回调函数contextual_pre_render_links,就是说,在呈现上下文链接的时候,会调用这个函数,而在contextual_pre_render_links里面,通过“drupal_alter('contextual_links_view', $element, $items);”,就为我们提供了一个钩子函数,来修改上下文链接。

好了,我们来看一下,具体的实现代码:

/**

 * Implements hook_contextual_links_view_alter().

 */

function  breadcrumb2_contextual_links_view_alter(&$element, &$items) {

  if (isset($element['#contextual_links']['breadcrumb2']) && empty($items)) {

    $current_path = current_path();

    $element['#links'] = array(

      array('title' => t('Add breadcrumb'), 'href' => 'breadcrumb/add', "query" => array('path' => $current_path, drupal_get_destination())),

    );

  }

}

    在这里,我们直接为$element['#links']设置了一个链接。清除缓存,现在,在没有面包屑实体对象的情况下,也有上下文链接了:

图片1.png 

我们在这里面,为链接提供了一个"query"参数,它是这样设置的; 

"query" => array('path' => $current_path, drupal_get_destination())

这里面,drupal_get_destination()返回的本身就是一个数组。除了它以外,我们还提供了'path',这里的目的是,通过URL传递参数。这样,用户在添加面包屑对象的时候,就不需要输入刚才的路径了。修改文件里面的:

function breadcrumb2_add(){

  $breadcrumb = entity_get_controller('breadcrumb2')->create();

  //drupal_set_title(t('Create breadcrumb'));

  if (isset($_GET['path'])) {

    $breadcrumb->path = $_GET['path'];

  }

  $output = drupal_get_form('breadcrumb2_form', $breadcrumb);

 

  return $output;

}

粗体为我们新增的代码。这里我们直接尝试从URL获取path参数,并将它赋值给$breadcrumb->path。这样用户就不用输入路径了。

当然,还有一点不足,那就是用户对于这个路径还是可以编辑的,实际上,我们应该不允许用户编辑这个路径。当路径已经存在的情况下,就不允许编辑了。因为路径是唯一的。

function breadcrumb2_form($form, &$form_state, $breadcrumb) {

……

  $form['path'] = array(

    '#type' => 'textfield',

    '#title' => t('Path'),

    '#maxlength' => 60,

    '#default_value' => !empty($breadcrumb->path) ? $breadcrumb->path : '',

    '#weight' => -10,

  );

  if (!empty($breadcrumb->path)) {

    $form['path']['#disabled'] = TRUE;

  }

  ……

breadcrumb2_form中追加上面的粗体代码即可。这是现在的效果图:

图片2.png 

    还不错吧。还有值得改进的地方,实际上Drupal本身的面包屑,总是存在的。所以我们这里的上下文链接,不应该叫做添加面包屑。此外,删除面包屑,对我们来说也没有什么用,所以删除链接,也可以不要。最终,我们将编辑、添加面包屑,统一成为配置面包屑。下面是修改后的代码。

/**

 * Implements MODULE_preprocess_HOOK().

 */

function breadcrumb2_preprocess_breadcrumb(&$variables) {

  if (user_access('administer breadcrumbs') && user_access('access contextual links')) {

    $contextual_links_element   =    array(

      '#contextual_links' => array(

        'breadcrumb2' => array('breadcrumb', array('0')),

      )

    ); 

 

    $contextual_links = array(

      '#type' => 'contextual_links',

      '#contextual_links' => $contextual_links_element['#contextual_links'],

      '#element' => $contextual_links_element,

    );

    $variables['contextual_links'] = drupal_render($contextual_links);

  }

}

 

 

/**

 * Implements hook_contextual_links_view_alter().

 */

function  breadcrumb2_contextual_links_view_alter(&$element, &$items) {

  if (isset($element['#contextual_links']['breadcrumb2']) && empty($items)) {

    $current_path = current_path();

    $breadcrumb = breadcrumb2_load_by_path($current_path);

    if (!empty($breadcrumb)) {

      $contextual_links_element['#contextual_links']['breadcrumb2'] = array('breadcrumb', array($breadcrumb->bid));

      $element['#links'] = array(

        array('title' => t('Config breadcrumb'), 'href' => 'breadcrumb/' . $breadcrumb->bid . '/edit', "query" => drupal_get_destination()),

      );

    }

    else {

      $element['#links'] = array(

        array('title' => t('Config breadcrumb'), 'href' => 'breadcrumb/add', "query" => array('path' => $current_path, drupal_get_destination())),

      );

    }

  }

}

我们这里只是构建了一个不存在的上下文链接,相当于占位符吧,然后在hook_contextual_links_view_alter里面,模拟出来上下文链接。

还有一个很小的问题,在IE8里面,鼠标常常放不到这个链接上,快移上去时,链接就会消失。后来我想到了一个办法,外面又加了层div,并为div设置了高度。这是修改后的代码:

<?php if (!empty($breadcrumb)): ?>

<div class="breadcrumb-wrapper contextual-links-region">

  <h2 class="element-invisible">   <?php print t('You are here'); ?>  </h2>

   <?php if (!empty($breadcrumb)): ?> 

    <div style='height:10px'> <?php print $contextual_links; ?> </div>

  <?php endif; ?>

  <?php //print drupal_render($contextual_link); ?>

  <div class="breadcrumb"> <?php print implode(' » ', $breadcrumb); ?> </div>

</div>

<?php endif; ?>

这里面可是有个bug哦,细心的朋友一眼可能就看出来了。到目前为止,我们已经完成了breadcrumb2模块项目的beta2版里面的所有代码了。同时还解决了Views的一个问题。我们这里也顺带的介绍一下,从beta2beta4里面所解决的问题。以及在实践当中遇到的其它问题。


Drupal版本:

5 Breadcrumb2已有问题的修正

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

没有哪个系统是完美的,也没有哪个Drupal模块不存在问题,可是我的心还是有点急,一口气从alpha1发布到beta2,希望别人能够尽快的看到效果。可是后来发现,下载了99次,只安装了一个站点,而这个站点还是我自己的。后来,让身边的朋友反馈意见,发现很多问题。如果你是跟着做过来的,可以尝试一下自己来解决一下这些问题。看看自己能否独立的解决这些问题。这些问题包括:   

模块安装不起来,http://drupal.org/node/1863470

pathauto模块冲突http://drupal.org/node/1863434

匿名用户,看到未定义的变量contextual_linkshttp://drupal.org/node/1863864

panels窗格里面输出面包屑,输出不出来。


Drupal版本:

6 模块安装不了

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

我们现在的这个模块,依赖于ViewsRulesLinksField validation,模块依赖的太多,安装的时候就会存在问题。模块直接安装不起来。我信心满满的,发布了beta2,别人却装不上。

我在本地,一个新建的站点,把所有的模块都下载好,一起安装,报了这样的错误消息:

FieldException: Attempt to create a field of unknown type link_field. in field_create_field() (line 110 of D:\xampp\htdocs\breadcrumb3\modules\field\field.crud.inc).

我把这个问题发到了官网上,http://drupal.org/node/1863470。我后来查了相关的资料,发现,这个和Drupal核心也有点关系,其实应该算是Drupal核心的一个bug。不过,解决办法还是有的:

function breadcrumb2_install() {

   // Add or remove the link field, as needed.

  $field = field_info_field('link');

  if (empty($field)) {

    $field = array(

      'cardinality' => '-1',

      'entity_types' => array('breadcrumb2'),

      'field_name' => 'link',

      'module' => 'link',

      'type' => 'link_field',

    );

    $field = field_create_field($field);

  }

 

  // If a field type we know should exist isn't found, clear the Field cache.

  if (!field_info_field_types('link_field')) {

    cache_clear_all('field_info_types:', 'cache_field', TRUE);

  }

粗体部分,为新增的,我们通过代码创建了link类型的字段以后,如果后面还要添加该类型的字段实例,此时就需要清除字段缓存。不然就会找不到对应的类型。而我们在安装函数的下面,就有field_create_instance($instance)这样的代码。

这段代码是从Commerce模块里面复制过来的,在此做出说明。我调试这个问题的时候,还发现了另外的一个问题,就是我把breadcrumb2模块卸载了,还是无法禁用link模块,这个也是因为缓存的原因。卸载了breadcrumb2模块以后,手动的清除缓存,就可以禁用、卸载link模块了。当然,我们也可以在代码里面清除缓存。这个也被列为了Drupal核心的bug

 


Drupal版本:

7 与Pathauto的冲突问题

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

问题地址,http://drupal.org/node/1863434。当我在一个实际的生产站点,添加面包屑的时候,报错了,错误信息如下:

Notice: Undefined index: alias in path_form_element_validate() (line 156 in /var/www/eplus.cn/modules/path/path.module).

 Recoverable fatal error: Argument 2 passed to drupal_array_set_nested_value() must be an array, null given, called in /var/www/eplus.cn/includes/form.inc on line 2518 and defined in drupal_array_set_nested_value() (line 6510 in /var/www/eplus.cn/includes/common.inc).

这是path模块报的错,我当时怀疑,是不是系统环境的问题,因为在我本地,一切都是好好的,但是到了线上,就出了问题。

我把线上的代码,下载到本地,然后使用NotePad++的强大的搜索功能,搜索都有哪个模块调用了path_form_element_validate函数,问题的原因一下子就找出来了,原来罪魁祸首是pathauto模块。pathauto.module里面有这样的一段代码:

/**

 * Implements hook_field_attach_form().

 *

 * Add the automatic alias form elements to an existing path form fieldset.

 */

function pathauto_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) {

  list($id, , $bundle) = entity_extract_ids($entity_type, $entity);

 

  if (!isset($form['path'])) {

    // This entity must be supported by core's path.module first.

    // @todo Investigate removing this and supporting all fieldable entities.

    return;

  }

  else {

    // Taxonomy terms do not have an actual fieldset for path settings.

    // Merge in the defaults.

    $form['path'] += array(

      '#type' => 'fieldset',

      '#title' => t('URL path settings'),

      '#collapsible' => TRUE,

      '#collapsed' => empty($form['path']['alias']),

      '#group' => 'additional_settings',

      '#attributes' => array(

        'class' => array('path-form'),

      ),

      '#access' => user_access('create url aliases') || user_access('administer url aliases'),

      '#weight' => 30,

      '#tree' => TRUE,

      '#element_validate' => array('path_form_element_validate'),

    );

  }

我的眼睛一下子亮了,我们定义了同样的表单元素’path’, pathauto模块为它追加了验证规则path_form_element_validate。这是我休整后的样子:

function breadcrumb2_form($form, &$form_state, $breadcrumb) {

  // Save the breadcrumb for later, in case we need it.

  $form['#breadcrumb'] = $breadcrumb;

  $form_state['breadcrumb'] = $breadcrumb;

  

  $form['bid'] = array(

    '#type' => 'value',

    '#value' => isset($breadcrumb->bid) ? $breadcrumb->bid : NULL,

  );

   

  // Add the field related form elements.

  field_attach_form('breadcrumb2', $breadcrumb, $form, $form_state);

  

  $form['path'] = array(

    '#type' => 'textfield',

    '#title' => t('Path'),

    '#maxlength' => 60,

    '#default_value' => !empty($breadcrumb->path) ? $breadcrumb->path : '',

    '#weight' => -10,

  );

  if (!empty($breadcrumb->path)) {

    $form['path']['#disabled'] = TRUE;

  }

 

  $form['actions'] = array('#type' => 'actions');

  $form['actions']['submit'] = array(

    '#type' => 'submit',

    '#value' => t('Save'),

    '#weight' => 40,

  );

  if (!empty($breadcrumb->bid)) {

    $form['actions']['delete'] = array(

      '#type' => 'submit',

      '#value' => t('Delete breadcrumb'),

      '#weight' => 45,

      '#limit_validation_errors' => array(),

      '#submit' => array('breadcrumb2_form_submit_delete')

    );

  }

  $form['#validate'][] = 'breadcrumb2_form_validate';

  $form['#submit'][] = 'breadcrumb2_form_submit';

  return $form;

}

就是将field_attach_form放到了$form['path']的前面,两者调整了一下位置而已,这样pathauto模块就找不到$form['path']这个元素了。pathauto模块里面的注释让我害怕,它要把它的$form['path']添加到所有的实体表单上。还让不让人活啊。不过,我们这里的办法仍然适用。


Drupal版本:

8 未定义的变量

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

问题的地址:http://drupal.org/node/1863864。当使用普通用户,或者匿名用户访问时,总是报这样的错误信息:

Notice: Undefined variable: contextual_links in include() (line 34 of D:\xampp\htdocs\breadcrumb3\sites\all\modules\breadcrumb2\breadcrumb.tpl.php

我检查了一下breadcrumb.tpl.php,原来是一个笔误。不过此时我已经发布了beta3版了。真是汗颜啊。都beta3了,还存在这样的问题。这是修正后的样子:

<?php if (!empty($breadcrumb)): ?>

<div class="breadcrumb-wrapper contextual-links-region">

  <h2 class="element-invisible">   <?php print t('You are here'); ?>  </h2>

   <?php if (!empty($contextual_links)): ?> 

    <div style='height:10px'> <?php print $contextual_links; ?> </div>

  <?php endif; ?>

  <?php //print drupal_render($contextual_link); ?>

  <div class="breadcrumb"> <?php print implode(' » ', $breadcrumb); ?> </div>

</div>

<?php endif; ?>

你开始的时候,看出来哪里出错了吗?还是读到这里才看出来这个错误。这就是beta4版。


Drupal版本:

9 与Panels的兼容问题

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

在一个实际的项目中,面包屑是添加到Panels的窗格里面去的。如果节点页面不使用Panels的话,就正常,使用了Panels,把面包屑放到Panels的窗格里面输出的话,就不正常了。你可以通过这个步骤重现这个问题,在panels的内容页面,为某一个区域添加内容,在弹出的对话框里面,左边选择页面元素(page elements),此时右边就会有面包屑可以添加,添加后,就会出现我们这里所说的问题。

 

我是这样解决这个问题的,首先,找到这个面包屑元素在Panels里面是怎么定义的,先通过前台界面,鼠标移上去,看看左下路径的变化,从路径里面可找到 page_breadcrumb,根据这个使用NotePad++的查找功能,就能找到对应的代码。代码位于ctools\plugins\content_types\page下面的page_breadcrumb.inc文件中:

$plugin = array(

  'title' => t('Breadcrumb'),

  'single' => TRUE,

  'icon' => 'icon_page.png',

  'description' => t('Add the breadcrumb trail as content.'),

  'category' => t('Page elements'),

  'render last' => TRUE,

);

 

/**

 * Output function for the 'page_breadcrumb' content type.

 *

 * Outputs the breadcrumb for the current page.

 */

function ctools_page_breadcrumb_content_type_render($subtype, $conf, $panel_args) {

  $block = new stdClass();

  $block->content = theme('breadcrumb', array('breadcrumb' => drupal_get_breadcrumb()));

 

  return $block;

}

我们看到,Panels里面的面包屑,根本没有走页面,所以通过hook_page_alter是修改不了这里的面包屑的。可能这样解释更正确一点,就是Panels生成面包屑之前,还没有调用hook_page_alter呢,就是说Panels先生成的面包屑,后面才调用了我们的hook_page_alter

解决办法很简单,就是将breadcrumb2_page_alter里面的代码搬到面包屑的预处理函数breadcrumb2_preprocess_breadcrumb里面。这是添加后的代码:

 

function breadcrumb2_preprocess_breadcrumb(&$variables) {

  $current_path = current_path();

  $breadcrumb2 = breadcrumb2_load_by_path($current_path);

  if(!empty($breadcrumb2)){

    $breadcrumbs = array();

      // Only keep the first one. 

    if (!empty($variables['breadcrumb'][0])) {

      $breadcrumbs[] = $variables['breadcrumb'][0];

    }

    $wrapper = entity_metadata_wrapper('breadcrumb2', $breadcrumb2);

    $breadcrumb_links = $wrapper->link->value();

    foreach($breadcrumb_links as $breadcrumb_link){

      $breadcrumbs[]= l($breadcrumb_link['title'], $breadcrumb_link['url']);

    }

$variables['breadcrumb'] = $breadcrumbs;

  }

 

  if (user_access('administer breadcrumbs') && user_access('access contextual links')) {

 

粗体部分为新增的。现在我们就可以注释掉breadcrumb2_page_alter了。清楚缓存,一切正常工作。前面忘记介绍了一个问题,当系统已经设置了面包屑,并且面包屑里面有多个链接的时候,就与我们这个模块的面包屑设置冲突了,此时,我们只取系统设置的面包屑链接里面的第一个,也就是首页的链接,其它全部去掉。在beta3beta4里面,都有对应的代码。

还有一个问题,就是说把breadcrumb.tpl.php复制到当前主题目录下,在这里覆写,不起作用的问题,这也是一个大问题。此外,还有两个工作要做,一个就是为breadcrumb2_load_by_path加缓存,就是在同一个页面请求处理期间,如果加载了一次,那么下次使用这个函数时,就会直接返回前面的结果。另外一个工作就是,为面包屑模板加上模板建议,可以按照路径的具体程度进行覆写。还有就是把模板里面的:

<div class="breadcrumb-wrapper contextual-links-region">

把这里的的设置,放到预处理函数中进行。比如contextual-links-region,只有当输出上下文链接的时候,才输出这个类。

由于这些问题,和上下文链接,都快没有直接的关系了,所以就不放到本章介绍了。


Drupal版本:

第5章 Drupal主题系统探索

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

我们前面,已经为Breadcrumb2实现了上下文链接,在里面也提到一个问题,就是将breadcrumb.tpl.php复制到当前主题下面,不起作用的问题。还有一些其它的问题,以及可以改善的地方。


Drupal版本:

1 无法在当前主题下面覆写breadcrumb.tpl.php

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

如果,我们提供了breadcrumb.tpl.php,但是却无法在当前主题下面覆写的话,这个模板文件的作用就大打折扣了。从发现这个问题以后,我就多次尝试去解决它。首先是借助于Google,有什么技术问题,先问Google。这是我在Google上使用的搜索词“hook_theme_registry_alter template file could not be override”

很幸运,有人遇到了同样的问题http://drupal.org/node/1424048,不幸的是,没有解决办法,如果你现在浏览这个问题的话,你会发现下面已经给出了解决办法,这个解决办法就是老葛给出来的。

我找到了一个类似的:

http://www.metachunk.com/blog/adding-module-path-drupal-7-theme-registry

在这篇文章里面,作者解决一个类似的问题,和我这里解决的问题还不完全一样,但是却为我们解决问题指明了方向。它是想在自己的模块目录下面,为模板文件提供一个模板建议,比如block--module--delta.tpl.php。这篇文章在解决这个问题的时候,也是花费了很大功夫的。它是这样解决的:

/**

 * Implements hook_theme_registry_alter()

**/

function mymodule_theme_registry_alter(&$theme_registry) {

  $mod_path = drupal_get_path('module', 'mymodule');

  $theme_registry_copy = $theme_registry;       // munge on a copy

  _theme_process_registry($theme_registry_copy, 'phptemplate', 'theme_engine', 'pow', $mod_path);

  $theme_registry += array_diff_key($theme_registry_copy, $theme_registry);

  $hooks = array('node');

  foreach ($hooks as $h) {

    _mymodule_insert_after_first_element($theme_registry[$h]['theme paths'], $mod_path);

  }

}

 

/**

 * Helper function for re-ordering arrays (needed by theme_registry_alter)

*/

function _mymodule_insert_after_first_element(&$a, $element) {

  if(is_array($a)) {

    $first_element = array_shift($a);

    array_unshift($a, $first_element, $element);

  }

}

hook_theme_registry_alter调用_theme_process_registry这种方式,让我眼前一亮,我的直觉告诉我,这种方式也能够解决我们的问题。开始的时候,我并没有看到它的代码的具体含义,但是我却做了这样的一件事情,认真的阅读了_theme_process_registry这个方法,以及_theme_build_registry,这两个函数里面的代码我读了很多遍,每个函数都认真的读过3遍以上的,弄清楚了函数里面每行代码的作用。我们来看一下_theme_build_registry的代码:

function _theme_build_registry($theme, $base_theme, $theme_engine) {

  $cache = array();

  // First, process the theme hooks advertised by modules. This will

  // serve as the basic registry. Since the list of enabled modules is the same

  // regardless of the theme used, this is cached in its own entry to save

  // building it for every theme.

  if ($cached = cache_get('theme_registry:build:modules')) {

    $cache = $cached->data;

  }

  else {

    foreach (module_implements('theme') as $module) {

      _theme_process_registry($cache, $module, 'module', $module, drupal_get_path('module', $module));

    }

    // Only cache this registry if all modules are loaded.

    if (module_load_all(NULL)) {

      cache_set('theme_registry:build:modules', $cache);

    }

  }

 

  // Process each base theme.

  foreach ($base_theme as $base) {

    // If the base theme uses a theme engine, process its hooks.

    $base_path = dirname($base->filename);

    if ($theme_engine) {

      _theme_process_registry($cache, $theme_engine, 'base_theme_engine', $base->name, $base_path);

    }

    _theme_process_registry($cache, $base->name, 'base_theme', $base->name, $base_path);

  }

 

  // And then the same thing, but for the theme.

  if ($theme_engine) {

    _theme_process_registry($cache, $theme_engine, 'theme_engine', $theme->name, dirname($theme->filename));

  }

 

  // Finally, hooks provided by the theme itself.

  _theme_process_registry($cache, $theme->name, 'theme', $theme->name, dirname($theme->filename));

 

  // Let modules alter the registry.

  drupal_alter('theme_registry', $cache);

 

  // Optimize the registry to not have empty arrays for functions.

  foreach ($cache as $hook => $info) {

    foreach (array('preprocess functions', 'process functions') as $phase) {

      if (empty($info[$phase])) {

        unset($cache[$hook][$phase]);

      }

    }

  }

  return $cache;

}

这是用来构建主题的注册表的,大致的流程如下:

图片1.png

 

    而注册的具体操作,则是委托给了_theme_process_registry。走到这里的时候,我做了一个非常大胆的尝试,我修改了这里的流程,这是我修改后的流程:

图片2.png

        这是我修改后的代码:

function _theme_build_registry($theme, $base_theme, $theme_engine) {

  $cache = array();

  // First, process the theme hooks advertised by modules. This will

  // serve as the basic registry. Since the list of enabled modules is the same

  // regardless of the theme used, this is cached in its own entry to save

  // building it for every theme.

  if ($cached = cache_get('theme_registry:build:modules')) {

    $cache = $cached->data;

  }

  else {

    foreach (module_implements('theme') as $module) {

      _theme_process_registry($cache, $module, 'module', $module, drupal_get_path('module', $module));

    }

    // Only cache this registry if all modules are loaded.

    if (module_load_all(NULL)) {

      cache_set('theme_registry:build:modules', $cache);

    }

  }

  // Let modules alter the registry.

  drupal_alter('theme_registry', $cache);

  // Process each base theme.

  foreach ($base_theme as $base) {

    // If the base theme uses a theme engine, process its hooks.

    $base_path = dirname($base->filename);

    if ($theme_engine) {

      _theme_process_registry($cache, $theme_engine, 'base_theme_engine', $base->name, $base_path);

    }

    _theme_process_registry($cache, $base->name, 'base_theme', $base->name, $base_path);

  }

 

  // And then the same thing, but for the theme.

  if ($theme_engine) {

    _theme_process_registry($cache, $theme_engine, 'theme_engine', $theme->name, dirname($theme->filename));

  }

 

  // Finally, hooks provided by the theme itself.

  _theme_process_registry($cache, $theme->name, 'theme', $theme->name, dirname($theme->filename));

 

  // Optimize the registry to not have empty arrays for functions.

  foreach ($cache as $hook => $info) {

    foreach (array('preprocess functions', 'process functions') as $phase) {

      if (empty($info[$phase])) {

        unset($cache[$hook][$phase]);

      }

    }

  }

  return $cache;

}

然后,我清除缓存,测试,竟然可以了。我竟然通过这种方式把问题解决掉了。这个时候,我认识到了一个问题,基主题的主题函数/模板文件注册这个流程前面,应该放置drupal_alter('theme_registry', $cache);这段代码,或者放置一段这样的代码:

drupal_alter('theme', $cache);

为什么这么说呢? 我们知道,主题引擎、主题,都是位于模块的上层的,虽然在Drupal7里面,模块可以使用hook_theme,主题引擎、主题也可以使用hook_theme。但是由于主题本身位于最上层,所以模板是没有必要通过hook_theme_registry_alter修改主题里面提供的特有的主题函数/模板文件,如果想要修改主题里面的,只需要在主题层直接修改就是了。而主题层的_theme_process_registry,还包含了对模块层已有主题函数/模板文件的覆写功能,这也是包含了的。

在此之前,我还做过这样的调试:

function breadcrumb2_theme_registry_alter(&$theme_registry) {

 //print debug($theme_registry['breadcrumb']);

  if (isset($theme_registry['breadcrumb'])) {

    $path = drupal_get_path('module', 'breadcrumb2');

//$path = path_to_theme();

    $theme_registry['breadcrumb']['path'] = $path;

$theme_registry['breadcrumb']['theme path'] = $path;

    $theme_registry['breadcrumb']['template'] = 'breadcrumb';

    $theme_registry['breadcrumb']['function'] = NULL;

unset($theme_registry['breadcrumb']['function']);

  }

  // print debug($theme_registry['breadcrumb']);

  //print debug($theme_registry['node']);

print debug($theme_registry);

}

就是直接将$theme_registry的结构在前台输出出来,然后观察里面每一元素的结构,同时重点研究了($theme_registry['node']($theme_registry['page']($theme_registry[' breadcrumb ']的输出。这是没有修改核心代码时的输出:

array (

  'variables' => 

  array (

    'breadcrumb' => NULL,

  ),

  'type' => 'module',

  'theme path' => 'sites/all/modules/breadcrumb2',

  'preprocess functions' => 

  array (

    0 => 'template_preprocess_breadcrumb',

  ),

  'process functions' => 

  array (

  ),

  'path' => 'sites/all/modules/breadcrumb2',

  'template' => 'breadcrumb',

)

Debug: 

array (

  'template' => 'page',

  'path' => 'themes/seven',

  'type' => 'theme_engine',

  'theme path' => 'themes/seven',

  'render element' => 'page',

  'preprocess functions' => 

  array (

    0 => 'template_preprocess',

    1 => 'template_preprocess_page',

    2 => 'contextual_preprocess',

    3 => 'overlay_preprocess_page',

    4 => 'shortcut_preprocess_page',

    5 => 'seven_preprocess_page',

  ),

  'process functions' => 

  array (

    0 => 'template_process',

    1 => 'template_process_page',

    2 => 'rdf_process',

  ),

)

我们看到,这里的数组,里面包含的信息远比hook_theme里面包含的多,而对于$theme_registry['page'],从它里面的结构可以看出,已经被当前主题seven给覆写了。


Drupal版本:

2 添加模板建议

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

好吧,关于breadcrumb.tpl.php在当前主题下面的覆写,我们先介绍到这里。我们已经找到了一个解决办法。现在我们来看另外一个问题,我们知道page.tpl.php它是有模板建议的,而我们的breadcrumb.tpl.php现在还没有这个功能,现在就为它添加模板建议。

我们可以直接到template_preprocess_page里面复制相关的代码即可:

function template_preprocess_page(&$variables) {

….

  // Populate the page template suggestions.

  if ($suggestions = theme_get_suggestions(arg(), 'page')) {

    $variables['theme_hook_suggestions'] = $suggestions;

  }

}

 

这是我依葫芦画瓢,复制过来的样子:

/**

 * Implements MODULE_preprocess_HOOK().

 */

function template_preprocess_breadcrumb(&$variables) {

……

  // Populate the breadcrumb template suggestions.

  if ($suggestions = theme_get_suggestions(arg(), 'breadcrumb')) {

    $variables['theme_hook_suggestions'] = $suggestions;

  }

}

我开始的时候,复制粘贴修改的时候,做成了这个样子; '  breadcrumb',结果出不来结果,后来调试,发现多了一个空格。复制粘贴的时候,经常会遇到这样的小问题。

如果,要想为模板设置模板建议的话,我们需要设置$variables['theme_hook_suggestions']这个变量。我们这里调用的是theme_get_suggestions,它能够帮助我们返回一个有关模板建议的数组。在template_preprocess_html里面,也是用的这个函数。这个函数的完整结构

theme_get_suggestions($args, $base, $delimiter = '__')

    它包含三个参数,$args$base$delimiter,我们这里只用到了前两个。我们看一下,三个预处理函数里面的用法:

theme_get_suggestions(arg(), 'breadcrumb')

theme_get_suggestions(arg(), 'page')

theme_get_suggestions(arg(), 'html')

这个时候,我们就能很好的理解$base的含义了,就是基模板文件的名字。arg()返回的是url里面的参数的数组。

现在,我们在bartik\templates下面,创建一个breadcrumb--user.tpl.php的模板文件,里面加点调试信息,来证明是这个模板文件起作用。清除缓存,访问“user”页面,此时调用的就是我们的模板建议breadcrumb--user.tpl.php了。

我们这里的模板建议规则,和page.tpl.php的保持一致,这是因为,breadcrumb.tpl.php也是基于路径的,这和页面模板文件一样。在block.tpl.php的预处理函数中,模板建议是这样定义的:

  $variables['theme_hook_suggestions'][] = 'block__' . $variables['block']->region;

  $variables['theme_hook_suggestions'][] = 'block__' . $variables['block']->module;

  $variables['theme_hook_suggestions'][] = 'block__' . $variables['block']->module . '__' . strtr($variables['block']->delta, '-', '_');

   它是逐个定义模板建议规则的。


Drupal版本:

3 template_preprocess_breadcrumb?

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

    我们前面忘记介绍了一点,就是什么时候改用template_preprocess_breadcrumb了,以前我们都是用的breadcrumb2_preprocess_breadcrumb,在调试breadcrumb.tpl.php无法在当前主题下面覆写的时候,我就把它改过来了。

    无论是使用breadcrumb2_preprocess_breadcrumb,还是使用template_preprocess_breadcrumb,在我们这里,作用都是一样的。为什么这里要把breadcrumb2这个前缀,改为template呢?

我是这样想的,在node.module文件里面,他们没有使用node_preprocess_node,而是使用的template_preprocess_node;同样,在block.module文件里面,用的是template_preprocess_blockDrupal核心是这样用的,所以我这里遵守了Drupal核心的习惯。

现在,我思考这个问题的时候,我又有了新的发现,如果我们这里面,使用breadcrumb2_preprocess_breadcrumb的话,如果存在这样的模块,比如abc,它也实现这个预处理函数abc_preprocess_breadcrumb,这个函数就会放到breadcrumb2_preprocess_breadcrumb前面执行。在breadcrumb2.module文件中,我们是为breadcrumb.tpl.php模板文件提供一些默认变量的,所以这里使用template打头,更方便其它模块的覆写。

我们这里介绍一下,预处理/处理函数的执行顺序。这里以breadcrumb.tpl.php为例:

 

1 template_preprocess(&$variables, $hook)这是系统默认的预处理函数。

2template_preprocess_breadcrumb(&$variables)breadcrumb2.moudle实现,为breadcrumb.tpl.php提供默认的变量。

3MODULE_preprocess(&$variables, $hook):调用所有模块上的 hook_preprocess()实现。

4) MODULE_preprocess_breadcrumb(&$variables)调用hook_preprocess_breadcrumb() ,这样允许其它模块可以修改breadcrumb.tpl.php的默认变量。

5) ENGINE_engine_preprocess(&$variables, $hook)允许主题引擎为所有的主题钩子设置变量。

6ENGINE_engine_preprocess_breadcrumb(&$variables)允许主题引擎为breadcrumb设置变量。

7THEME_preprocess(&$variables, $hook)允许主题为所有的主题钩子设置变量。

8THEME_preprocess_breadcrumb(&$variables)允许主题为breadcrumb设置变量

9template_process(&$variables, $hook):为所有的主题钩子添加一些附加变量。

10template_process_breadcrumb(&$variables)我们这里没有实现这个函数

11MODULE_process(&$variables, $hook):触发钩子hook_process() 

12MODULE_process_breadcrumb(&$variables):触发钩子 hook_process_HOOK() ,允许第三方模块为breadcrumb添加附加变量。

13ENGINE_engine_process(&$variables, $hook)允许主题引擎为所有主题钩子添加附加变量

14) ENGINE_engine_process_breadcrumb(&$variables)允许主题引擎为breadcrumb添加附加变量.

15) THEME_process(&$variables, $hook):  允许主题为所有主题钩子添加附加变量

16) THEME_process_breadcrumb(&$variables):  允许主题为breadcrumb添加附加变量

    

从这个执行顺序上面来看,由于breadcrumb2模块提供了breadcrumb.tpl.php,所以我们需要为breadcrumb.tpl.php提供默认变量。template_preprocess_breadcrumb在这里也就更合适一点。

    我们这里列的预处理函数比较多,实际上系统会把要执行的预处理函数、处理函数,缓存到主题注册表里面,这样在执行的时候,就不用去查找有多少个预处理函数需要执行了。

    如果,我们没有把theme_breadcrumb改造为模板文件的形式,此时的执行预处理函数,只有下面的四个函数,可被调用:

 

THEME_preprocess(&$variables, $hook)允许主题为所有的主题钩子设置变量。

THEME_preprocess_breadcrumb(&$variables)允许主题为breadcrumb设置变量

THEME_process(&$variables, $hook):  允许主题为所有主题钩子添加附加变量

THEME_process_breadcrumb(&$variables):  允许主题为breadcrumb添加附加变量

这样的好处就是,速度快,不需要调用太多的预处理函数。主题函数与模板文件相比,一个很大的优势,就是主题函数的速度快,大约快10倍左右。模板文件的优势时,方便不懂PHP的美工修改这个文件,开发时比较方便。


Drupal版本:

5 drupal_static

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

我们在阅读Drupal核心代码的时候,可以经常碰到这个函数drupal_static,它的作用是,能够将一个变量缓存起来,在同一HTTP请求期间,重复调用这个变量,不会计算第二次。就是说,如果有这么一个变量,它在一个HTTP请求内,可以被调用多次的话,我们可以只计算一次,然后使用drupal_static将它缓存起来。

我想为breadcrumb2_load_by_path函数,使用drupal_static。这是没有使用drupal_static的代码:

function breadcrumb2_load_by_path($path) {

  $breadcrumbs = breadcrumb2_load_multiple(FALSE, array('path' => $path));

  return reset($breadcrumbs);

}

这是使用后的代码:

function breadcrumb2_load_by_path($path) {

  $cache = &drupal_static(__FUNCTION__, array());

  if (!isset($cache[$path])) {

    $breadcrumbs = breadcrumb2_load_multiple(FALSE, array('path' => $path));

    $cache[$path] = reset($breadcrumbs);

    return $cache[$path];

  }

  return $cache[$path];

}

我对这个函数,开始的时候,最不理解的地方就是__FUNCTION__,后来才弄明白,这是一个PHP常量,上面的代码等价于:

$cache = &drupal_static('breadcrumb2_load_by_path', array());

类似的实现,可以参看Profile2模块里面的profile2_load_by_user,它的这个要复杂很多:

function profile2_load_by_user($account, $type_name = NULL) {

  // Use a separate query to determine all profile ids per user and cache them.

  // That way we can look up profiles by id and benefit from the static cache

  // of the entity loader.

  $cache = &drupal_static(__FUNCTION__, array());

  $uid = is_object($account) ? $account->uid : $account;

 

  if (!isset($cache[$uid])) {

    if (empty($type_name)) {

      $profiles = profile2_load_multiple(FALSE, array('uid' => $uid));

      // Cache ids for further lookups.

      $cache[$uid] = array();

      foreach ($profiles as $pid => $profile) {

        $cache[$uid][$profile->type] = $pid;

      }

      return $profiles ? array_combine(array_keys($cache[$uid]), $profiles) : array();

    }

    $cache[$uid] = db_select('profile', 'p')

      ->fields('p', array('type', 'pid'))

      ->condition('uid', $uid)

      ->execute()

      ->fetchAllKeyed();

  }

  if (isset($type_name)) {

    return isset($cache[$uid][$type_name]) ? profile2_load($cache[$uid][$type_name]) : FALSE;

  }

  // Return an array containing profiles keyed by profile type.

  return $cache[$uid] ? array_combine(array_keys($cache[$uid]), profile2_load_multiple($cache[$uid])) : $cache[$uid];

}

其实这里的逻辑很简单,检查静态变量里面,是否已经存在,如果存在,直接返回;如果不存在,则重新生成一遍。我们这里向函数传递了参数,如果没有传递参数,代码结构会更加简单:

function mymodule_function() {

  $cache = &drupal_static(__FUNCTION__);

  if (!isset($cache)) {

    // 如果没有设置,就计算一遍,为$cache赋值。

  }

  return $ cache;

}


Drupal版本:

6 在保存面包屑时,更新对应的静态缓存

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

前面,我们讲了如何使用drupal_static,新的问题出来了,如果在一个HTTP内,如果我们更新的对应的面包屑对象,更新了之后,再去调用breadcrumb2_load_by_path函数,有可能返回的还是最初的面包屑对象。所以我们有必要在保存面包屑对象的时候,也修改一下这个静态缓存。

class Breadcrumb extends Entity {

….

  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();

    // Update the static cache from breadcrumb2_load_by_path().

    $cache = &drupal_static('breadcrumb2_load_by_path', array());

    if (isset($cache[$this->path])) {

      $cache[$this->path] = $this;

    }

  }

}

这是对面包屑对象保存,改进后的代码,就是我们在保存一个面包屑对象后,对对应的静态缓存也做了更新。

有兴趣的读者,可以在breadcrumb2_load_by_path里面加上几句drupal_set_message这样的调试代码,观察一下,同一个HTTP请求内,IF语句里面的代码被调用了多少次。


Drupal版本:

7 不修改Drupal核心代码

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

我们在前面通过修改Drupal核心代码,实现了在主题层也能覆写breadcrumb.tpl.php这个模版文件。在实现这个功能以后,我就想到了,其实不修改核心代码,也能够在我们的模块中实现这个功能。就是说,我们把_theme_build_registry里面的基主题、主题引擎、主题的_theme_process_registry的调用,在breadcrumb2_theme_registry_alter里面,重新调用执行一遍就可以了。

开始的时候,我是这样写的代码:

function breadcrumb2_theme_registry_alter(&$theme_registry) {

  if (isset($theme_registry['breadcrumb'])) {

    $path = drupal_get_path('module', 'breadcrumb2');

//$path = path_to_theme();

    $theme_registry['breadcrumb']['path'] = $path;

$theme_registry['breadcrumb']['theme path'] = $path;

    $theme_registry['breadcrumb']['template'] = 'breadcrumb';

    $theme_registry['breadcrumb']['function'] = NULL;

unset($theme_registry['breadcrumb']['function']);

  }

  $themes = list_themes();

  foreach ($themes as $theme) {

    _theme_process_registry($theme_registry, $theme->name, 'theme', $theme->name, dirname($theme->filename));

//print debug($theme);

  }

}

我当时这样想的,把所有启用的主题,分别重新注册一遍,这样它们在注册的时候,就可以覆写我们的模版文件了。就是把所有主题的_theme_process_registry,放到后面,重新执行一遍。

但是很遗憾,还是不行。当时,我就有了一个疑问,什么疑问呢?就是说,在_theme_build_registry里面,这句话是这样写的:

  // Finally, hooks provided by the theme itself.

  _theme_process_registry($cache, $theme->name, 'theme', $theme->name, dirname($theme->filename));

 

    这里面,没有把所有的主题都列出来,分别处理一下,而是处理了一个。我当时特别想知道,这个$theme是从哪里来的,我能不能在breadcrumb2_theme_registry_alter里面直接调用这个变量?很遗憾,在breadcrumb2_theme_registry_alter,是没有将这个变量传递过来的。这个时候,对代码,进行了分析。这里的$theme是从函数的参数里面传递过来的:

function _theme_build_registry($theme, $base_theme, $theme_engine) 

    

都有哪些函数,调用了函数_theme_build_registry,我通过搜索查找,这个函数的调用,只出现在了_theme_load_registry函数里面:

function _theme_load_registry($theme, $base_theme = NULL, $theme_engine = NULL, $complete = TRUE) {

  if ($complete) {

    // Check the theme registry cache; if it exists, use it.

    $cached = cache_get("theme_registry:$theme->name");

    if (isset($cached->data)) {

      $registry = $cached->data;

    }

    else {

      // If not, build one and cache it.

      $registry = _theme_build_registry($theme, $base_theme, $theme_engine);

      // Only persist this registry if all modules are loaded. This assures a

      // complete set of theme hooks.

      if (module_load_all(NULL)) {

        _theme_save_registry($theme, $registry);

      }

    }

    return $registry;

  }

  else {

    return new ThemeRegistry('theme_registry:runtime:' . $theme->name, 'cache');

  }

}

    而这个$theme,也来自于_theme_load_registry函数本身的参数传递。通过搜索查找,我在theme.inc文件里面,找到两个地方,包含这个函数:

function _drupal_theme_initialize($theme, $base_theme = array(), $registry_callback = '_theme_load_registry') {

还有一处是:

function theme($hook, $variables = array()) {

  // If called before all modules are loaded, we do not necessarily have a full

  // theme registry to work with, and therefore cannot process the theme

  // request properly. See also _theme_load_registry().

  if (!module_load_all(NULL) && !defined('MAINTENANCE_MODE')) {

    throw new Exception(t('theme() may not be called until all modules are loaded.'));

  }

    通过阅读_drupal_theme_initialize,我们知道这个函数最终调用了_theme_load_registry,而$theme也是从这个函数的参数里面传递过来的。

function _drupal_theme_initialize($theme, $base_theme = array(), $registry_callback = '_theme_load_registry') {

  global $theme_info, $base_theme_info, $theme_engine, $theme_path;

  $theme_info = $theme;

  $base_theme_info = $base_theme;

 

  $theme_path = dirname($theme->filename);

……

  if (isset($registry_callback)) {

    _theme_registry_callback($registry_callback, array($theme, $base_theme, $theme_engine));

  }

}

_drupal_theme_initialize里面的$theme,则是从drupal_theme_initialize传递过来的 。drupal_theme_initialize函数本身没有参数,

function drupal_theme_initialize() {

  global $theme, $user, $theme_key;

 

  // If $theme is already set, assume the others are set, too, and do nothing

  if (isset($theme)) {

    return;

  }

 

  drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE);

  $themes = list_themes();

 

  // Only select the user selected theme if it is available in the

  // list of themes that can be accessed.

  $theme = !empty($user->theme) && drupal_theme_access($user->theme) ? $user->theme : variable_get('theme_default', 'bartik');

 

  // Allow modules to override the theme. Validation has already been performed

  // inside menu_get_custom_theme(), so we do not need to check it again here.

  $custom_theme = menu_get_custom_theme();

  $theme = !empty($custom_theme) ? $custom_theme : $theme;

 

  // Store the identifier for retrieving theme settings with.

  $theme_key = $theme;

 

  // Find all our ancestor themes and put them in an array.

  $base_theme = array();

  $ancestor = $theme;

  while ($ancestor && isset($themes[$ancestor]->base_theme)) {

    $ancestor = $themes[$ancestor]->base_theme;

    $base_theme[] = $themes[$ancestor];

  }

  _drupal_theme_initialize($themes[$theme], array_reverse($base_theme));

 

  // Themes can have alter functions, so reset the drupal_alter() cache.

  drupal_static_reset('drupal_alter');

 

  // Provide the page with information about the theme that's used, so that a

  // later Ajax request can be rendered using the same theme.

  // @see ajax_base_page_theme()

  $setting['ajaxPageState'] = array(

    'theme' => $theme_key,

    'theme_token' => drupal_get_token($theme_key),

  );

  drupal_add_js($setting, 'setting');

}

这里面,我们的$theme来自于$themes[$theme],而global $theme返回的则是当前主题的机读名字。

drupal_theme_initialize,主要是在theme_get_registry函数中被调用的;而theme_get_registry,则主要是在theme()里面调用:

function theme($hook, $variables = array()) {

  // If called before all modules are loaded, we do not necessarily have a full

  // theme registry to work with, and therefore cannot process the theme

  // request properly. See also _theme_load_registry().

  if (!module_load_all(NULL) && !defined('MAINTENANCE_MODE')) {

    throw new Exception(t('theme() may not be called until all modules are loaded.'));

  }

 

  $hooks = theme_get_registry(FALSE);

….

我们画个流程图:

图片1.png


                    

弄明白这个结构,不容易啊。但是,我还没有完全不明白,一个Drupal站点里面,包含多个Drupal主题,每个主题的注册表是怎么注册的呢?

我们都知道,点击性能页面的清除缓存按钮,就可以清除所有的缓存。那个时候,肯定也会清除主题注册表的缓存。我们平时,添加一个新模版文件的时候,总是要清除缓存才会起作用的。我们从这个路径入手,根据admin/config/development/performance,从system.module里面,找到system_menu这个钩子函数。我们找到对应菜单项的定义:

  $items['admin/config/development/performance'] = array(

    'title' => 'Performance',

    'description' => 'Enable or disable page caching for anonymous users and set CSS and JS bandwidth optimization options.',

    'page callback' => 'drupal_get_form',

    'page arguments' => array('system_performance_settings'),

    'access arguments' => array('administer site configuration'),

    'file' => 'system.admin.inc',

    'weight' => -20,

  );

根据这个信息,在'system.admin.inc'文件中找到'system_performance_settings'

function system_performance_settings() {

  drupal_add_js(drupal_get_path('module', 'system') . '/system.js');

 

  $form['clear_cache'] = array(

    '#type' => 'fieldset',

    '#title' => t('Clear cache'),

  );

 

  $form['clear_cache']['clear'] = array(

    '#type' => 'submit',

    '#value' => t('Clear all caches'),

    '#submit' => array('system_clear_cache_submit'),

  );

……

  return system_settings_form($form);

}

    我们找到了对应的提交处理函数system_clear_cache_submit,然后查看这个提交函数的源代码:

function system_clear_cache_submit($form, &$form_state) {

  drupal_flush_all_caches();

  drupal_set_message(t('Caches cleared.'));

}

代码很简单,主要是drupal_flush_all_caches函数的调用。我们在当前页面搜索这个函数,没有找到它的定义。通过Google搜索一下,这个Drupal API函数。这个函数定义在includes/common.inc里面:

function drupal_flush_all_caches() {

  // Change query-strings on css/js files to enforce reload for all users.

  _drupal_flush_css_js();

 

  registry_rebuild();

  drupal_clear_css_cache();

  drupal_clear_js_cache();

 

  // Rebuild the theme data. Note that the module data is rebuilt above, as

  // part of registry_rebuild().

  system_rebuild_theme_data();

  drupal_theme_rebuild();

 

  entity_info_cache_clear();

  node_types_rebuild();

  // node_menu() defines menu items based on node types so it needs to come

  // after node types are rebuilt.

  menu_rebuild();

 

  // Synchronize to catch any actions that were added or removed.

  actions_synchronize();

 

  // Don't clear cache_form - in-progress form submissions may break.

  // Ordered so clearing the page cache will always be the last action.

  $core = array('cache', 'cache_path', 'cache_filter', 'cache_bootstrap', 'cache_page');

  $cache_tables = array_merge(module_invoke_all('flush_caches'), $core);

  foreach ($cache_tables as $table) {

    cache_clear_all('*', $table, TRUE);

  }

 

  // Rebuild the bootstrap module list. We do this here so that developers

  // can get new hook_boot() implementations registered without having to

  // write a hook_update_N() function.

  _system_update_bootstrap_status();

}

这段代码,建议大家认真的读两遍,这里涉及到CSSJS、主题、Entity、菜单,以及Drupal核心缓存表中缓存的清除工作。而我们这里关心的是:

  system_rebuild_theme_data();

  drupal_theme_rebuild();

这里面的system_rebuild_theme_data,是负责构建主题的info文件里面的数据的,有兴趣的可以继续沿着这个方向阅读对应的源代码。我们这里略过这个函数。drupal_theme_rebuild是用来重新构建Drupal的主题注册表的。我们来看这个函数,这个函数位于includes/theme.inc中:

function drupal_theme_rebuild() {

  drupal_static_reset('theme_get_registry');

  cache_clear_all('theme_registry', 'cache', TRUE);

}

我们看到了什么?这里清除了theme_get_registry以及theme_registry的所有缓存数据。但是这里面并没有重建这些数据。这个时候,我开始快明白了。

这个时候,我又做了一个这样的试验,就是在breadcrumb2_theme_registry_alter里面加上这样的调试信息

function breadcrumb2_theme_registry_alter(&$theme_registry) {

 print debug($theme_registry['page']);

….

}

然后清除缓存。我得到了这样的调试信息:

Debug: 

array (

  'template' => 'page',

  'path' => 'themes/seven',

  'type' => 'theme_engine',

  'theme path' => 'themes/seven',

  'render element' => 'page',

  'preprocess functions' => 

  array (

    0 => 'template_preprocess',

    1 => 'template_preprocess_page',

    2 => 'contextual_preprocess',

    3 => 'overlay_preprocess_page',

    4 => 'shortcut_preprocess_page',

    5 => 'seven_preprocess_page',

  ),

  'process functions' => 

  array (

    0 => 'template_process',

    1 => 'template_process_page',

    2 => 'rdf_process',

  ),

)

接着,我访问首页,我看到了新的调试信息:

array (

  'template' => 'page',

  'path' => 'themes/bartik/templates',

  'type' => 'theme_engine',

  'theme path' => 'themes/bartik',

  'render element' => 'page',

  'preprocess functions' => 

  array (

    0 => 'template_preprocess',

    1 => 'template_preprocess_page',

    2 => 'contextual_preprocess',

    3 => 'overlay_preprocess_page',

    4 => 'shortcut_preprocess_page',

  ),

  'process functions' => 

  array (

    0 => 'template_process',

    1 => 'template_process_page',

    2 => 'rdf_process',

    3 => 'bartik_process_page',

  ),

)

这个时候,我明白了一个道理,Drupal主题的注册表缓存数据,是在第一次需要的时候,才生成这些数据的。我们这里用到了Sevenbartik两个主题,对于其它Drupal主题,比如Garland,假如我们启用了它,但是没有页面使用这个主题的话,就不会为它缓存主题注册表数据。而不是我刚开始猜测的,为所有启用的主题,在清除缓存的时候,就重新生成一遍主题注册表里面的数据。

这一点和ImageCacheImage style),Boost模块的机制是一样的。归纳一下,就是说,当需要这个东西的时候,首先检查缓存,看是否存在,如果存在就使用缓存数据,如果不存在,系统就会重新生成一遍,并把数据缓存起来。在需要的时候,才会去缓存。不会去缓存用不到的数据。

至此,我通过对Drupal核心代码的研读,终于明白了主题机制背后的原理。这是改进后的代码:

function breadcrumb2_theme_registry_alter(&$theme_registry) {

 print debug($theme_registry['page']);

  if (isset($theme_registry['breadcrumb'])) {

    $path = drupal_get_path('module', 'breadcrumb2');

//$path = path_to_theme();

    $theme_registry['breadcrumb']['path'] = $path;

$theme_registry['breadcrumb']['theme path'] = $path;

    $theme_registry['breadcrumb']['template'] = 'breadcrumb';

    $theme_registry['breadcrumb']['function'] = NULL;

unset($theme_registry['breadcrumb']['function']);

  }

  global $theme;

  $themes = list_themes();

  $theme_obj = $themes[$theme];

  //print debug($themes[$theme]);

   _theme_process_registry($theme_registry, $theme_obj->name, 'theme', $theme_obj->name, dirname($theme_obj->filename));

}

清除缓存,测试,仍然没有起作用。我们最后一次修改核心代码:

function _theme_build_registry($theme, $base_theme, $theme_engine) {

  $cache = array();

  // First, process the theme hooks advertised by modules. This will

  // serve as the basic registry. Since the list of enabled modules is the same

  // regardless of the theme used, this is cached in its own entry to save

  // building it for every theme.

  if ($cached = cache_get('theme_registry:build:modules')) {

    $cache = $cached->data;

  }

  else {

    foreach (module_implements('theme') as $module) {

      _theme_process_registry($cache, $module, 'module', $module, drupal_get_path('module', $module));

    }

    // Only cache this registry if all modules are loaded.

    if (module_load_all(NULL)) {

      cache_set('theme_registry:build:modules', $cache);

    }

  }

  // Let modules alter the registry.

  drupal_alter('theme_registry', $cache);

  print debug($cache['breadcrumb']);

  // Process each base theme.

  foreach ($base_theme as $base) {

    // If the base theme uses a theme engine, process its hooks.

    $base_path = dirname($base->filename);

    if ($theme_engine) {

      _theme_process_registry($cache, $theme_engine, 'base_theme_engine', $base->name, $base_path);

    }

    _theme_process_registry($cache, $base->name, 'base_theme', $base->name, $base_path);

  }

  print debug($cache['breadcrumb']);

  // And then the same thing, but for the theme.

  if ($theme_engine) {

    _theme_process_registry($cache, $theme_engine, 'theme_engine', $theme->name, dirname($theme->filename));

  }

  print debug($cache['breadcrumb']);

  // Finally, hooks provided by the theme itself.

  _theme_process_registry($cache, $theme->name, 'theme', $theme->name, dirname($theme->filename));

  print debug($cache['breadcrumb']);

  // Let modules alter the registry.

  //drupal_alter('theme_registry', $cache);

 

  // Optimize the registry to not have empty arrays for functions.

  foreach ($cache as $hook => $info) {

    foreach (array('preprocess functions', 'process functions') as $phase) {

      if (empty($info[$phase])) {

        unset($cache[$hook][$phase]);

      }

    }

  }

  return $cache;

}

这里面,我加入了四个print debug($cache['breadcrumb']);,我想看看是在哪个阶段覆写的。清除缓存,然后再访问首页,我们看到这样的调试信息:

array (

  'variables' => 

  array (

    'breadcrumb' => NULL,

  ),

  'type' => 'module',

  'theme path' => 'sites/all/modules/breadcrumb2',

  'preprocess functions' => 

  array (

    0 => 'template_preprocess_breadcrumb',

  ),

  'process functions' => 

  array (

  ),

  'path' => 'sites/all/modules/breadcrumb2',

  'template' => 'breadcrumb',

)in _theme_build_registry() (line 695 of D:\xampp\htdocs\breadcrumb2\includes\theme.inc). 

Debug: array (

  'variables' => 

  array (

    'breadcrumb' => NULL,

  ),

  'type' => 'module',

  'theme path' => 'sites/all/modules/breadcrumb2',

  'preprocess functions' => 

  array (

    0 => 'template_preprocess_breadcrumb',

  ),

  'process functions' => 

  array (

  ),

  'path' => 'sites/all/modules/breadcrumb2',

  'template' => 'breadcrumb',

)in _theme_build_registry() (line 705 of D:\xampp\htdocs\breadcrumb2\includes\theme.inc). 

Debug: array (

  'template' => 'breadcrumb',

  'path' => 'themes/bartik/templates',

  'type' => 'theme_engine',

  'theme path' => 'themes/bartik',

  'variables' => 

  array (

    'breadcrumb' => NULL,

  ),

  'preprocess functions' => 

  array (

    0 => 'template_preprocess_breadcrumb',

  ),

  'process functions' => 

  array (

  ),

)in _theme_build_registry() (line 710 of D:\xampp\htdocs\breadcrumb2\includes\theme.inc). 

Debug: array (

  'template' => 'breadcrumb',

  'path' => 'themes/bartik/templates',

  'type' => 'theme_engine',

  'theme path' => 'themes/bartik',

  'variables' => 

  array (

    'breadcrumb' => NULL,

  ),

  'preprocess functions' => 

  array (

    0 => 'template_preprocess_breadcrumb',

  ),

  'process functions' => 

  array (

  ),

)

    从调试信息里面可以看出,是在第3print debug前面的一个阶段,完成了覆写工作。所以我们的代码应该这样写:

function breadcrumb2_theme_registry_alter(&$theme_registry) {

  //print debug($theme_registry['page']);

  if (isset($theme_registry['breadcrumb'])) {

    $path = drupal_get_path('module', 'breadcrumb2');

//$path = path_to_theme();

    $theme_registry['breadcrumb']['path'] = $path;

$theme_registry['breadcrumb']['theme path'] = $path;

    $theme_registry['breadcrumb']['template'] = 'breadcrumb';

    $theme_registry['breadcrumb']['function'] = NULL;

unset($theme_registry['breadcrumb']['function']);

  }

  global $theme;

  $themes = list_themes();

  $theme_obj = $themes[$theme];

  //print debug($themes[$theme]);

  _theme_process_registry($theme_registry, 'phptemplate', 'theme_engine', $theme_obj->name, dirname($theme_obj->filename));

  //_theme_process_registry($theme_registry, $theme_obj->name, 'theme', $theme_obj->name, dirname($theme_obj->filename));

}

我们把主题引擎的搬了过来。由于Drupal7下面,只有一个phptemplate主题引擎,所以这里直接写死在里面了。然后我们把theme.inc文件里面,修改过的代码,改回原状。恢复到Drupal核心最初的样子即可。

清除缓存,测试,成功了。其实我们这里面,只加了四行代码,但是为了加上这四行代码,我们对Drupal核心的主题机制,做了非常认真的分析,彻底搞明白了背后的基本原理。在实际的开发中,我从来不鼓励别人修改Drupal核心的代码,但是在练习当中,在学习实践当中,我总是随心所欲的在Drupal核心代码里面加上print debug,以追踪Drupal核心的执行流程。我这里介绍的是我的土办法,如果你有更好的工具,也可以使用你集成环境里面的代码逐行调试功能。

此外,我们的Think in Drupal4集也就写到这里了。加上以前所写的Field validation的历程,Drupal核心模板文件里面变量的含义说明,还有Drupal开发最佳实践什么的。

本来还想写JavaScript、单元测试、安装包profile的开发,考虑到Javascript本身的变化不大,而且我们阅读了contextula模块的代码,里面有相关的JS可供参考;单元测试、安装包profile的开发在中国,没有公司用的,所以就将这些知识点的介绍推后了。下面就是把所有的这段时间编写的,合并到一起,打印出来,然后,自己认真的读一遍,修正里面的文字错误,修正完以后的版本,自己拿去打印店打印,开始taobao上面出售了。就这样的流程。

20121231号的1724分,完成了Think in Drupal的写作。就是说,当别人放假,出去玩的时候,我们还在整理着这样的资料。

 



Drupal版本:

4为模板新增变量$classes

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

我们其实在前面已经讲过了,如何新增一个变量。不过我们在Drupal核心的模板文件里面,经常看到这样的代码:

<div class="<?php print $classes; ?>">

而在我们现在的模板文件中,则是这样写的:

<div class="breadcrumb-wrapper contextual-links-region">

这里的class是写死在里面的,我们也想把它改造为变量的形式,好的,我们修改预处理函数里面的代码,这是修改后的样子:

function template_preprocess_breadcrumb(&$variables) {

  $variables['classes_array'][] = 'breadcrumb-wrapper';

  // Populate the breadcrumb classes.

  if ($suggestions = theme_get_suggestions(arg(), 'breadcrumb', '-')) {

    foreach ($suggestions as $suggestion) {

      if ($suggestion != 'breadcrumb-front') {

        // Add current suggestion to breadcrumb classes to make it possible to theme

        // the breadcrumb depending on the current page type (e.g. node, admin, user,

        // etc.) as well as more specific data like node-12 or node-edit.

        $variables['classes_array'][] = drupal_html_class($suggestion);

      }

    }

  }

  $current_path = current_path();

  $breadcrumb2 = breadcrumb2_load_by_path($current_path);

  if(!empty($breadcrumb2)){

    $breadcrumbs = array();

      // Only keep the first one. 

    if (!empty($variables['breadcrumb'][0])) {

      $breadcrumbs[] = $variables['breadcrumb'][0];

    }

    $wrapper = entity_metadata_wrapper('breadcrumb2', $breadcrumb2);

    $breadcrumb_links = $wrapper->link->value();

    foreach($breadcrumb_links as $breadcrumb_link){

      $breadcrumbs[]= l($breadcrumb_link['title'], $breadcrumb_link['url']);

    }

$variables['breadcrumb'] = $breadcrumbs;

  }

 

  if (user_access('administer breadcrumbs') && user_access('access contextual links')) {

    $contextual_links_element   =    array(

      '#contextual_links' => array(

        'breadcrumb2' => array('breadcrumb', array('0')),

      )

    ); 

 

    $contextual_links = array(

      '#type' => 'contextual_links',

      '#contextual_links' => $contextual_links_element['#contextual_links'],

      '#element' => $contextual_links_element,

    );

    $variables['contextual_links'] = drupal_render($contextual_links);

$variables['classes_array'][] = 'contextual-links-region';

  }

 

  // Populate the breadcrumb template suggestions.

  if ($suggestions = theme_get_suggestions(arg(), 'breadcrumb')) {

    $variables['theme_hook_suggestions'] = $suggestions;

  }

  

  // Flatten out classes.

  $variables['classes'] = implode(' ', $variables['classes_array']);

}

开始的时候,我没有添加$variables['classes']相关的代码,我是这样想的,系统会调用template_process函数,而在这个函数中,会增加附加的变量classes,不过我想错了。默认处理函数的代码很简单:

function template_process(&$variables, $hook) {

  // Flatten out classes.

  $variables['classes'] = implode(' ', $variables['classes_array']);

 

  // Flatten out attributes, title_attributes, and content_attributes.

  // Because this function can be called very often, and often with empty

  // attributes, optimize performance by only calling drupal_attributes() if

  // necessary.

  $variables['attributes'] = $variables['attributes_array'] ? drupal_attributes($variables['attributes_array']) : '';

  $variables['title_attributes'] = $variables['title_attributes_array'] ? drupal_attributes($variables['title_attributes_array']) : '';

  $variables['content_attributes'] = $variables['content_attributes_array'] ? drupal_attributes($variables['content_attributes_array']) : '';

}

这里面为我们提供了变量$variables['classes']。但是实际上对于breadcrumb.tpl.php,却没有调用这个处理函数。所以我将这里的代码直接复制到了预处理函数中了。

注意,这里面$variables['classes_array']是一个数组,$variables['classes']是一个字符串。为了生成classes_array这个数组,我们还使用了drupal_html_class这个函数,这些都是从Drupal核心模板的预处理函数中复制过来的。

这是现在的模板文件:

<?php if (!empty($breadcrumb)): ?>

<div class="<?php print $classes; ?>">

  <h2 class="element-invisible">   <?php print t('You are here'); ?>  </h2>

   <?php if (!empty($contextual_links)): ?> 

    <div style='height:10px'> <?php print $contextual_links; ?> </div>

  <?php endif; ?>

  <?php //print drupal_render($contextual_link); ?>

  <div class="breadcrumb"> <?php print implode(' » ', $breadcrumb); ?> </div>

</div>

<?php endif; ?>


Drupal版本:

第6章 Drupal核心模板文件

第6章 Drupal核心模板文件

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

Drupal版本:

1 html.tpl.php

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

在单个Drupal页面中这个模板用来显示基本HTML结构的。它包含以下可用变量:

$css:当前页面的CSS文件数组。

$language:站点正在使用的语言。它是一个对象,$language->language包含了它的文字表述;$language->dir包含了语言的方向,可以使'ltr'(从左到右),或者是'rtl'(从右到左)。

$rdf_namespaces:用在HTML文档中的所有RDF命名空间前缀。

$grddl_profile:一个GRDDL profile(轮廓),用于允许代理提取RDF数据。

$head_title:页面标题修改后的版本,用在TITLE标签中。

$head_title_array: $head_title变量的关联数组版本,$head_title变量就是基于它生成的,Drupal对它已经做过预处理,所以可用它来输出成TITLE标签。根据实际情况,键值对儿可以包含一个或多个以下元素:title,当前页面的标题;name,网站的名字;slogan:网站的口号。

$head: HEAD部分的标记文本,包括meta标签、keyword标签等等。

 

$styles:样式标签,用来导入当前页面的所有CSS文件。

$scripts:脚本标签,用来为当前页面加载所有的JavaScript文件和设置。

$page_top最前面的标记文本,这个变量的内容来自于修改该页面的第三方模块。从名字便可以看出,这个变量应该放在其它动态内容之前输出。

 

$page:呈现的页面内容。

$page_bottom:最后的结束标记文本,这个变量的内容来自于修改该页面的第三方模块。这个变量应该放在其它动态内容之后输出。

$classes类字符串,用于通过CSS定制样式。

 


Drupal版本:

10 node.tpl.php默认模板建议

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

 它的模板建议规则,首先按照节点ID来定义的,然后使用节点类型。我们还以node/1/edit为例,假定当前节点类型为article

   node--1.tpl.php         

   node--story.tpl.php

node.tpl.php


Drupal版本:

11 block.tpl.php默认变量

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

block.tpl.php默认变量

Drupal版本:

11.1 可用变量:

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

$block->subject区块标题。

$content区块内容。

$block->module生成该区块的模块。

$block->delta区块的ID,在其所在的模块中是唯一的。

$block->region当前区块所在的区域。

$classes: CSS类字符串。可以在预处理函数中,通过修改$classes_array的值,来修改$classes的值。默认值有:blockblock-[module]。需要注意的是,这里的[module]表示生成该区块的模块,例如对于默认的用户导航区块,它是由用户(user)模块生成的,那么此时对应的CSS类就是"block-user"

$title_prefix (array): 在模板的主标题标签前面显示的内容,注意它是数组结构,在模板中输出时,使用render()输出。

$title_suffix (array): 在模板的主标题标签后面显示的内容,注意它是数组结构,在模板中输出时,使用render()输出。


Drupal版本:

11.2 辅助变量:

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

$classes_array一个数组,里面包含了CSS类属性的值。$classes就是基于$classes_array生成的。

$block_zebra区块斑马线,可用值有'odd''even',依赖于当前区域。

$zebra区块斑马线,可用值有'odd''even',独立于任何区域。

$block_id计数器,依赖于当前区域。

$id计数器,独立于任何区域。

$is_front标记,用来表示是否显示在首页。

$logged_in标记,用来表示当前用户是否是登录用户。

$is_admin标记,用来表示当前用户是不是管理员。

$block_html_id一个有效的唯一的HTML ID

 

这些变量来自于:

template_preprocess()

template_preprocess_block()

template_process()


Drupal版本:

12 block.tpl.php默认模板文件

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

<div id="<?php print $block_html_id; ?>" class="<?php print $classes; ?>"<?php print $attributes; ?>>

 

  <?php print render($title_prefix); ?>

<?php if ($block->subject): ?>

  <h2<?php print $title_attributes; ?>><?php print $block->subject ?></h2>

<?php endif;?>

  <?php print render($title_suffix); ?>

  <div class="content"<?php print $content_attributes; ?>>

    <?php print $content ?>

  </div>

</div>


Drupal版本:

13 block.tpl.php默认模板建议

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

 它的模板建议规则,首先按照它所在的moduledelta来定义,然后按照它所在的区域来定义。我们还以主菜单区块为例,假定将它放到了navigation区域:

block--system--main.tpl.php

block--system.tpl.php

block--navigation.tpl.php

block.tpl.php

 


Drupal版本:

14 field.tpl.php

默认的模板文件位于modules\field\theme下面:

<div class="<?php print $classes; ?> clearfix"<?php print $attributes; ?>>

  <?php if (!$label_hidden): ?>

    <div class="field-label"<?php print $title_attributes; ?>><?php print $label ?>: </div>

  <?php endif; ?>

  <div class="field-items"<?php print $content_attributes; ?>>

    <?php foreach ($items as $delta => $item): ?>

      <div class="field-item <?php print $delta % 2 ? 'odd' : 'even'; ?>"<?php print $item_attributes[$delta]; ?>><?php print render($item); ?></div>

    <?php endforeach; ?>

  </div>

</div>


Drupal版本:

14.1 可用变量:

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

$items包含字段值的数组。使用render()输出。

$label条目的标签。

$label_hidden标签显示是否设置为了'hidden'

$classes:  CSS类字符串。可以在预处理函数中,通过修改$classes_array的值,来修改$classes的值。默认值有:fieldfield-name-[field_name]field-type-[field_type]field-label-[label_display]

其它变量:

$element['#object']字段附属的实体对象。

$element['#view_mode']查看模式,比如:'full', 'teaser'...

$element['#field_name']字段名字。

$element['#field_type']字段类型。

$element['#field_language']字段语言。

$element['#field_translatable']字段是否可被翻译。

$element['#label_display']标签显示的位置,inline(内联显示), above(上方显示), hidden(隐藏显示)。

$field_name_css: CSS兼容的字段名字。

$field_type_css: CSS兼容的字段类型。

$classes_array一个数组,里面包含了CSS类属性的值。$classes就是基于$classes_array生成的。

 


Drupal版本:

14.2 预处理函数:

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

template_preprocess_field()


Drupal版本:

15 field.tpl.php的模板建议

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

我们以字段field_email为例,它的字段类型为text,把它添加到了用户实体上面:

field--field-email--user.tpl.php

field--user.tpl.php

field--field-email.tpl.php

field--text.tpl.php

field.tpl.php


Drupal版本:

16 常见问题

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

很多人经常会问这样的问题,Drupal是怎么把这些模板文件转为HTML内容返回的呢?我们前面已经讲过了,预处理函书里面,可以为模板文件设置变量。现在的问题是怎么最终使用模板文件生成HTM?这个工作,是在下面的函数中实现的:

 

function theme_render_template($template_file, $variables) {

  extract($variables, EXTR_SKIP);               // Extract the variables to a local namespace

  ob_start();                                   // Start output buffering

  include DRUPAL_ROOT . '/' . $template_file;   // Include the template file

  return ob_get_clean();                        // End buffering and return its contents

}

    把模板文件和变量传递了过来,然后返回生成的HTML片段。



Drupal版本:

2 默认的html.tpl.php模板文件

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

Drupal自带的默认html.tpl.php模板文件,位于modules\system目录下面:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN"

  "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<?php print $language->language; ?>" version="XHTML+RDFa 1.0" dir="<?php print $language->dir; ?>"<?php print $rdf_namespaces; ?>>

 

<head profile="<?php print $grddl_profile; ?>">

  <?php print $head; ?>

  <title><?php print $head_title; ?></title>

  <?php print $styles; ?>

  <?php print $scripts; ?>

</head>

<body class="<?php print $classes; ?>" <?php print $attributes;?>>

  <div id="skip-link">

    <a href="#main-content" class="element-invisible element-focusable"><?php print t('Skip to main content'); ?></a>

  </div>

  <?php print $page_top; ?>

  <?php print $page; ?>

  <?php print $page_bottom; ?>

</body>

</html>

 

    模板文件,是由两部分组成的,静态的html标记文本,动态的变量。Drupal在生成这个页面时,会把这些变量预先的填充好内容,然后将其套到对应的模板上,这样便合成了一个完整的html页面。对于其它模板来说,是合成了对应功能的html片断。注意这里面的语法:

 

<?php print $page; ?>

 

    这就是一个很普通的php语句,Drupal自带的模板引擎是PHPTemplate,这样只要我们了解一点PHP,便可以很快地上手。对于其它的模板引擎,由于还需要学习额外的语法,学习成本会高那么一点点,所以在Drupal中很少用到。


Drupal版本:

3 html.tpl.php的模板建议

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

html.tpl.php的模板建议,是由theme_get_suggestions设置的,在预处理函数template_preprocess_html中,对应的代码:

 

  if ($suggestions = theme_get_suggestions(arg(), 'html')) {

    $variables['theme_hook_suggestions'] = $suggestions;

  }

 

    它的模板建议规则是按照路径来的,路径越具体,优先级越高。我们举个例子,来了解一下模板建议及其优先级问题。假定当前Drupal路径为node/1/edit,那么html.tpl.php有以下模板建议:

   html--node--edit.tpl.php

   html--node--1.tpl.php

   html--node--%.tpl.php

   html--node.tpl.php

 

    当用户访问页面node/1/edit,会使用这里面的哪个模板。Drupal的主题系统,会按照html.tpl.php的模板建议规则,首先检查html--node--edit.tpl.php是否存在,如果存在,就使用该模板文件;如果不存在,会接着检查html--node--1.tpl.php是否存在,如果存在,就使用html--node--1.tpl.php;如果不存在,会接着检查html--node--%.tpl.php是否存在,如果存在,就使用html--node--%.tpl.php;如果不存在,会接着检查html--node.tpl.php是否存在,如果存在,就使用html--node.tpl.php;如果不存在,就会使用主题自带的html.tpl.php文件,如果该主题没有html.tpl.php文件,则会使用Drupal核心自带的默认html.tpl.php文件。

    注意,在$variables['theme_hook_suggestions']对应的数组中,越靠后的模板建议,优先级越高。 


Drupal版本:

4 page.tpl.php

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

page.tpl.php 


Drupal版本:

4.1 一般实用变量:

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

$base_path: Drupal安装的基路径。通常情况下,默认为/。它的值由base_path()生成。

$directory: 模板文件所在的目录,比如modules/system或者themes/bartik它的值由path_to_theme()生成通常把这个变量和$base_path一起使用,来构建模板所在的绝对路径:

<?php print $base_path . $directory ?>

这段代码等价于:

<?php print base_path() . path_to_theme() ?>

$is_front如果当前页面为首页,返回TRUE

$logged_in当前用户是注册用户并且已经登录,返回TRUE;否则,返回FALSE

$is_admin:如果用户有权访问管理界面,返回TRUE;否则,返回FALSE


Drupal版本:

4.2 站点标识变量:

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

$front_page:首页的URL,它包含了语言前缀/域名。它的值由不带参数的url()生成。当链接指向首页时,可以使用这个变量,来替代$base_path

$logo: 指向logo图片的路径,定义在主题的配置中。

$site_name: 站点的名称。当管理员在主题配置中禁止显示时,$site_name为空。

$site_slogan: 站点的口号。当管理员在主题配置中禁止显示时,$site_slogan为空。


Drupal版本:

4.3 导航变量:

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

$main_menu站点主导航菜单链接的数组,如果没有配置,则为空。它在模板文件中这样输出,如下所示:

<?php print theme('links__system_main_menu', array('links' => $main_menu, 'attributes' => array('id' => 'main-menu', 'class' => array('links', 'inline', 'clearfix')), 'heading' => t('Main menu'))); ?>

    通过修改上面的代码,我们可以修改主菜单的IDClass,从而能够更好的控制它的样式输出。

$secondary_menu站点二级导航菜单链接的数组,如果没有配置,则为空。

$breadcrumb当前页面的面包屑。


Drupal版本:

4.4 页面内容变量:

$title_prefix (array):在模板的主标题标签前面显示的内容,注意它是数组结构,在模板中输出时,使用render()输出。

<?php print render($title_prefix); ?>

$title:页面的标题,可用在实际的HTML内容中。它在模板中这样输出:

<?php if ($title): ?><h1 class="title" id="page-title"><?php print $title; ?></h1><?php endif; ?>

   注意,这里面$title已经是现成的html内容了,而$title_prefix此时还是一个数组,它们的输出方式有所不同。在Drupal7的模板中,使用render(),是很普遍的。

$title_suffix (array): 在模板的主标题标签后面显示的内容,注意它是数组结构。

$messages状态消息和错误消息对应的HTML,应该突出显示。

$tabs (array): 当前页面对应的子页面的Tabs链接,例如当显示一个节点时,对应的查看/编辑标签。

$action_links (array): 作用于当前页面的动作,比如菜单管理界面的添加菜单链接。

$feed_icons当前页面的所有种子图标。

$node如果当前页面是一个节点页面,比如node/123,或者是节点的子页面node/123/revisions,这个变量就是对应的自动加载的节点对象。


Drupal版本:

4.5 区域变量:

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

$page['help'] : 动态的帮助文本,主要用于管理界面。


Drupal版本:

4.6 $page不修改Drupal核心代码: 页脚区域对应的条目。

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

很多人,特别是初学Drupal的人,经常会问,模板里面的变量是从哪里蹦出来的。肯定是在别的地方定义了这些变量,所以我们才能在这里引用。这些变量是在下面的函数中定义的:

template_preprocess()

template_process()

template_preprocess_page()

template_process_page()

 

    有兴趣的可以在api.drupal.org查一下这些函数。我们也会在后面的内容中,对这些函数做进一步的介绍。


Drupal版本:

5 page.tpl.php默认模板文件

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

  <?php if ($title): ?><h1 class="title" id="page-title"><?php print $title; ?></h1><?php endif; ?>

        <?php print render($title_suffix); ?>

        <?php if ($tabs): ?><div class="tabs"><?p<div id="page-wrapper"><div id="page">

 

    <div id="header"><div class="section clearfix">

 

      <?php if ($logo): ?>

        <a href="<?php print $front_page; ?>" title="<?php print t('Home'); ?>" rel="home" id="logo">

          <img src="<?php print $logo; ?>" alt="<?php print t('Home'); ?>" />

        </a>

      <?php endif; ?>

 

      <?php if ($site_name || $site_slogan): ?>

        <div id="name-and-slogan">

          <?php if ($site_name): ?>

            <?php if ($title): ?>

              <div id="site-name"><strong>

                <a href="<?php print $front_page; ?>" title="<?php print t('Home'); ?>" rel="home"><span><?php print $site_name; ?></span></a>

              </strong></div>

            <?php else: /* Use h1 when the content title is empty */ ?>

              <h1 id="site-name">

                <a href="<?php print $front_page; ?>" title="<?php print t('Home'); ?>" rel="home"><span><?php print $site_name; ?></span></a>

              </h1>

            <?php endif; ?>

          <?php endif; ?>

 

          <?php if ($site_slogan): ?>

            <div id="site-slogan"><?php print $site_slogan; ?></div>

          <?php endif; ?>

        </div> <!-- /#name-and-slogan -->

      <?php endif; ?>

 

      <?php print render($page['header']); ?>

 

    </div></div> <!-- /.section, /#header -->

 

    <?php if ($main_menu || $secondary_menu): ?>

      <div id="navigation"><div class="section">

        <?php print theme('links__system_main_menu', array('links' => $main_menu, 'attributes' => array('id' => 'main-menu', 'class' => array('links', 'inline', 'clearfix')), 'heading' => t('Main menu'))); ?>

        <?php print theme('links__system_secondary_menu', array('links' => $secondary_menu, 'attributes' => array('id' => 'secondary-menu', 'class' => array('links', 'inline', 'clearfix')), 'heading' => t('Secondary menu'))); ?>

      </div></div> <!-- /.section, /#navigation -->

    <?php endif; ?>

 

    <?php if ($breadcrumb): ?>

      <div id="breadcrumb"><?php print $breadcrumb; ?></div>

    <?php endif; ?>

 

    <?php print $messages; ?>

 

    <div id="main-wrapper"><div id="main" class="clearfix">

 

      <div id="content" class="column"><div class="section">

        <?php if ($page['highlighted']): ?><div id="highlighted"><?php print render($page['highlighted']); ?></div><?php endif; ?>

        <a id="main-content"></a>

        <?php print render($title_prefix); ?>

hp print render($tabs); ?></div><?php endif; ?>

        <?php print render($page['help']); ?>

        <?php if ($action_links): ?><ul class="action-links"><?php print render($action_links); ?></ul><?php endif; ?>

        <?php print render($page['content']); ?>

        <?php print $feed_icons; ?>

      </div></div> <!-- /.section, /#content -->

 

      <?php if ($page['sidebar_first']): ?>

        <div id="sidebar-first" class="column sidebar"><div class="section">

          <?php print render($page['sidebar_first']); ?>

        </div></div> <!-- /.section, /#sidebar-first -->

      <?php endif; ?>

 

      <?php if ($page['sidebar_second']): ?>

        <div id="sidebar-second" class="column sidebar"><div class="section">

          <?php print render($page['sidebar_second']); ?>

        </div></div> <!-- /.section, /#sidebar-second -->

      <?php endif; ?>

 

    </div></div> <!-- /#main, /#main-wrapper -->

 

    <div id="footer"><div class="section">

      <?php print render($page['footer']); ?>

    </div></div> <!-- /.section, /#footer -->

 

  </div></div> <!-- /#page, /#page-wrapper -->


Drupal版本:

6 page.tpl.php默认模板建议

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

    它的模板建议规则也是按照路径来定义的,路径越具体,优先级越高。我们还以node/1/edit为例,那么它有以下模板建议:

        

   page--node--edit.tpl.php

   page--node--1.tpl.php

   page--node--%.tpl.php

   page--node.tpl.php


Drupal版本:

7 region.tpl.php

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

region.tpl.php


Drupal版本:

7.1 region.tpl.php默认变量

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

$content这个区域的内容,通常为区块。

$classes包含CSS类的字符串。可以通过修改预处理函数中的$classes_array变量来修改这里的值。它的默认值有regionregion-[name],注意“[name]”中的下划线将自动替代为连字符,比如page_top区域,其中对应的一个类为region-page-top

$region:区域变量的名字,定义在主题的.info文件中。

$classes_array: CSS类的数组,$classes变量的值基于$classes_array生成。

    在这个模板中,还可以使用的通用变量有: $is_admin$is_front$logged_in

 

对应的预处理函数:

template_preprocess()

template_preprocess_region()

template_process()


Drupal版本:

7.2 region.tpl.php的默认模板建议

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

它的模板建议规则,是按照区域名字来定义的。我们还以right区域为例,它的模板建议为:  

   region--right.tpl.php

region.tpl.php

 

  Drupal自带的默认region.tpl.php模板文件,位于modules\system目录下面,内容如下:

 

<?php if ($content): ?>

  <div class="<?php print $classes; ?>">

    <?php print $content; ?>

  </div>

<?php endif; ?>


Drupal版本:

9 node.tpl.php默认模板文件

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

<div id="node-<?php print $node->nid; ?>" class="<?php print $classes; ?> clearfix"<?php print $attributes; ?>>

 

  <?php print $user_picture; ?>

 

  <?php print render($title_prefix); ?>

  <?php if (!$page): ?>

    <h2<?php print $title_attributes; ?>><a href="<?php print $node_url; ?>"><?php print $title; ?></a></h2>

  <?php endif; ?>

  <?php print render($title_suffix); ?>

 

  <?php if ($display_submitted): ?>

    <div class="submitted">

      <?php print $submitted; ?>

    </div>

  <?php endif; ?>

 

  <div class="content"<?php print $content_attributes; ?>>

    <?php

      // We hide the comments and links now so that we can render them later.

      hide($content['comments']);

      hide($content['links']);

      print render($content);

    ?>

  </div>

 

  <?php print render($content['links']); ?>

 

  <?php print render($content['comments']); ?>

 

</div>


Drupal版本:

8node.tpl.php 默认变量

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

node.tpl.php 默认变量

Drupal版本:

8.1 可用变量:

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

$title节点的标题,已过滤。

$content:节点条目数组。使用render($content),可以全部输出;使用 render($content['field_example'])可以输出一个子集;使用hide($content['field_example'])可以临时隐藏给定元素的输出。

$user_picture节点作者的头像,来自于user-picture.tpl.php

$date格式化的创建日期。如果想对这个变量重新格式化,那么可以在预处理函数中,对$created使用format_date()函数,并向其传递合适的参数。

$name节点作者用户名的格式化输出,这里使用了theme_username()

$node_url当前节点的直接URL

$display_submitted布尔值,用来指示是否显示提交信息。

$submitted提交信息,在template_preprocess_node()中基于$name$date生成。

$classes: CSS类字符串。可以在预处理函数中,通过修改$classes_array的值,来修改$classes的值。默认值有:nodenode-[type]node-teasernode-previewnode-promotednode-stickynode-unpublished。注意,这里的node-[type]表示当前节点类型,如果当前节点为博客,那么这里就是"node-blog"

$title_prefix (array): 在模板的主标题标签前面显示的内容,注意它是数组结构,在模板中输出时,使用render()输出。

$title_suffix (array): 在模板的主标题标签后面显示的内容,注意它是数组结构,在模板中输出时,使用render()输出。


Drupal版本:

8.2 其它变量:

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

$node:完整的节点对象。它里面包含的数据可能并不安全。

$type:节点类型,例如:articlepageblog等等

$comment_count:该节点上面的评论数量。

$uid节点作者的用户ID

$created节点的发布时间,格式为Unix时间戳。

$classes_array一个数组,里面包含了CSS类属性的值。$classes就是基于$classes_array生成的。

$zebra斑马线,可能的值有:"even""odd"。用在摘要列表中。

$id节点的位置。每输出一次,这个变量加一。

 


Drupal版本:

8.3 节点状态变量:

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

$view_mode:查看模式,比如:'full''teaser'等等。

$teaser:摘要状态的标记,等价于$view_mode == 'teaser'

$page标记,用来表示当前是否是完整页面状态。

$promote标记,用来表示当前节点是否推荐到了首页。

$sticky标记,用来表示当前节点是否被置顶。

$status标记,用来表示当前节点是否已发布。

$comment当前节点的评论设置的状态。

$readmore标记,当摘要内容HOLD不住正文内容时,为TRUE

$is_front标记,用来表示是否显示在首页。

$logged_in标记,用来表示当前用户是否是登录用户。

$is_admin标记,用来表示当前用户是不是管理员。


Drupal版本:

8.4 字段变量:

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

对于附加在该节点上面的每个字段实例,都会为其定义一个对应的变量。例如,$node->body变成了$body。当需要访问一个字段的原始数据时,推荐大家使用这些变量,如果从$node变量中直接获取,此时还需要考虑字段上面的语言,例如,$node->body['en'];这样就会覆写掉前面设置的语言协定规则。

 

    有关字段变量的生成,可以参看函数field_attach_preprocess,在这个函数中,去掉了字段上面的语言信息。代码如下:

$variables[$field_name] = isset($entity->{$field_name}[$langcode]) ? entity->{$field_name}[$langcode] : NULL;

 

这些变量来自于:

template_preprocess()

template_preprocess_node()

template_process()


Drupal版本:

第7章 Batch API(批处理)

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

我们做开发的时候,经常会遇到批量处理数据的情况,如果一次处理10条或者上百条数据,我们一次性的处理完成就可以了。当需要处理上千条、上万条的数据时,我们不可能把所有的数据,一股脑的都加载进来,逐一处理,这是行不通的,很容易就超出了PHP的各种限制,比如内存限制、执行时间的限制。这个时候,就需要批处理这种方式,Drupal提供了一套批处理API,方便我们做这件事情。

什么是批处理呢?批处理,顾名思义,就是一批一批的处理,假如有一万条数据需要处理,我们一批处理20条,当处理完这20条后,批处理API会再次发送一个HTTP请求,系统接着处理下面的20条,处理完以后,再发送HTTP请求,这样循环下去,直到这一万条数据处理完为止。注意,这里发送的HTTP 请求,是由Drupal自动完成的,在我们看起来,Drupal一次性的把所有的数据都处理了,实际后台方面,Drupal是把这些数据分成若干批分别处理。

什么地方用到了批处理?Drupal里面使用批处理的地方太多了,比如我们安装Drupal的时候,模块的安装,使用的就是批处理,这个是我们再熟悉不过的了。我们使用Apachesolr的时候,当需要重建所有索引的时候,而数据量又比较大,这个时候就可以使用批处理进行索引。批量修改特定字段的值时,如果涉及到的实体数量比较大时,也需要使用批处理。Feeds模块,使用单独表单提交后,导入数据时,用的就是批处理。Drupal系统中,用到的地方还有很多。


Drupal版本:

1使用Batch API批量修改各种会员价格

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

我们在为不同角色设置不同价格时,曾经用到过一个uc_batch_price模块,它能够根据定义好的折扣,一次性的批量修改所有商品的会员价格。具体配置可以参看第三集,这里我们主要讲解一下代码,首先是info文件:

name = Ubercart Batch Price

description = Ubercart Batch Price.

dependencies[] = uc_price_per_role

package = Ubercart - extra

core = 7.x

这里面的内容我们都比较熟悉了,我在编写程序的时候,如果这个模块具有通用性,我尽量都采用英文的形式,这样更便于其它用户接受。这里我们依赖于uc_price_per_role模块。

uc_batch_price.module文件中,我们实现了hook_menu,这里定义了两个菜单项:

/**

 * 实现钩子hook_menu().

 */

function uc_batch_price_menu() {

  $items['admin/store/settings/price_per_role/default'] = array(

    'title' => 'Default setting',

    'description' => 'Configure price per role settings.',

    'access arguments' => array('administer store'),

    'page callback' => 'drupal_get_form',

    'page arguments' => array('uc_batch_price_default_form'),

    'type' => MENU_LOCAL_TASK,

'file' => 'uc_batch_price.admin.inc',

  );

 

  $items['admin/store/settings/price_per_role/batch'] = array(

    'title'            => 'Batch process price',

    'page callback'    => 'drupal_get_form',

    'page arguments'    =>  array('uc_batch_price_batch_form'),

    'access arguments' => array('administer store'),

    'weight'           => 10,

    'type'             => MENU_LOCAL_TASK,

'file' => 'uc_batch_price.admin.inc',

  );

  return $items;

}

admin/store/settings/price_per_role/default,这是是定义配置表单的,对应的表单定义位于uc_batch_price.admin.inc'admin/store/settings/price_per_role/batch,用来定义批处理所在的表单页面的,对应的表单也位于uc_batch_price.admin.inc

我们先来看一下,不同角色的默认折扣配置页面的实现代码:

function uc_batch_price_default_form(){

  $enabled = variable_get('uc_price_per_role_enabled', array());

  $enabled_roles = array_filter($enabled);

  $uc_price_per_role_default = variable_get('uc_price_per_role_default', array());

  $roles = user_roles();

  //$rid = 1;

  //debug($roles);

  foreach($enabled_roles as $rid => $enabled){

//$rid++;

    $form['role'][$rid] = array(

      '#type' => 'fieldset',

      '#collapsible' => TRUE,

      '#collapsed' => FALSE,

      '#title' => $roles[$rid],

    );

    $form['role'][$rid]['multiplicator_'.$rid] = array(

      '#type' => 'textfield',

      '#title' => t('乘数'),

      '#description' => t('大于0小于等于1的小数'),

      '#default_value' => isset($uc_price_per_role_default[$rid]) ? $uc_price_per_role_default[$rid] : 0.8,

      '#size' => 16,

    );

  }

  $form['submit'] = array(

    '#type' =>'submit',

    '#value' => t('Submit'),

  );

  return $form;

}

 

function uc_batch_price_default_form_validate($form, &$form_state) {

  $enabled = variable_get('uc_price_per_role_enabled', array());

  $enabled_roles = array_filter($enabled);

  foreach($enabled_roles as $rid => $enabled){

    $multiplicator = $form_state['values']['multiplicator_'.$rid];

    if (!empty($multiplicator) && !is_numeric($multiplicator)) {

      form_set_error('multiplicator_'.$rid, t('乘数必须是一个数字.'));

    }

    elseif (!empty($multiplicator)) {

      if(($multiplicator > 1) || ($multiplicator < 0)){

         form_set_error('multiplicator_'.$rid, t('乘数必须是一个大于0小于等于1的数字.'));

  }

    }

  }

}

 

function uc_batch_price_default_form_submit($form, &$form_state) {

  $enabled = variable_get('uc_price_per_role_enabled', array());

  $enabled_roles = array_filter($enabled);

  $uc_price_per_role_default = array();

  foreach($enabled_roles as $rid => $enabled){

    $uc_price_per_role_default[$rid] = $form_state['values']['multiplicator_'.$rid];

  }

  variable_set('uc_price_per_role_default', $uc_price_per_role_default);

}

 

    在uc_batch_price_default_form里面,我们为每个与积分相关的会员角色,定义了一个表单元素,用来设置折扣值,比如8折,就是输入0.8就可以了。在表单的验证函数里面,我们对输入的折扣进行了验证,确保它是一个从01的小数。在提交函数里面,我们把每个角色的折扣值,都保存到了变量$uc_price_per_role_default里面,并通过variable_set,将它保存到数据库中。这里面的逻辑没有什么复杂的。

我们来看批处理的代码部分:

function uc_batch_price_batch_form(){

  $product_types = uc_product_types();

  $types = node_type_get_types();

  $options = array();

  //debug($product_types);

  //debug($types);

  foreach ($product_types as $key => $value) {

    $options[$value] = $types[$value]->name;

  }


  $form['types'] = array(

    '#title' => t('Content types to be processed'),

    '#type' =>'checkboxes',

    '#description' =>t('All nodes of these selected types will be processed'),

    '#options' => $options,

  );

  $form['submit'] = array(

    '#type' =>'submit',

    '#value' => t('Submit'),

  );

  return $form;

}

 

function uc_batch_price_batch_form_submit($form,&$form_state){

  $types = array_filter($form_state['values']['types']);

  //drupal_set_message('1234567');

  $batch = array(

    'operations' => array(

      array('uc_batch_price_price_process',array($types)),

    ),

    'finished' => 'uc_batch_price_batch_finished',

    'title' => t('批量处理价格'),

    'init_message' => t('开始批量处理价格.'),

    //'progress_message' => t('Reindexed @current out of @total.'),

    'error_message' => t('批量处理价格遇到错误.'),

'file' => drupal_get_path('module', 'uc_batch_price') . '/uc_batch_price.admin.inc',

  );

  batch_set($batch);

}

 

function uc_batch_price_price_process($types, &$context){

  //drupal_set_message('123456');

  //debug($types);

  $size = 20;

  //drupal_set_message('123456');

  if(!isset($context['sandbox']['progress'])){

    $context['sandbox']['progress'] = 0;

    $context['sandbox']['last_nid'] = 0;

    $context['sandbox']['max'] = db_select('node', 'n')

      ->condition('n.type', $types, 'IN')

      ->countQuery()

      ->execute()

      ->fetchField();

  }

  $enabled = variable_get('uc_price_per_role_enabled', array());

  $enabled_roles = array_filter($enabled);

  $uc_price_per_role_default = variable_get('uc_price_per_role_default', array());

  //drupal_set_message('123456');

  $query = db_select('node', 'n')

    ->fields('n', array('nid'))

    ->condition('n.type', $types, 'IN')

->condition('n.nid', $context['sandbox']['last_nid'], '>')

->orderBy('n.nid', 'ASC')

    ->range(0, $size);

  $result = $query->execute();

  foreach ($result as $record) {

    //drupal_set_message('123456');

    $product = node_load($record->nid);

$list_price = $product->list_price;

    foreach($enabled_roles as $rid => $enabled){

      if(!empty($uc_price_per_role_default[$rid])){

      //if(empty($product->role_prices[$rid]) && !empty($uc_price_per_role_default[$rid])){

    $product->role_prices[$rid] = $list_price * $uc_price_per_role_default[$rid];

  }

}

    node_save($product);

    $context['sandbox']['progress']++;

    $context['sandbox']['last_nid'] = $product->nid;

  }

  if($context['sandbox']['progress'] ==$context['sandbox']['max']){

    $context['finished'] = 1;

  }else{

    $context['finished'] = $context['sandbox']['progress']/$context['sandbox']['max'];

  }

}

 

function uc_batch_price_batch_finished($success, $results, $operations){

  if ($success) {

    // Here we do something meaningful with the results.

    $message = t('批量处理价格完成');

  }

  else {

    // An error occurred.

    // $operations contains the operations that remained unprocessed.

    $error_operation = reset($operations);

    $message = '在批量处理价格时出现一个错误'. $error_operation[0] .' 其参数为 :'. print_r($error_operation[0], TRUE);

  }

  drupal_set_message($message);

}

这里面的代码,包括四部分,表单定义、表单的提交处理函数、批处理函数、批处理的完成函数。我们的批处理程序,通常都是由这四部分组成的。

在表单定义函数uc_batch_price_batch_form里面,我们使用uc_product_types获取了所有的产品类型,把它作为选项,赋值给复选框。注意,这里的node_type_get_types,没有什么用,这个程序是同别的地方复制过来的。

在表单提交函数uc_batch_price_batch_form_submit里面,我们首先获取所有需要处理的产品类型,注意这里是如何提取复选框里面的数据的,我们使用了array_filter

$types = array_filter($form_state['values']['types']);

接着,我们定义了一个批处理:

  $batch = array(

    'operations' => array(

      array('uc_batch_price_price_process',array($types)),

    ),

    'finished' => 'uc_batch_price_batch_finished',

    'title' => t('批量处理价格'),

    'init_message' => t('开始批量处理价格.'),

    //'progress_message' => t('Reindexed @current out of @total.'),

    'error_message' => t('批量处理价格遇到错误.'),

'file' => drupal_get_path('module', 'uc_batch_price') . '/uc_batch_price.admin.inc',

  );

我们看到,批处理就是一个数组,里面包含几个键:operationsfinishedtitleinit_messageprogress_messageerror_messagefile。这里面的titleinit_messageerror_message都比较好理解,分别用来设置标题、初始化消息、错误消息;progress_message,用来设置批处理过程中的消息,提示用户完成了多少,这个我一般没有用过,一直都没有用过,因为上面有个进度条,可以看到大致的进度;file用来指定批处理所在的文件,在批处理的过程中,完成一批就会发送一个新的HTTP请求,新的请求过来以后,默认会加载所有的module文件,如果我们这里没有设置这个file文件的话,由于我们将它放到了uc_batch_price.admin.inc里面,Drupal就会找不到我们的批处理函数,这样就会报错,通过设置file,系统就可以加载uc_batch_price.admin.inc文件,继续执行里面的批处理函数了。

以前的时候,我都是把批处理的逻辑代码放到module文件里面的,也尝试过几次,放到admin.inc这样的文件里面,总是报错,后来在阅读别人的代码时,发现批处理没有放到module文件里面,我才学会用了通过设置这个file键,将批处理的逻辑放到inc文件里面。这是一个小的进步。我昨天,看别人写的一篇文章,如何在themetemplate.php文件里面实现mytheme_form_alter这个钩子函数,以前就不知道这个技巧,在Drupal6下面是不可以这样的,但是在Drupal7下面,却是可以的,技术在分享中进步。我通过阅读module_invoke_allmodule_implementsystem_listAPI函数,终于弄明白了为什么mytheme_form_alter也可以工作的原因,原来是这样的,在system_list里面,获取模块列表的SQL语句是这样写的:

$result = db_query("SELECT * FROM {system} WHERE type = 'theme' OR (type = 'module' AND status = 1) ORDER BY weight ASC, name ASC");

这里同时支持了thememodule,一下子就明白了为什么。我对批处理里面的file键的学习,就和这个差不多,在阅读别人的代码里面,看到了这个用法,留心了一下,下次实现同样的功能时,就派上了用场。

finished是用来指定批处理的完成函数的,这里指定为uc_batch_price_batch_finished。我们来看看批处理完成函数的模式:

function uc_batch_price_batch_finished($success, $results, $operations){

  if ($success) {

    // Here we do something meaningful with the results.

    $message = t('批量处理价格完成');

  }

  else {

    // An error occurred.

    // $operations contains the operations that remained unprocessed.

    $error_operation = reset($operations);

    $message = '在批量处理价格时出现一个错误'. $error_operation[0] .' 其参数为 :'. print_r($error_operation[0], TRUE);

  }

  drupal_set_message($message);

}

在这里面,逻辑处理非常简单,如果成功了,设置一个成功的消息,如果失败了设置一个失败的消息,消息的设置,使用的是drupal_set_message;这里唯一有点难以理解的是

$error_operation[0] .' 其参数为 :'. print_r($error_operation[0], TRUE)

我们只需要知道,这段代码是用来显示错误消息的即可,对于error_operation里面的结构,有兴趣的可以继续研究,工作中是用不到的。所以我们在编写批处理的过程中,对于这个批处理完成函数,只需要修改它的函数名字,消息的内容,包括成功时的消息、失败时的消息,就这三个地方。其它工作就是复制粘贴。

我们来看看批处理数组的最后一个参数operations

    'operations' => array(

      array('uc_batch_price_price_process',array($types)),

    ),

这是一个数组,数组里面又是一个数组;在里面的数组中,第一个uc_batch_price_price_process,就是批处理的处理函数,第二个array($types),就是传递给处理函数的参数;我们从operations这个名字可以看出,它是一个复数形式,就是可以支持多个处理函数,但是我用到的都是一个处理函数就够了的情况;另外需要注意的是,参数的传递,也是采用数组的形式,这意味着我们可以传递多个参数。注意,在Drupal7下面,带有参数的处理函数是这样写的:

function uc_batch_price_price_process($types, &$context)

这里的$types放到了&$context前面,我记得以前在Drupal6下面,参数的传递好像是这样的:

function uc_batch_price_price_process(&$context, $types)

如果在Drupal7下面也这样写的话,就会出问题,我开始就是这样搬过来的,结果接收不到参数,不知道为什么。所以这里特别强调一下,这个参数的顺序。

在表单提交函数的最后,使用batch_set($batch),这样就将我们定义好的批处理传递给了Drupal系统本身。batch_set($batch),这句话,就这么用的,不需要修改。

我们现在来看一下批处理的处理函数里面的逻辑,这里面的逻辑大致包含三大块,头部、中部、尾部。头部和尾部的代码在每个同类的函数中变化都不大,只有中间部分的代码,才真正是我们需要着重修改的。

我们来看头部代码:

  $size = 20;

  //drupal_set_message('123456');

  if(!isset($context['sandbox']['progress'])){

    $context['sandbox']['progress'] = 0;

    $context['sandbox']['last_nid'] = 0;

    $context['sandbox']['max'] = db_select('node', 'n')

      ->condition('n.type', $types, 'IN')

      ->countQuery()

      ->execute()

      ->fetchField();

  }

这里的size是用来定义一批次,能够处理多少条数据的,我一般使用20,根据实际也会有所调整。比如每条数据的计算工作量比较大时,我们可以把size调的小一点;单条数据的计算量比较小时,我们就可以把size相应的调的大一点。

接下来,是一个初始化设置,用来设置上下文里面的三个变量,progresslast_nidmax;其中progressmax是基本上所有的场合都用到的,progress表示已经处理了多少,max表示总共有多少需要处理,为了获取总共有多少条记录需要处理,我们这里使用了db_select查询语句;而last_nid是我们这里用到的,或者说是特有的,它表示最后处理的节点的nid。注意这些变量都是存放到$contextsandbox里面的,sandbox中文常常翻译为沙盒。

我们现在来看看底部的代码部分:

  if($context['sandbox']['progress'] ==$context['sandbox']['max']){

    $context['finished'] = 1;

  }else{

    $context['finished'] = $context['sandbox']['progress']/$context['sandbox']['max'];

  }

这段代码的含义是,如果进度progress等于最大记录时,将$context['finished']设置为1,否则将它设置为两者的除数。当$context['finished']等于1时,批处理系统就会结束处理,调用批处理的完成函数。

掐头去尾后,就剩下了中间的逻辑部分,在中间的这部分代码中,我们首先获取当前需要我们处理的这一批数据,然后对这一批数据进行迭代循环,每循环一次progress都会自动加1,我们同时将last_nid设置为当前的节点的nid。这里面唯一需要一点技巧的就是,如何获取当前批次的所有数据,我们这里使用了db_select

  $query = db_select('node', 'n')

    ->fields('n', array('nid'))

    ->condition('n.type', $types, 'IN')

->condition('n.nid', $context['sandbox']['last_nid'], '>')

->orderBy('n.nid', 'ASC')

    ->range(0, $size);

我们这里可以看到last_nid的用处了,由于我们是按照节点从小到大的顺序来处理的,通过使用last_nid,就可以将已经处理过了的排除在外了,读取过来的,恰好是我们当前需要处理的数据。

迭代循环里面的逻辑代码,我们这里的逻辑是加载当前节点,根据我们设置好的折扣,来修改对应角色的价格,修改过后保存节点。这部分是需要我们自己编写的。

通过这个例子,我们可以看到,批处理的相关代码,三段论,固定格式的,我们只需要改改里面的名字,然后再把中间部分的逻辑处理部分改为我们自己的即可,写过一次以后,下次再写,就跟走平地一样,和写一个只需要处理20条记录的程序代码,难度差不多,费不了多少事。

我们最后顺便看一下,这个模块的另一个钩子实现,hook_node_presave

function uc_batch_price_node_presave($node) {

  if (!empty($node->role_prices)) {

    $enabled = variable_get('uc_price_per_role_enabled', array());

    $enabled_roles = array_filter($enabled);

    $uc_price_per_role_default = variable_get('uc_price_per_role_default', array());

$list_price = $node->list_price;

foreach($enabled_roles as $rid => $enabled){

      if(empty($node->role_prices[$rid]) && !empty($uc_price_per_role_default[$rid])){

        $node->role_prices[$rid] = $list_price * $uc_price_per_role_default[$rid];

      }

    }

  }

}

    在保存节点之前,如果特定的会员价格为空的,我们根据默认折扣,为它赋值,这样用户就不需要分别计算不同会员的价格了。这里用的是hook_node_presave,类似的钩子还有

hook_node_insert,hook_node_update,hook_node_load,hook_node_view,hook_node_view_alter,hook_node_delete,hook_node_validate,hook_node_prepare。随着对应的entity钩子的流行,这些特定于节点的钩子函数,用途越来越窄了。


Drupal版本:

2批量删除模块batchdelete

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

我们再来看一个例子,批量删除模块batchdelete,这个模块主要用来批量删除节点的,还可以删除自定义区块、词汇表,不过后面两者的删除,没有用到批处理机制。

我们来看一下代码,菜单项定义:

function batchdelete_menu() {

  $items['admin/config/development/batchdelete'] = array(

    'title'            => 'Batch delete node',

    'page callback'    => 'drupal_get_form',

'page arguments'    =>  array('batchdelete_form'),

    'access arguments' => array('administer site configuration'),

    'weight'           => 1,

    'type'             => MENU_NORMAL_ITEM,

   

  );

  $items['admin/config/development/batchdelete/node'] = array(

    'title'            => 'Batch delete node',

    'page callback'    => 'drupal_get_form',

'page arguments'    =>  array('batchdelete_form'),

    'access arguments' => array('administer site configuration'),

    'weight'           => 1,

    'type'             => MENU_DEFAULT_LOCAL_TASK,

   

  );

  $items['admin/config/development/batchdelete/block'] = array(

    'title'            => 'Batch delete block',

    'page callback'    => 'drupal_get_form',

'page arguments'    =>  array('batchdelete_block_form'),

    'access arguments' => array('administer site configuration'),

    'weight'           => 2,

    'type'             => MENU_LOCAL_TASK,

   

  );

  $items['admin/config/development/batchdelete/taxonomy'] = array(

    'title'            => 'Batch delete taxonomy term',

    'page callback'    => 'drupal_get_form',

'page arguments'    =>  array('batchdelete_taxonomy_form'),

    'access arguments' => array('administer site configuration'),

    'weight'           => 3,

    'type'             => MENU_LOCAL_TASK,

   

  );

  return $items;

}

这里我们为节点、区块、分类,分别定义了一个菜单项,我们来看一下,节点的删除代码。

function batchdelete_form(){

  $options = array();

  $types = node_type_get_types();

  foreach ($types as $key => $values) {

    $options[$key] = $values->name;

  }


  $form['types'] = array(

    '#title' => t('Content types to be delete'),

    '#type' =>'checkboxes',

    '#description' =>t('All nodes of these selected types will be deleted'),

    '#options' => $options,

  );

 

  $form['submit'] = array(

    '#type' =>'submit',

    '#value' => t('Delete'),

  );

  return $form;

}

 

function batchdelete_form_submit($form,&$form_state){

  $types = array_filter($form_state['values']['types']);

  $batch =array(

    'operations' => array(

      array('batchdelete_node_process', array($types)),

    ),

    'finished' => 'batchdelete_node_finished',

    'title' => t('删除节点'),

    'init_message' => t('开始批量删除.'),

    //'progress_message' => t('Reindexed @current out of @total.'),

    'error_message' => t('批量删除遇到错误.'),

  );

  batch_set($batch);

}

function batchdelete_node_process($types, &$context){

  $size =100;

  //$types_str ='story,page';

  //$types_str = "'".implode("','", $types)."'";

  //drupal_set_message('types:'.print_r($types));

  //debug($types);

  if(!isset($context['sandbox']['progress'])){

    $context['sandbox']['progress'] = 0;

    $context['sandbox']['max'] = db_select('node', 'n')

      ->condition('n.type', $types, 'IN')

      ->countQuery()

      ->execute()

      ->fetchField();

    //drupal_set_message('max:'.$context['sandbox']['max']);

  }

  //$sql = "SELECT nid FROM {node} WHERE type in ($types_str)";

  //$result = db_query_range($sql,0,$size);

  $query =db_select('node', 'n')

    ->fields('n', array('nid'))

    ->condition('n.type', $types, 'IN')

    ->range(0, $size);

  $result = $query->execute();


  foreach ($result as $record) {

    //drupal_set_message('123max:'.$result->nid);  

    node_delete($result->nid);

    $context['sandbox']['progress']++;

    //$context['message'] = t('删除节点 %nid',array('%nid' => $node['nid']));

  }

  if($context['sandbox']['progress'] ==$context['sandbox']['max']){

    $context['finished'] = 1;

  }else{

    $context['finished'] = $context['sandbox']['progress']/$context['sandbox']['max'];

  }

}

 

function batchdelete_node_finished($success, $results, $operations){

  if ($success) {

    // Here we do something meaningful with the results.

    $message = t('节点删除完成');

  }

  else {

    // An error occurred.

    // $operations contains the operations that remained unprocessed.

    $error_operation = reset($operations);

    $message = '在删除节点时出现一个错误'. $error_operation[0] .' 其参数为 :'. print_r($error_operation[0], TRUE);

  }

  drupal_set_message($message);

}

这段代码,和我们前面第一个例子非常类似,其实我们第一个例子里面的代码,就是从这个模块复制过来的。而batchdelete模块,则是从Drupal6下面升级过来的,我们最初编写的是Drupal6下面的,现在升级到了Drupal7。升级的过程中,就遇到了向批处理的处理函数中传递参数的问题,batchdelete_node_process$types最初是放在后面的,结果后来发现了问题,调试了多次,我们可以看到代码里面还有很多的调试信息,最后才发现参数的顺序问题。

删除自定义区块的代码,还没有升级过来。我们看一下删除词汇表的:

function batchdelete_taxonomy_form(){

  $options=array();

  $vocabs = taxonomy_get_vocabularies();

  foreach($vocabs as $vocab){

    $options[$vocab->vid] = $vocab->name; 

  }

  $form['vids'] = array(

    '#title' => t('要删除的分类词汇表ID'),

    '#type' =>'checkboxes',

    '#description' =>t('请选择哪些分类词汇表要被删除。'),

    '#options' => $options,

  );

  $form['submit'] = array(

    '#type' =>'submit',

    '#value' => t('Delete'),

  );

  return $form;

}

function batchdelete_taxonomy_form_submit($form,&$form_state){

  $vids = array_filter($form_state['values']['vids']);

  foreach($vids as $vid){

    //taxonomy_del_vocabulary($vid);

    $transaction = db_transaction();

    try {

      // Only load terms without a parent, child terms will get deleted too.

      $result = db_query('SELECT t.tid FROM {taxonomy_term_data} t INNER JOIN {taxonomy_term_hierarchy} th ON th.tid = t.tid WHERE t.vid = :vid AND th.parent = 0', array(':vid' => $vid))->fetchCol();

      foreach ($result as $tid) {

        taxonomy_term_delete($tid);

      }

      return SAVED_DELETED;

    }

    catch (Exception $e) {

      $transaction->rollback();

      watchdog_exception('taxonomy', $e);

      throw $e;

    }

  }

}

我们这里面删除的是词汇表下面的所有分类术语,而没有删除词汇表本身。taxonomy_get_vocabularies,用来获取所有的词汇表;taxonomy_del_vocabulary可以用来删除词汇表,taxonomy_term_delete用来删除分类术语。我们这里在删除的过程中,使用了事务处理,它的代码结构大致是这样的:

    $transaction = db_transaction();

    try {

      //做一些操作.

    }

    catch (Exception $e) {

      $transaction->rollback();

      watchdog_exception('taxonomy', $e);

      throw $e;

    }

其实在编写好batchdelete的最初版本后,发现drupal.org上面有个类似的模块,Bulk delete,也是Drupal6下面的模块,我的模块比它晚写了三个月,所以编写的时候,没有注意到,已经存在这样一个模块了。不过,现在Bulk delete模块已经不再维护,为什么呢?因为VBO模块的存在。


Drupal版本:

3VBO

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

好像我们在前面并没有介绍过VBO,这是一个我在项目中,经常使用的模块,它的全称为Views Bulk Operations,翻译过来就是基于Views的批量操作。我们以批量删除某一类型的节点为例,看看VBO是如何实现这样的功能的。我们还在bookstore里面操作。

我们创建一个视图,初始配置如下:

图片1.png 

点击保存并继续按钮,在字段的添加对话框里面,找到“Bulk operations: 内容

图片2.png 

选中这个字段,点击应用按钮,该字段的配置选项很多,我们看与批处理相关的:

图片3.png 

这里我们采用默认配置就可以了,前面两个选项的意思是,采用下拉选择框的形式,还是逐个采用按钮的形式,来显示操作。Enable "Select all items on all pages",表示允许选中所有页面的全部条目;Force single,表示强制单选;Display processing result,表示是否显示处理结果的相关消息;Number of entities to load at once,就是一次加载多少个实体,也就是我们在批处理中,一次处理多少条记录。

再往下的配置,就是可选的操作:

图片4.png 

除了我们这里想要的删除操作以外,这里包括更多的动作可供选择,比如发布内容、使内容置顶、发送邮件、修改实体的值,等等,我们根据需要还可以编写自己的操作。VBO支持以下操作,系统自带的Action(动作),VBO定义的动作,RULES定义的动作。

我们这里选中,第一个删除条目Delete item (views_bulk_operations_delete_item)),其它采用默认配置即可。此时会显示出来三个新的复选框:

图片5.png 

Enqueue the operation instead of executing it directly,表示采用队列的形式,而不是直接执行;Skip confirmation step,表示跳过确认步骤;Override label,用来覆写标签。我们这里采用默认的即可。保存。

接着,我们将“Bulk operations: 内容 (内容)”调整到内容标题的上面:

图片6.png 

最后,添加一个内容类型过滤器,并将其暴露(exposed)出来:

图片7.png 

保存视图,现在访问页面,我们就实现了和batchdelete模块同样的功能:

图片8.png 

配置起来非常简单,所以,当我们需要批量操作Drupal系统里面的各种数据时,我们应该首先来看一看,VBO是否能够满足我们的需求,如果不能满足的话,再考虑Batch API的形式。其实VBO本身采用的也是Batch API的形式。我们有关批处理的,就介绍到这里。写完才发现,前面在第三集里面已经简单的介绍过VBO了。

 



Drupal版本:

第9章 Field Validation的历程

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

我们第4集主要讲解开发与代码,当然很多代码是在前面的网上书店系统里面都用到的,部分代码是对网上书店原有功能的补充。此外,他不是一个严格意义上面的参考书,但是希望这份资料对大家仍然有用。

我们先来看一个简单的,这个对我来说很简单的,因为代码已经写好了,还放到了网络上了,这就是我写的Field validation模块。我们这一章,主要讲解field validation的演化历程,讲解里面代码的变化,用到的Drupal7的相关技术。如果让我凭空写出来一个模块进行讲解,效果未必会怎么好。

其实很久很久以前,我一直都有一个愿望,就是在drupal.org上贡献几个模块,而且这几个模块还比较流行,这一直是我的心愿。从最初的uc_alipay,到block_morelink,两个模块的用户量都是比较小的,uc_alipay我觉得怎么也能发展到56百的安装量,但是实际只有80+多个,block_morelink我觉得可以达到1000,或者更多,但是实际也仅仅只有100+个。每当看到东哥(eastcn, http://drupal.org/user/134014)在drupal.org上面的安装量,达到了几千多个,http://drupal.org/project/photos最多的时候达到过2000+的,自己都艳羡不已。什么时候,自己写的模块的安装量 ,能够超过东哥。

榜样的力量永远会激励着人,只要你没有忘记这个念头。机会总要来的,我在帮助外研社改造他们的社网时,遇到了这样的需求,字段的验证问题。当时章林提出了一个想法,如果Drupal的验证,能够像.net里面的那样方便就可以,最初是用来验证webform的,然后我就告诉他,有webform_validation这样的模块,他用了之后,觉得很好用;再后来,我们遇到了字段的验证,实体表单里面的字段验证,章林又提出了一个想法,如果字段(Field)的验证能够像webform的验证那样简单就可以了,只需要能够配置正则表达式就可以了,这样会方便很多。

这是一个很好的想法,我检查了所有的与验证相关的模块,没有找到一个是用来处理字段验证的,有一个Validation API,只有Drupal6的版本,而且已经无人维护了。还有CCK Validation。那个时候,Drupal7刚刚出来,很多模块都没有跟上。这是一个机会。


Drupal版本:

1 最初的解决办法

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

但是项目中的实际问题,还是需要解决的。在实际的项目中,章林选择是用了form_alter来解决这个问题。Form_alter我们在前面,Think in Drupal第一集里面的第一章讲的就是这个钩子,它是非常有用的,在这里解决这个问题也是非常有效的。首先确定实体表单的ID,然后通过form_alter为这个表单添加一个验证回调函数,在自己的验证函数里面添加自己的逻辑。原有的代码已经找不到了,我在网络上找了段类似的代码,做了一下改造:

function mymodule_form_alter(&$form, &$form_state, $form_id) { 

  if ($form_id == 'test_node_form') { 

    $form['#validate'] = 'mymodule_custom_validation_handler'; 

  } 

}

 

function mymodule_custom_validation_handler((&$form, &$form_state){

  $length = strlen($form_state['values']['field_testlength']['und']['0']['value'])

  if ($length > 8) { 

    form_set_error("field_testlength']['und']['0']['value", t('长度必须小于等于8')); 

  } 

}

这段程序我没有调试,用的时候,自己需要检查一下。这里需要注意的是,获取字段值的方法:

$form_state['values']['field_testlength']['und']['0']['value']

以及设置错误消息时,第一个参数:

"field_testlength']['und']['0']['value"

另一个需要注意的是,字段的语言属性,['und'],这里究竟是用und还是默认语言,用的时候也需要自己检查一下。

 

我觉得,解决Drupal7下面的字段验证,应该有更好一点的钩子,我便寻找,寻找,终于找到了一个可能工作的钩子。我的解决办法是这样的:

 

/**

 * 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);

  if($entity_type =='node' &&  $bundle = 'test'){

    if($field_text = $entity->field_text){

      if(strlen($field_text['und'][0]['value'])>8){

        $errors['field_text']['und'][0][] = array(

          'error' => 'field_text', 

          'message' => t('length must less than 8.'),

        );

      }

    }

  }

}

这是最原始的测试代码,它仅仅用来说明了hook_field_attach_validate是可以用来解决这个问题。这也是我第一次使用这个钩子函数。而以后,有关Field的验证问题,一直都是通过这个钩子解决的。首先,我觉得这种方式比form_alter高级一点,Dries曾经写过一篇介绍Drupal7的文章,里面就讲到了字段的验证,与表单的验证分离开了,这在Drupal7里面是一个进步。而这种分离,在Drupal8里面还会进一步的改进。我在drupal.org上面贡献了field validation模块以后,曾经有人建议使用Form_alter的形式,但是我没有理他们,还是坚持使用hook_field_attach_validate

另外的值得注意的是,这段代码:

list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);

这是我从别处拷贝过来的。这里根据一个实体对象、还有实体类型,使用entity_extract_ids这个API函数,便可以提取出来它的idvidbundle。这是很有用的,但是有时候,这个API函数并不能正常工作,个别时候,大部分都是可以的。

此外,这个错误消息的设置:

        $errors['field_text']['und'][0][] = array(

          'error' => 'field_text', 

          'message' => t('length must less than 8.'),

        );

这个,我们在第一集的,字段API一章里面介绍过了这个结构。我最初编写的时候,并不知道这两个键的含义,尤其是'error',所以默认把它设置为了字段名字。'message'的含义我是清楚的,用来设置具体的错误消息。

代码并不是一下子就工作了,我还做了很多的调试工作。还是使用原始的drupal_set_messagedebug

有了hook_field_attach_validate,我们便不需要考虑具体的表单ID了,因为内容类型很多,实体类型也很多。我们不可能处理所有的情况,而hook_field_attach_validate可以帮助我们从这个问题中解脱出来。我在选用这个钩子以前,应该读过Field Attach APIhttp://api.drupal.org/api/drupal/modules%21field%21field.attach.inc/group/field_attach/7,以及http://api.drupal.org/api/drupal/modules%21field%21field.module/group/field/7, 当然,我还在api.drupal.org上面搜索相关的钩子函数:

图片1.png 

它的触发函数,field_attach_validate,我应该也读过的:

function field_attach_validate($entity_type, $entity) {

  $errors = array();

  // Check generic, field-type-agnostic errors first.

  _field_invoke_default('validate', $entity_type, $entity, $errors);

  // Check field-type specific errors.

  _field_invoke('validate', $entity_type, $entity, $errors);

 

  // Let other modules validate the entity.

  // Avoid module_invoke_all() to let $errors be taken by reference.

  foreach (module_implements('field_attach_validate') as $module) {

    $function = $module . '_field_attach_validate';

    $function($entity_type, $entity, $errors);

  }

 

  if ($errors) {

    throw new FieldValidationException($errors);

  }

}

我读的时候不够细致,忽略里面的注释。总之,我做了很多的准备工作的。


Drupal版本:

10 field_validation_field_delete

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

field_validation_field_deletehook_field_delete的钩子实现,这段代码的作用是,当删除一个字段时,删除字段上面的验证规则。这里面用到的field_dynamic_delete_rule函数,命名不符合Drupal的规范,最好命名为field_validation_dynamic_delete_rulefield_validation_field_deletefield_dynamic_delete_rule的名字也都是来源于webform_validation的。在webform_validation里面,使用的钩子函数是hook_node_delete,当一个节点被删除时,删除节点上面的验证规则,其实我觉得webform_validation里面应该检查一下这个节点的类型是不是webform形式的,不是的话直接跳过,这样效率会更高一点。


Drupal版本:

11 field_validation_rule_save

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

field_validation_rule_save函数是用来保存验证规则的,我现在觉得应该放到field_validation.rules.inc文件中去。这里值的学习的是drupal_write_record的使用,这个函数能够帮助我们少写很多代码,插入还是更新,对于程序员来说,我们经常需要写if语句做判断的,而drupal_write_record将插入和更新,很好的结合了起来:

drupal_write_record('field_validation_rule', $values);

drupal_write_record('field_validation_rule', $values, 'ruleid');

当然,这个函数里面再次调用了module_invoke_all,这意味着,我们的模块又提供了一个钩子函数供其它模块交互。这样写有好的地方,就是其它模块不用修改代码,就可以与我们的系统交互;不好的地方是,field_validation是一个很孤立的小功能,很少会有其它系统与我们交互,我的意思是说,这里的这个钩子是多余的,单纯为了创建钩子函数而已。当然这里的代码也是直接复制过来的。对当时的我来说,这个字段验证的模块已经足够复杂了。所以直接借鉴webform_validation的代码。如果让我自己写,是写不出来这样的代码的。


Drupal版本:

12 field_validation.validators.inc

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

这是field_validation.validators.inc文件的:

<?php

 

/**

 * @file

 * Provides validation functionality and hooks

 */

 

/**

 * Implements hook_webform_validation_validators().

 *

*/

function field_validation_field_validation_validators() {

  return array(

    'regex' => array(

      'name' => "Regular expression",

      'component_types' => array(

        'textfield',

        'textarea',

        'email',

        'hidden',

      ),

      'custom_error' => TRUE,

      'custom_data' => array(

        'label' => t('Regex code'),

        'description' => t('Specify regex code to validate the user input against.'),

      ),

      'description' => t("Validates user-entered text against a specified regular expression. Note: don't include delimiters such as /."),

    ),

  );

}

 

/**

 * Implements hook_webform_validation_validate().

 */

function field_validation_field_validation_validate($validator_name, $items, $rule) {

  if ($items) {

    $error = array();

    switch ($validator_name) {

     

      case "regex":

        mb_regex_encoding('UTF-8');

        $regex = $rule['data'];

        foreach ($items as $delta => $item) {

          if ($item['value'] != '' && (!mb_ereg("$regex", $item['value']))) {

$error[$delta][] = array(

              'error' => 'regex', 

              'message' =>$rule['error_message'],

            );

//debug($errors);

          }

        }

        return $error;

        break;

 

    }

  }

}

 

function _field_validation_flatten_array($val) {

 …

}

 

/**

 * Get a list of validator definitions

 */

function field_validation_get_validators() {

  $validators = module_invoke_all("field_validation_validators");

  // let modules use hook_webform_validator_alter($validators) to change validator settings

  drupal_alter('field_validator', $validators);

  return $validators;

}

 

…….

function field_validation_get_validators_selection() {

….

}

……

function field_validation_valid_component_types($validator) {

}

….

function _field_validation_all_allowed($allowed) {

….

}

function field_validation_get_validator_info($validator_key) {

}

….

function _field_validation_i18n_error_message($rule) {

….

}

……

function _field_validation_check_false($var) {

……

}

….

function _field_numeric_check_data($data) {

….

}

这个文件里面,我知道的好像只有field_validation_field_validation_validatorsfield_validation_field_validation_validatefield_validation_get_validators是有用的。其它的大部分都是特定于webform_validation的,当时是一股脑的复制了过来的,然后一个一个的字符串替换。我们看到这里保留了大量的webform_validation的信息。


Drupal版本:

13 field_validation_field_validation_validators

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

field_validation_field_validation_validators是一个钩子函数,实现的就是我们自己定义的钩子hook_field_validation_validators,它类似于hook_menu,返回的是一个数组,在这个数组里面,可以定义多个验证器,我们这里只定义了一个regex,正则表达式。在单个验证器数组里面,它包含5个键,namecomponent_typescustom_errorcustom_datadescription。都是直接来自于webform_validation的。我在这里出于方便的考虑,做了取舍,比如每个验证器都是需要自定义错误消息的,而在webform_validation里面,只有部分验证器可以自定义错误消息,为什么?其中一个原因就是,这些错误消息,需要用到英文,如果将来定义的验证器类型比较多的话,需要写很多这样的英文,自己的英文读还可以,写就不行了,怕出问题;另外一个就是,如果用英文的,还需要翻译,这对很多非英文的用户不方便,特别是中文用户;出于这两点考虑,我全部采用了自定义消息。这里的component_types其实应该改为field_types,表示这个验证器适用于哪些字段类型,实际上,我在程序里面,省去了这一检查,就是说一个验证器适用于所有的字段类型,这也是一个改进。在webform_validation,只有适用于一个组件类型的验证器,才能应用到该组件上,webform的组件类型比较少,主要定义在webform模块里面,而field的类型,除了Drupal核心自带的,很多第三方模块还定义了很多。我希望一个验证器可以应用于所有的字段类型,这一点上面不加限制,至于是否真的适用,由用户自己决定,而不是交给程序,写死在这里。


Drupal版本:

14 field_validation_field_validation_validate

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

field_validation_field_validation_validate这也是一个钩子函数的实现,这里面包含了验证器的验证规则。当传递过来的数据没有通过正则验证时,我们返回一个包含错误信息的数组。这个错误消息,应该通过引用传递的,但是module_invoke_all不支持引用传递。所以只好这样变通一下


Drupal版本:

15 field_validation_get_validators

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

函数field_validation_get_validators用来获取所有的字段验证器,这里再次使用了module_invoke_allhook_field_validation_validators就是在这里定义的,这里还使用了:

drupal_alter('field_validator', $validators);

这样其它模块就可以通过钩子hook_field_validator_alter来修改验证器定义了。

 

field_validation_get_validators_selectionfield_validation_get_validator_info这两个函数还是有一点用的,不过只在field_validation.admin.inc中用到。


Drupal版本:

16 field_validation.rules.inc

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

这是field_validation.rules.inc文件的:

<?php

 

/**

 * @file

 * provides API and management functions for the field validation rules

 */

 

/**

 * Get a rule entry

 */

function field_validation_get_rule($ruleid) {

  $result = db_query("SELECT * FROM {field_validation_rule} WHERE ruleid = :ruleid", array(':ruleid' => $ruleid), array('fetch' => PDO::FETCH_ASSOC));

  $rule = $result->fetchAssoc();

  return $rule;

}

 

/**

 * Get an array of rules assigned to a field instance

 */

function field_validation_get_field_rules($instance) {

  $rules = array();

$bundle = $instance['bundle'];

  $entity_type = $instance['entity_type'];

  $field_name = $instance['field_name'];

$sql = "SELECT * FROM {field_validation_rule} WHERE field_name = :field_name AND entity_type = :entity_type AND bundle = :bundle ORDER BY ruleid DESC";

  $result = db_query($sql, array(':field_name' => $field_name, ':entity_type' => $entity_type, ':bundle' => $bundle), array('fetch' => PDO::FETCH_ASSOC));

  foreach ($result as $rule) {

    $rules[$rule['ruleid']] = $rule;

  }

  return $rules;

}

 

/**

 * Get an array of rules assigned to a field instance

 */

function field_validation_get_bundle_rules($entity_type, $bundle) {

  $rules = array();

$sql = "SELECT * FROM {field_validation_rule} WHERE entity_type = :entity_type AND bundle = :bundle ORDER BY ruleid DESC";

  $result = db_query($sql, array(':entity_type' => $entity_type, ':bundle' => $bundle), array('fetch' => PDO::FETCH_ASSOC));

  foreach ($result as $rule) {

    $rules[$rule['ruleid']] = $rule;

  }

  return $rules;

}

 

 

/**

……

 */

function field_validation_rule_components_basic($components) {

……

}

 

/**

 * Delete a rule and dependencies

 */

function field_dynamic_delete_rule($ruleid) {

  // delete rule

  db_delete('field_validation_rule')

  ->condition('ruleid', $ruleid)

  ->execute();

}

这个文件里面主要包含了有关验证规则API函数,比如删除规则field_dynamic_delete_rule,获取一个规则field_validation_get_rule,获取一个字段上的规则field_validation_get_field_rules,获取一个bundle上面的规则field_validation_get_bundle_rules

这里的field_validation_rule_components_basic是复制过来的,这里应该没有什么用处。

    这里的代码没有什么特别的,主要就是数据库的查询操作,看看这里的代码或许对于熟悉Drupal7下面的数据库操作有点帮助,不过这里的查询方式都是最原始的db_query。在后面的项目中,我更倾向于使用EntityFieldQuery


Drupal版本:

17 field_validation.admin.inc

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

这是field_validation.admin.inc文件的:

<?php

 

/**

 * @file

 * Manages validation rules administration UI

 */

 

/**

 * Menu callback function to show an overview of the existing validation rules, and the option to add a rule

 */

function field_validation_manage($instance) {

  $rules = field_validation_get_field_rules($instance);

  $output = '';

  $output .= theme('field_validation_manage_overview', array('rules' => $rules, 'instance' => $instance));

  $output .= theme('field_validation_manage_add_rule', array('instance' => $instance));

$output .= '123456';

  return $output;

}

 

/**

 * Themable function to list the rules assigned to a webform

 */

function theme_field_validation_manage_overview($variables) {

  $rules = $variables['rules'];

  $instance = $variables['instance'];

 

  $header = array(t('Rule name'), t('Validator'), array(

      'data' => t('Operations'),

      'colspan' => 2,

    ));

  $validators = field_validation_get_validators_selection();

  if (!empty($rules)) {

    foreach ($rules as $rule) {

      $row = array();

      $row[] = array(

        'data' => $rule['rulename'],

      );

      $row[] = array(

        'data' => $validators[$rule['validator']],

      );

  $path = isset($_GET['q']) ? $_GET['q'] : '';

      $row[] = array(

        'data' => l(t('Edit'), $path.'/edit/' . $rule['validator'] . '/' . $rule['ruleid'], array("query" => drupal_get_destination())),

      );

      $row[] = array(

        'data' => l(t('Delete'), $path.'/delete/' . $rule['ruleid'], array("query" => drupal_get_destination())),

      );

      $rows[] = $row;

    }

  }

  else {

    $rows[][] = array(

      'data' => t('No validation rules available.'),

      'colspan' => 5,

    );

  }

 

  return theme('table', array('header' => $header, 'rows' => $rows));

}

 

/**

 * Callback function to add or edit a validation rule

 */

function field_validation_manage_rule($form, $form_state, $instance, $action = 'add', $validator = 'regex', $ruleid = NULL) {

  $form = array();

  $rule_validator = field_validation_get_validator_info($validator);

  $rule = field_validation_get_rule($ruleid);

  $form['rule'] = array(

    '#type' => 'fieldset',

    '#title' => ($action == 'edit') ? t('Edit rule') : t('Add rule'),

    '#collapsible' => FALSE,

    '#collapsed' => FALSE,

  );

 

  $form['rule']['validator'] = array(

    '#type' => 'hidden',

    '#value' => $validator,

  );

 

  $form['rule']['action'] = array(

    '#type' => 'hidden',

    '#value' => $action,

  );

 

  if ($action == 'edit' && $rule) {

    $form['rule']['ruleid'] = array(

      '#type' => 'hidden',

      '#value' => $rule['ruleid'],

    );

 

    $form['rule']['field_name'] = array(

      '#type' => 'hidden',

      '#value' => $rule['field_name'],

    );


  $form['rule']['entity_type'] = array(

      '#type' => 'hidden',

      '#value' => $rule['entity_type'],

    );


  $form['rule']['bundle'] = array(

      '#type' => 'hidden',

      '#value' => $rule['bundle'],

    );

  }

  else {

    $form['rule']['field_name'] = array(

      '#type' => 'hidden',

      '#value' => $instance['field_name'],

    );


  $form['rule']['entity_type'] = array(

      '#type' => 'hidden',

      '#value' => $instance['entity_type'],

    );


  $form['rule']['bundle'] = array(

      '#type' => 'hidden',

      '#value' => $instance['bundle'],

    );

  }

 

  $form['rule']['rulename'] = array(

    '#type' => 'textfield',

    '#title' => t('Rule name'),

    '#default_value' => (isset($rule['rulename'])) ? $rule['rulename'] : '',

    '#required' => TRUE,

    '#size' => 60,

    '#maxlength' => 255,

    '#weight' => 1,

  );

 

  if (isset($rule_validator['custom_data']) && is_array($rule_validator['custom_data'])) {

    $required = isset($rule_validator['custom_data']['required']) ? $rule_validator['custom_data']['required'] : TRUE;

    $form['rule']['data'] = array(

      '#type' => 'textfield',

      '#title' => $rule_validator['custom_data']['label'],

      '#description' => $rule_validator['custom_data']['description'],

      '#required' => ($required !== FALSE) ? TRUE : FALSE,

      '#size' => 60,

      '#maxlength' => 255,

      '#default_value' => $rule['data'],

      '#weight' => 4,

    );

  }

 

  if (isset($rule_validator['custom_error'])) {

    $form['rule']['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' => $rule['error_message'],

      '#weight' => 5,

    );

  }

 

  $form['rule']['submit'] = array(

    '#type' => 'submit',

    '#value' => (isset($rule['ruleid'])) ? t('Edit rule') : t('Add rule'),

    '#weight' => 25,

  );

 

  return $form;

}

 

/**

 * Validation handler to add / edit a rule

 */

function field_validation_manage_rule_validate($form, &$form_state) {

  $values = $form_state['values'];

  if ($values['action'] == 'edit') {

    if (!is_numeric($values['ruleid']) || $values['ruleid'] == 0) {

      form_set_error(NULL, t('A problem occurred while editing this rule. Please try again.'));

    }

  }

}

 

 

/**

 * Submit handler to add / edit a rule

 */

function field_validation_manage_rule_submit($form, &$form_state) {

  $values = $form_state['values'];

  field_validation_rule_save($values);

}

 

/**

 * Confirmation form to delete a rule

 */

function field_validation_delete_rule($form, &$form_state, $rule) {

  if (isset($rule['ruleid'])) {

    $form['ruleid'] = array(

      '#type' => 'value',

      '#value' => $rule['ruleid'],

    );

  }

 

  return confirm_form($form,

    t('Are you sure you want to delete the rule %name?', array('%name' => $rule['rulename'])),

    isset($_GET['destination']) ? $_GET['destination'] : $_GET['q'],

    t('This action cannot be undone.'),

    t('Delete'),

    t('Cancel')

  );

}

 

/**

 * Submit handler to delete a rule

 */

function field_validation_delete_rule_submit($form, &$form_state) {

  $ruleid = $form_state['values']['ruleid'];

  field_dynamic_delete_rule($ruleid);

  module_invoke_all('field_validation', 'rule', 'delete', $ruleid);

}


Drupal版本:

18 确认表单的使用

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

admin.inc文件,负责的是Field validationUI界面,用来向字段添加规则、编辑规则、删除规则、浏览规则的。这里面有几点可供借鉴,一个是确认表单的使用:

return confirm_form($form,

    t('Are you sure you want to delete the rule %name?', array('%name' => $rule['rulename'])),

    isset($_GET['destination']) ? $_GET['destination'] : $_GET['q'],

    t('This action cannot be undone.'),

    t('Delete'),

    t('Cancel')

  );

通常在删除一个东西的时候,使用这个确认表单,当用户执行一个动作,而这个动作又是不可以撤销的情况下,我们可以让用户确认一下,这个时候如果用户是不小心点到的,看到警告信息,可能就会取消操作。


Drupal版本:

19 添加/编辑表单合二为一

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

另外就是添加/编辑验证规则的表单,使用了同一个函数field_validation_manage_rule,只不过在这个函数内部,做了判断。这样的话,便可以达到复用代码的作用,因为添加和编辑表单两者之间,相似度是非常大的,在Drupal内部,将两者合并是最常见的用法。我以前写程序的时候,最开始开发Drupal的时候,总是将两者分开写。

另外就是theme(‘table’)的使用,输出一个表格也没有什么难的,这里提供了一个具体的例子。


Drupal版本:

2 Field validation的Alpha1版

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

我在hook_field_attach_validate的实验成功以后,便开始寻求更一般的解决办法了。当时章林已经使用form_alter解决这个问题了,我同时也告诉了他在Drupal7下面,可以采用hook_field_attach_validate来解决这样的问题。如果仅仅是为了满足项目的需要,这个问题就可以这样结束了。每当我们遇到字段验证的时候,套用已有的代码,改造改造就可以了。

但是,我并没有这样停止下来。寻找更简单的解决办法,让字段验证可以像 webform验证一样,可以通过配置来解决问题,这对于很多人来说,是很有用的。借用模块webform_validation的名字,改造一下field_validation,我发现这个名字竟然没有被占用。想一想吧,很多好的模块名字,早已经被占用完毕了。我多少觉得自己非常的幸运。

我已经想象出来了,这个模块未来将会被上万多个站点使用,它的应用范围,与webform_validation相比,用途更广,用户量更大。我是希望它能够成为TOP 100里面的一员。这是我很早很早以前,便有的一个想法,就像希望自己的模块的安装量,能够超过东哥一样。

我在外研社,花了一个下午来开发这个模块,后来回到家里,又花了一个晚上,才有了基本的雏形,紧接着,自己又花了一个周末的时间,终于搞定了最初的alpha1版。在alpha1版里面,实现了通过配置,为字段添加正则表达式验证的功能。

现在,我们可以下载http://drupal.org/node/1148738 里面的alpha1版,来看看里面的代码。此外,大家也可以到http://drupal.org/node/1107346, 下载webform validation7.x-1.0版。我最初的代码,很多都是从webform validation里面复制过来的。

首先,我从头到尾的完整的读过webform validation的代码,觉得很不错,至少那个时候,觉得这个已经相当复杂了。同时,我还测试过webform validation模块的具体用法。但是将webform validation改造成field_validation的过程也是相当的痛苦的,调试,调试再调试。大致的过程是这样的,首先是数据库设计,也就是install文件,这个可以工作;接着是自己手工的在数据库里面,插入一个验证规则,然后检查这个验证规则,能够正常工作;最后是编写规则的配置界面。第一步比较简单,第二步是关键,第三步就是规则的增删改查,一个体力活而已,但是调试起来比较麻烦。


Drupal版本:

20 补充说明

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

代码里面问题很多,我把这样的代码放到了drupal.org上面去以后,有人用过以后,给出了大量的改进意见,还有人提出了赤裸裸的批评。不过alpha1里面的代码,是可以工作的,很好的解决了我们实际中的问题。

其实阅读别人的代码是一个很痛苦的事情,上来我就放了这么多的代码,我想很多人都不愿读下去了。学Drupal也一样,只有足够的代码阅读量以后,写起模块才会得心应手。我读过Drupal5的源代码、Drupal6的源代码,Ubercart的源代码,Commerce的源代码,Views的源代码,Drupal7的大部分源代码。所以对于学Drupal的人,想以这个技能为生的人,建议找一些优秀的模块,读读别人写的代码。读一读自己的同事写的代码。

读完field validation的代码以后,再读读webform validation的代码,别骂我无耻啊,很多代码,基本上都是原封不动的复制过来的。我仅仅把webfrom替换成为了field而已。不过我还是有点自知之明的,在field validation的项目页面,介绍了这些代码的出处,直接说明了,是从webform validation模块里面复制过来的。也算是对webform validation模块作者的一种尊敬。我觉得这一点,至少比国内的很多人,我讲的那些抄袭过后连名字都改了的人,好很多。

Alpha1版,带着众多的问题,问世了,那个时候,drupal.org正在从CSV转到GIT,我对GIT的操作不熟悉,创建了项目页面以后,直接在issue里面传上了代码。后来在小白的帮助下,才学会使用GIT


Drupal版本:

21 Field Validation Beta1版的改进

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

很多人提出了很多的问题,其中一个很简单,就是代码里面带有调试信息,相关问题可以参看http://drupal.org/node/1157324,问题的提出者是Lullabotericduran。我按照他提交的补丁,去除了调试信息,当然,还删除了很多从webform_validation复制过来的无用的函数。


Drupal版本:

22 删除无用的函数

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

哪些函数无用,没有被其它程序调用的孤立函数是无用的。自己使用nodepadd++的查找功能,逐个函数查找,然后确定,哪些函数确实用不到,用不到的就删除了。

field_validation.validators.inc里面删除了函数:

_field_validation_flatten_array

field_validation_valid_component_types

_field_validation_all_allowed

_field_validation_i18n_error_message

_field_validation_check_false

_field_numeric_check_data

 

field_validation.rules.inc文件里面删除了:

field_validation_rule_components_basic


Drupal版本:

23 使用module_implements替代module_invoke_all

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

axel.rutz还发现了一个bughttp://drupal.org/node/1149684),就是如果向多个字段添加验证规则的话,只有最后一个起作用,我最终是这样解决这个问题的,将field_validation_field_attach_validate里面的代码:

 

$errors[$rule['field_name']][$langcode] = module_invoke_all("field_validation_validate", $rule['validator'], $items, $rule);

替换为:

//module_invoke_all does not work here, so i call it directly.

foreach (module_implements("field_validation_validate") as $module) {

  $function = $module . '_' . "field_validation_validate";

  if (function_exists($function)) {

    $function($rule['validator'], $rule, $langcode, $items,  $errors);

  }

}

 

module_invoke_allmodule_implements的转换,是一个进步,使我认识到module_invoke_all是无法传递引用的。field_validation_field_validation_validate也做了相应的调整,这是调整后的样子:

/**

 * Implements hook_field_validation_validate().

 */

function field_validation_field_validation_validate($validator_name, $rule, $langcode, $items, &$errors) {

  if (!empty($items)) {

    switch ($validator_name) {

      case "regex":

        mb_regex_encoding('UTF-8');

        $regex = $rule['data'];

        foreach ($items as $delta => $item) {

          if ($item['value'] != '' && (!mb_ereg("$regex", $item['value']))) {

    $errors[$rule['field_name']][$langcode][$delta][] = array(

              'error' => 'regex_'.$rule['ruleid'], 

              'message' => t($rule['error_message']),

            );

          }

        }

        break;

    }

  }

}

这里可以传递引用参数&$errors了,这里还有另外一个改进,就是为错误消息使用了t函数。


Drupal版本:

24 补充说明

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

很多人提出了新的功能:

1),验证规则可以导入导出。

2),一个验证规则,可以直接应用到多个字段实例,而不是一个一个的复制。

3),多内置一些正则表达式规则,这样用户就不用输入了。

4),验证规则可以取反。

5),希望能够对字段的数据进行预处理,比如使用trim函数。

6),有人还给出了正则表达式的正确格式,我这里使用的是mb_ereg,很多人开始不习惯。mb_ereg是从webform_validation里面复制过来的,直到前不久(Beta1版发布后的1年后),我才知道PHP里面支持两种格式的正则表达式。

这些新功能在beta1里面并没有实现。在修正了明显的bug以后,我发布了Beta1版。Beta1版里面的改进,就是上面所说的。不久,Lullabotericduran写了一篇文章,专门介绍这个模块,http://www.lullabot.com/articles/module-monday-field-validationLullabot是全球知名的Drupal培训公司,自己编写的模块,能够在他们的首页出现,也是一个莫大的荣誉。随之而来的,是模块被更多的人关注,包括webform_validation的作者svendecabootersvendecabooter有一个想法,就是把表单验证、webform验证、字段验证统一起来。这个想法很好,很早的时候,就有人做了尝试,比如Validation API

Svendecabooter的建议,让我感到诚惶诚恐,他写的webform_validation模块,用户量要远远大于刚出来的Field validation。如果他要写一个涵盖所有验证方面的模块的话,那么Field validation就会被包含到里面,最终也将会被废弃掉,因为大家都采用新的模块了。因此我建议Svendecabooter接管Validation API模块,开发一个Drupal7的版本,来涵盖webform

、字段、表单等验证。Svendecabooter接管了Validation API,但是由于他工作比较忙,一直迟迟没有动工。

Field validation是我的练习自己Drupal技能的模块,那个想凭借着它超越东哥的念头是一致存在的,所以内心深处,是比较不喜欢与Svendecabooter的合作的。


Drupal版本:

25 从Beta2到Beta6

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

function field_validation_get_bundle_rules($entity_type, $bundle) {

  $rules = array();

$sql = "SELECT * FROM {field_validation_rule} WHERE entity_type = :entity_type AND bundle = :bundle ORDER BY ruleid DESC";

  $result = db_query($sql, array(':entity_type' => $entity_type, ':bundle' => $bundle), array('fetch' => PDO::FETCH_ASSOC));

  foreach ($result as $rule) {

    $rules[$rule['ruleid']] = $rule;

  }

  return $rules;

}

 

 

/**

……

 */

function field_validation_rule_components_basic($components) {

……

}

 

/**

 * Delete a rule and dependencies

 */

function field_dynamic_delete_rule($ruleid) {

  // delete rule

  db_delete('field_validation_rule')

  ->condition('ruleid', $ruleid)

  ->execute();

}

这个文件里面主要包含了有关验证规则API函数,比如删除规则field_dynamic_delete_rule,获取一个规则field_validation_get_rule,获取一个字段上的规则field_validation_get_field_rules,获取一个bundle上面的规则field_validation_get_bundle_rules

这里的field_validation_rule_components_basic是复制过来的,这里应该没有什么用处。

    这里的代码没有什么特别的,主要就是数据库的查询操作,看看这里的代码或许对于熟悉Drupal7下面的数据库操作有点帮助,不过这里的查询方式都是最原始的db_query。在后面的项目中,我更倾向于使用EntityFieldQuery


Drupal版本:

26 field_validation.admin.inc

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

这是field_validation.admin.inc文件的:

<?php

 

/**

 * @file

 * Manages validation rules administration UI

 */

 

/**

 * Menu callback function to show an overview of the existing validation rules, and the option to add a rule

 */

function field_validation_manage($instance) {

  $rules = field_validation_get_field_rules($instance);

  $output = '';

  $output .= theme('field_validation_manage_overview', array('rules' => $rules, 'instance' => $instance));

  $output .= theme('field_validation_manage_add_rule', array('instance' => $instance));

$output .= '123456';

  return $output;

}

 

/**

 * Themable function to list the rules assigned to a webform

 */

function theme_field_validation_manage_overview($variables) {

  $rules = $variables['rules'];

  $instance = $variables['instance'];

 

  $header = array(t('Rule name'), t('Validator'), array(

      'data' => t('Operations'),

      'colspan' => 2,

    ));

  $validators = field_validation_get_validators_selection();

  if (!empty($rules)) {

    foreach ($rules as $rule) {

      $row = array();

      $row[] = array(

        'data' => $rule['rulename'],

      );

      $row[] = array(

        'data' => $validators[$rule['validator']],

      );

  $path = isset($_GET['q']) ? $_GET['q'] : '';

      $row[] = array(

        'data' => l(t('Edit'), $path.'/edit/' . $rule['validator'] . '/' . $rule['ruleid'], array("query" => drupal_get_destination())),

      );

      $row[] = array(

        'data' => l(t('Delete'), $path.'/delete/' . $rule['ruleid'], array("query" => drupal_get_destination())),

      );

      $rows[] = $row;

    }

  }

  else {

    $rows[][] = array(

      'data' => t('No validation rules available.'),

      'colspan' => 5,

    );

  }

 

  return theme('table', array('header' => $header, 'rows' => $rows));

}

 

/**

 * Callback function to add or edit a validation rule

 */

function field_validation_manage_rule($form, $form_state, $instance, $action = 'add', $validator = 'regex', $ruleid = NULL) {

  $form = array();

  $rule_validator = field_validation_get_validator_info($validator);

  $rule = field_validation_get_rule($ruleid);

  $form['rule'] = array(

    '#type' => 'fieldset',

    '#title' => ($action == 'edit') ? t('Edit rule') : t('Add rule'),

    '#collapsible' => FALSE,

    '#collapsed' => FALSE,

  );

 

  $form['rule']['validator'] = array(

    '#type' => 'hidden',

    '#value' => $validator,

  );

 

  $form['rule']['action'] = array(

    '#type' => 'hidden',

    '#value' => $action,

  );

 

  if ($action == 'edit' && $rule) {

    $form['rule']['ruleid'] = array(

      '#type' => 'hidden',

      '#value' => $rule['ruleid'],

    );

 

    $form['rule']['field_name'] = array(

      '#type' => 'hidden',

      '#value' => $rule['field_name'],

    );


  $form['rule']['entity_type'] = array(

      '#type' => 'hidden',

      '#value' => $rule['entity_type'],

    );


  $form['rule']['bundle'] = array(

      '#type' => 'hidden',

      '#value' => $rule['bundle'],

    );

  }

  else {

    $form['rule']['field_name'] = array(

      '#type' => 'hidden',

      '#value' => $instance['field_name'],

    );


  $form['rule']['entity_type'] = array(

      '#type' => 'hidden',

      '#value' => $instance['entity_type'],

    );


  $form['rule']['bundle'] = array(

      '#type' => 'hidden',

      '#value' => $instance['bundle'],

    );

  }

 

  $form['rule']['rulename'] = array(

    '#type' => 'textfield',

    '#title' => t('Rule name'),

    '#default_value' => (isset($rule['rulename'])) ? $rule['rulename'] : '',

    '#required' => TRUE,

    '#size' => 60,

    '#maxlength' => 255,

    '#weight' => 1,

  );

 

  if (isset($rule_validator['custom_data']) && is_array($rule_validator['custom_data'])) {

    $required = isset($rule_validator['custom_data']['required']) ? $rule_validator['custom_data']['required'] : TRUE;

    $form['rule']['data'] = array(

      '#type' => 'textfield',

      '#title' => $rule_validator['custom_data']['label'],

      '#description' => $rule_validator['custom_data']['description'],

      '#required' => ($required !== FALSE) ? TRUE : FALSE,

      '#size' => 60,

      '#maxlength' => 255,

      '#default_value' => $rule['data'],

      '#weight' => 4,

    );

  }

 

  if (isset($rule_validator['custom_error'])) {

    $form['rule']['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' => $rule['error_message'],

      '#weight' => 5,

    );

  }

 

  $form['rule']['submit'] = array(

    '#type' => 'submit',

    '#value' => (isset($rule['ruleid'])) ? t('Edit rule') : t('Add rule'),

    '#weight' => 25,

  );

 

  return $form;

}

 

/**

 * Validation handler to add / edit a rule

 */

function field_validation_manage_rule_validate($form, &$form_state) {

  $values = $form_state['values'];

  if ($values['action'] == 'edit') {

    if (!is_numeric($values['ruleid']) || $values['ruleid'] == 0) {

      form_set_error(NULL, t('A problem occurred while editing this rule. Please try again.'));

    }

  }

}

 

 

/**

 * Submit handler to add / edit a rule

 */

function field_validation_manage_rule_submit($form, &$form_state) {

  $values = $form_state['values'];

  field_validation_rule_save($values);

}

 

/**

 * Confirmation form to delete a rule

 */

function field_validation_delete_rule($form, &$form_state, $rule) {

  if (isset($rule['ruleid'])) {

    $form['ruleid'] = array(

      '#type' => 'value',

      '#value' => $rule['ruleid'],

    );

  }

 

  return confirm_form($form,

    t('Are you sure you want to delete the rule %name?', array('%name' => $rule['rulename'])),

    isset($_GET['destination']) ? $_GET['destination'] : $_GET['q'],

    t('This action cannot be undone.'),

    t('Delete'),

    t('Cancel')

  );

}

 

/**

 * Submit handler to delete a rule

 */

function field_validation_delete_rule_submit($form, &$form_state) {

  $ruleid = $form_state['values']['ruleid'];

  field_dynamic_delete_rule($ruleid);

  module_invoke_all('field_validation', 'rule', 'delete', $ruleid);

}



这是field_validation.admin.inc文件的:

<?php

 

/**

 * @file

 * Manages validation rules administration UI

 */

 

/**

 * Menu callback function to show an overview of the existing validation rules, and the option to add a rule

 */

function field_validation_manage($instance) {

  $rules = field_validation_get_field_rules($instance);

  $output = '';

  $output .= theme('field_validation_manage_overview', array('rules' => $rules, 'instance' => $instance));

  $output .= theme('field_validation_manage_add_rule', array('instance' => $instance));

$output .= '123456';

  return $output;

}

 

/**

 * Themable function to list the rules assigned to a webform

 */

function theme_field_validation_manage_overview($variables) {

  $rules = $variables['rules'];

  $instance = $variables['instance'];

 

  $header = array(t('Rule name'), t('Validator'), array(

      'data' => t('Operations'),

      'colspan' => 2,

    ));

  $validators = field_validation_get_validators_selection();

  if (!empty($rules)) {

    foreach ($rules as $rule) {

      $row = array();

      $row[] = array(

        'data' => $rule['rulename'],

      );

      $row[] = array(

        'data' => $validators[$rule['validator']],

      );

  $path = isset($_GET['q']) ? $_GET['q'] : '';

      $row[] = array(

        'data' => l(t('Edit'), $path.'/edit/' . $rule['validator'] . '/' . $rule['ruleid'], array("query" => drupal_get_destination())),

      );

      $row[] = array(

        'data' => l(t('Delete'), $path.'/delete/' . $rule['ruleid'], array("query" => drupal_get_destination())),

      );

      $rows[] = $row;

    }

  }

  else {

    $rows[][] = array(

      'data' => t('No validation rules available.'),

      'colspan' => 5,

    );

  }

 

  return theme('table', array('header' => $header, 'rows' => $rows));

}

 

/**

 * Callback function to add or edit a validation rule

 */

function field_validation_manage_rule($form, $form_state, $instance, $action = 'add', $validator = 'regex', $ruleid = NULL) {

  $form = array();

  $rule_validator = field_validation_get_validator_info($validator);

  $rule = field_validation_get_rule($ruleid);

  $form['rule'] = array(

    '#type' => 'fieldset',

    '#title' => ($action == 'edit') ? t('Edit rule') : t('Add rule'),

    '#collapsible' => FALSE,

    '#collapsed' => FALSE,

  );

 

  $form['rule']['validator'] = array(

    '#type' => 'hidden',

    '#value' => $validator,

  );

 

  $form['rule']['action'] = array(

    '#type' => 'hidden',

    '#value' => $action,

  );

 

  if ($action == 'edit' && $rule) {

    $form['rule']['ruleid'] = array(

      '#type' => 'hidden',

      '#value' => $rule['ruleid'],

    );

 

    $form['rule']['field_name'] = array(

      '#type' => 'hidden',

      '#value' => $rule['field_name'],

    );


  $form['rule']['entity_type'] = array(

      '#type' => 'hidden',

      '#value' => $rule['entity_type'],

    );


  $form['rule']['bundle'] = array(

      '#type' => 'hidden',

      '#value' => $rule['bundle'],

    );

  }

  else {

    $form['rule']['field_name'] = array(

      '#type' => 'hidden',

      '#value' => $instance['field_name'],

    );


  $form['rule']['entity_type'] = array(

      '#type' => 'hidden',

      '#value' => $instance['entity_type'],

    );


  $form['rule']['bundle'] = array(

      '#type' => 'hidden',

      '#value' => $instance['bundle'],

    );

  }

 

  $form['rule']['rulename'] = array(

    '#type' => 'textfield',

    '#title' => t('Rule name'),

    '#default_value' => (isset($rule['rulename'])) ? $rule['rulename'] : '',

    '#required' => TRUE,

    '#size' => 60,

    '#maxlength' => 255,

    '#weight' => 1,

  );

 

  if (isset($rule_validator['custom_data']) && is_array($rule_validator['custom_data'])) {

    $required = isset($rule_validator['custom_data']['required']) ? $rule_validator['custom_data']['required'] : TRUE;

    $form['rule']['data'] = array(

      '#type' => 'textfield',

      '#title' => $rule_validator['custom_data']['label'],

      '#description' => $rule_validator['custom_data']['description'],

      '#required' => ($required !== FALSE) ? TRUE : FALSE,

      '#size' => 60,

      '#maxlength' => 255,

      '#default_value' => $rule['data'],

      '#weight' => 4,

    );

  }

 

  if (isset($rule_validator['custom_error'])) {

    $form['rule']['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' => $rule['error_message'],

      '#weight' => 5,

    );

  }

 

  $form['rule']['submit'] = array(

    '#type' => 'submit',

    '#value' => (isset($rule['ruleid'])) ? t('Edit rule') : t('Add rule'),

    '#weight' => 25,

  );

 

  return $form;

}

 

/**

 * Validation handler to add / edit a rule

 */

function field_validation_manage_rule_validate($form, &$form_state) {

  $values = $form_state['values'];

  if ($values['action'] == 'edit') {

    if (!is_numeric($values['ruleid']) || $values['ruleid'] == 0) {

      form_set_error(NULL, t('A problem occurred while editing this rule. Please try again.'));

    }

  }

}

 

 

/**

 * Submit handler to add / edit a rule

 */

function field_validation_manage_rule_submit($form, &$form_state) {

  $values = $form_state['values'];

  field_validation_rule_save($values);

}

 

/**

 * Confirmation form to delete a rule

 */

function field_validation_delete_rule($form, &$form_state, $rule) {

  if (isset($rule['ruleid'])) {

    $form['ruleid'] = array(

      '#type' => 'value',

      '#value' => $rule['ruleid'],

    );

  }

 

  return confirm_form($form,

    t('Are you sure you want to delete the rule %name?', array('%name' => $rule['rulename'])),

    isset($_GET['destination']) ? $_GET['destination'] : $_GET['q'],

    t('This action cannot be undone.'),

    t('Delete'),

    t('Cancel')

  );

}

 

/**

 * Submit handler to delete a rule

 */

function field_validation_delete_rule_submit($form, &$form_state) {

  $ruleid = $form_state['values']['ruleid'];

  field_dynamic_delete_rule($ruleid);

  module_invoke_all('field_validation', 'rule', 'delete', $ruleid);

}


Drupal版本:

27 确认表单的使用

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

admin.inc文件,负责的是Field validationUI界面,用来向字段添加规则、编辑规则、删除规则、浏览规则的。这里面有几点可供借鉴,一个是确认表单的使用:

return confirm_form($form,

    t('Are you sure you want to delete the rule %name?', array('%name' => $rule['rulename'])),

    isset($_GET['destination']) ? $_GET['destination'] : $_GET['q'],

    t('This action cannot be undone.'),

    t('Delete'),

    t('Cancel')

  );

通常在删除一个东西的时候,使用这个确认表单,当用户执行一个动作,而这个动作又是不可以撤销的情况下,我们可以让用户确认一下,这个时候如果用户是不小心点到的,看到警告信息,可能就会取消操作。


Drupal版本:

28 添加/编辑表单合二为一

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

另外就是添加/编辑验证规则的表单,使用了同一个函数field_validation_manage_rule,只不过在这个函数内部,做了判断。这样的话,便可以达到复用代码的作用,因为添加和编辑表单两者之间,相似度是非常大的,在Drupal内部,将两者合并是最常见的用法。我以前写程序的时候,最开始开发Drupal的时候,总是将两者分开写。

另外就是theme(‘table’)的使用,输出一个表格也没有什么难的,这里提供了一个具体的例子。


Drupal版本:

29 补充说明

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

代码里面问题很多,我把这样的代码放到了drupal.org上面去以后,有人用过以后,给出了大量的改进意见,还有人提出了赤裸裸的批评。不过alpha1里面的代码,是可以工作的,很好的解决了我们实际中的问题。

其实阅读别人的代码是一个很痛苦的事情,上来我就放了这么多的代码,我想很多人都不愿读下去了。学Drupal也一样,只有足够的代码阅读量以后,写起模块才会得心应手。我读过Drupal5的源代码、Drupal6的源代码,Ubercart的源代码,Commerce的源代码,Views的源代码,Drupal7的大部分源代码。所以对于学Drupal的人,想以这个技能为生的人,建议找一些优秀的模块,读读别人写的代码。读一读自己的同事写的代码。

读完field validation的代码以后,再读读webform validation的代码,别骂我无耻啊,很多代码,基本上都是原封不动的复制过来的。我仅仅把webfrom替换成为了field而已。不过我还是有点自知之明的,在field validation的项目页面,介绍了这些代码的出处,直接说明了,是从webform validation模块里面复制过来的。也算是对webform validation模块作者的一种尊敬。我觉得这一点,至少比国内的很多人,我讲的那些抄袭过后连名字都改了的人,好很多。

Alpha1版,带着众多的问题,问世了,那个时候,drupal.org正在从CSV转到GIT,我对GIT的操作不熟悉,创建了项目页面以后,直接在issue里面传上了代码。后来在小白的帮助下,才学会使用GIT


Drupal版本:

3 info文件

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

我们来看一下,最初的代码,这是info文件的:

name = Field Validation

description = "Add validation rules to fields."

core = 7.x

dependencies[] = field

dependencies[] = field_ui

files[] = field_validation.admin.inc

files[] = field_validation.install

files[] = field_validation.module

files[] = field_validation.rules.inc

files[] = field_validation.validators.inc

 

这里面的键值的含义,在第一集里面已经讲过了。我这里补充一点,下面的这段代码:

files[] = field_validation.admin.inc

files[] = field_validation.validators.inc

是多余的。Drupal7里面有个注册表机制,计划解决缓加载这个问题,但是在Drupal7正式发布的时候,这个功能只实现了一小部分,通常只有对那些带有PHP类、接口的文件,使用这里的files键,用来缓加载它们。这是一个很有意思的问题。以前讲过,后面还会讲到,缓加载。


Drupal版本:

30 Field Validation Beta1版的改进

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

很多人提出了很多的问题,其中一个很简单,就是代码里面带有调试信息,相关问题可以参看http://drupal.org/node/1157324,问题的提出者是Lullabotericduran。我按照他提交的补丁,去除了调试信息,当然,还删除了很多从webform_validation复制过来的无用的函数。


Drupal版本:

32 删除无用的函数

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

validators.inc里面删除了函数:

_field_validation_flatten_array

field_validation_valid_component_types

_field_validation_all_allowed

_field_validation_i18n_error_message

_field_validation_check_false

_field_numeric_check_data

 

field_validation.rules.inc文件里面删除了:

field_validation_rule_components_basic


Drupal版本:

33 使用module_implements替代module_invoke_all

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

axel.rutz还发现了一个bughttp://drupal.org/node/1149684),就是如果向多个字段添加验证规则的话,只有最后一个起作用,我最终是这样解决这个问题的,将field_validation_field_attach_validate里面的代码:

 

$errors[$rule['field_name']][$langcode] = module_invoke_all("field_validation_validate", $rule['validator'], $items, $rule);

替换为:

//module_invoke_all does not work here, so i call it directly.

foreach (module_implements("field_validation_validate") as $module) {

  $function = $module . '_' . "field_validation_validate";

  if (function_exists($function)) {

    $function($rule['validator'], $rule, $langcode, $items,  $errors);

  }

}

 

module_invoke_allmodule_implements的转换,是一个进步,使我认识到module_invoke_all是无法传递引用的。field_validation_field_validation_validate也做了相应的调整,这是调整后的样子:

/**

 * Implements hook_field_validation_validate().

 */

function field_validation_field_validation_validate($validator_name, $rule, $langcode, $items, &$errors) {

  if (!empty($items)) {

    switch ($validator_name) {

      case "regex":

        mb_regex_encoding('UTF-8');

        $regex = $rule['data'];

        foreach ($items as $delta => $item) {

          if ($item['value'] != '' && (!mb_ereg("$regex", $item['value']))) {

    $errors[$rule['field_name']][$langcode][$delta][] = array(

              'error' => 'regex_'.$rule['ruleid'], 

              'message' => t($rule['error_message']),

            );

          }

        }

        break;

    }

  }

}

这里可以传递引用参数&$errors了,这里还有另外一个改进,就是为错误消息使用了t函数。


Drupal版本:

34 补充说明

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

很多人提出了新的功能:

1),验证规则可以导入导出。

2),一个验证规则,可以直接应用到多个字段实例,而不是一个一个的复制。

3),多内置一些正则表达式规则,这样用户就不用输入了。

4),验证规则可以取反。

5),希望能够对字段的数据进行预处理,比如使用trim函数。

6),有人还给出了正则表达式的正确格式,我这里使用的是mb_ereg,很多人开始不习惯。mb_ereg是从webform_validation里面复制过来的,直到前不久(Beta1版发布后的1年后),我才知道PHP里面支持两种格式的正则表达式。

这些新功能在beta1里面并没有实现。在修正了明显的bug以后,我发布了Beta1版。Beta1版里面的改进,就是上面所说的。不久,Lullabotericduran写了一篇文章,专门介绍这个模块,http://www.lullabot.com/articles/module-monday-field-validationLullabot是全球知名的Drupal培训公司,自己编写的模块,能够在他们的首页出现,也是一个莫大的荣誉。随之而来的,是模块被更多的人关注,包括webform_validation的作者svendecabootersvendecabooter有一个想法,就是把表单验证、webform验证、字段验证统一起来。这个想法很好,很早的时候,就有人做了尝试,比如Validation API

Svendecabooter的建议,让我感到诚惶诚恐,他写的webform_validation模块,用户量要远远大于刚出来的Field validation。如果他要写一个涵盖所有验证方面的模块的话,那么Field validation就会被包含到里面,最终也将会被废弃掉,因为大家都采用新的模块了。因此我建议Svendecabooter接管Validation API模块,开发一个Drupal7的版本,来涵盖webform

、字段、表单等验证。Svendecabooter接管了Validation API,但是由于他工作比较忙,一直迟迟没有动工。

Field validation是我的练习自己Drupal技能的模块,那个想凭借着它超越东哥的念头是一致存在的,所以内心深处,是比较不喜欢与Svendecabooter的合作的。


Drupal版本:

35 从Beta2到Beta6

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

Beta1版写好以后,我们在项目中一直用它,但是此后的相当长的时间内,我并没有继续开发Field validation。一直过了将近四个月的时间,到2011年的9月份的时候,才开始进一步的完善Field validation。为什么要等这么久,一个原因是当时项目比较忙,另一个就是面对Field validation引起的关注,自己无所适从。


Drupal版本:

36 Beta2版

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

Field validation的安装量,一直在缓慢的增长着,一直到了当时的9月份,增长的速度才快了起来。这样的增长速度,远远低于我的预期。到了9月份,安装量的增加,让我对Field validation的信心增长了不少。我是这样想的,现在只有一个正则表达式验证器,如果我能够多写几个验证器的话,那么用户量就会增加很多。因此我决定增加验证器的数量。在Beta2版里面,我增加以下验证器:

•Numeric values (optionally specify min and / or max value)(数值,可指定min/max

 •Minimum length(最小长度)

 •Maximum length(最大长度)

 •Minimum number of words(最小字数)

 •Maximum number of words(最大字数)

 •Plain text (disallow tags) (纯文本)

 •Must be empty (Anti-Spam: Hide with CSS) (必须为空)

 •Words blacklist(黑名单)

这些验证器都是从webform_validation里面迁移过来的,对我来说,不用去考虑那些界面英文怎么写了。对应的验证逻辑代码,也比较简单。


Drupal版本:

37 beta3版

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

两周后,我又增加了5个验证器,同时修正了一个bug,发布了beta3。这5个验证器分别为:

•Minimum number of selections required(适用于复选框、下拉选择框,最小选中多少项)

•Maximum number of selections allowed(适用于复选框、下拉选择框,最多选中多少项)

•Exact number of selections required(适用于复选框、下拉选择框,只能选中多少项)

•Unique(唯一性验证)

•Match another field(or entity property)(比如匹配一个字段或者一个属性)

在唯一性验证的代码里面,首先,我们检查这个字段当前的值数组里面,是否有重复的,如果没有的话,通过查询数据库,看数据库里面是否存在重复的,如果有重复的,表示不唯一,否则表示字段值是唯一的。如果我们的当前表单是编辑表单,我们在查询数据库的时候,还需要把当前实体排除在外。注意,用户编辑页面,通过entity_extract_ids获取不到用户的ID,不知道现在Drupal核心修正了这个bug没有,所以我单独的为它写了一个判断语句。

以前人们经常使用unique field模块,来验证字段的惟一性,现在有了Field ValidationUnique验证器,基本上就可以取代这个模块了,而且Field Validation支持所有的实体类型。unique field模块仅支持节点类型。

验证器匹配一个字段或者一个属性也使用了EntityFieldQuery,我对这种查询方式的喜欢,可能就是从编写这两个验证器开始的,在这里我发现了EntityFieldQuery的强大。如果使用db_select,也能够实现同样的功能。但是就会比较麻烦,代码的可读性也不高。


Drupal版本:

38 菜单项中路径的构成不能超过10

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

Beta3里面解决了 无法为评论字段添加验证规则http://drupal.org/node/1297366)这个问题,它的原因是这样的,Drupal中的菜单项,里面定义的路径,最多只能包含10个组成部分,超过了这个限制,就无法正常工作。对于评论字段,为它添加验证的菜单项超过了这个限制,也就是路径包含的部分太多了。

我修改了菜单项$items["$path/fields/%field_ui_menu/validation"]的回调函数,将它改为field_validation_callback_dispatch,在回调函数里面做了判断。

这样,在评论的字段管理界面,路径:

admin/structure/types/manage/article/comment/fields/comment_body/validation/add/min_words

会被看作菜单项$items["$path/fields/%field_ui_menu/validation"]

admin/structure/types/manage/article/comment/fields/comment_body/validation

通过field_validation_callback_dispatch函数,这个路径仍然能够被正确处理。这是一个很有意思的问题,以前是不知道Drupal有这个限制的。


Drupal版本:

39 Beta4版

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

两周后,自己又新增了5个验证器

•Specific value(s) :白名单,所填的值必须是给定的值中的一个

•Require at least one of several fields: 多个字段至少填写其中的一个

•Equal values on multiple fields :多个字段中的值必须相等

•Unique values on multiple fields :多个字段中的值必须不同

•Custom PHP function : 自定义PHP函数验证器


Drupal版本:

4 field_validation.install文件

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

这是field_validation.install文件的:

<?php

 

/**

 * @file

 * field_validation installation file

 */

 

/**

 * Implements hook_schema().

 */

function field_validation_schema() {

  $schema['field_validation_rule'] = array(

    'description' => 'Stores rule definitions',

    'fields' => array(

      'ruleid' => array(

        'type' => 'serial',

        'description' => 'Unique identifier for a rule',

        'unsigned' => TRUE,

        'not null' => TRUE,

      ),

      'rulename' => array(

        'type' => 'varchar',

        'description' => 'Name for the rule',

        'not null' => TRUE,

        'default' => '',

        'length' => 255,

      ),

'field_name' => array(

        'type' => 'varchar',

        'length' => 32,

        'not null' => TRUE,

        'default' => ''

      ),

      '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,

      ),

      'data' => array(

        'type' => 'varchar',

        'description' => 'Additional rule data',

        'not null' => FALSE,

        'length' => 255,

      ),

      '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;

}

我们来看install文件,这里面的代码很简单,就是用来创建一个数据库表,为数据库表定义了它可以包含那些列,以及数据库表的主键、索引。中国人的抄袭、模仿的能力是比较强的,我也是中国人麻,直接把webform_validation.install里面的代码复制了过来。当然,我做了取舍。webform_validation里面定义了两个数据库表,而我这里只定义了其中的一个,为什么?为什么我没有允许一个规则可以应用到多个字段上面来,就像webform validation里面的那样,一个规则可以应用到多个webform component(组件)上面来?

如果最初这样设计的话,也是可以的,但是我主要考虑到webform component都是单值的,而字段可以是单值的,也可以是多值的,还可以是复合字段,也就是说字段远比webform component复杂。如果一个规则对应多个字段的话,程序会很复杂。而一个规则对应一个字段的话,程序会简单很多。

后来,有很多人,提出来了,如果一个规则能够支持多个字段,就好了,但是一直都没有实现,因为牵涉到的东西太多。而一个规则应用到一个字段的话,实现以后,只需要复制一下,就可以将它应用到多个字段了。简单的,就是好的。在这个问题上,我的想法是正确的。


Drupal版本:

40 对column的支持

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

另外增加的功能是实现了对column的支持,使得验证器可以作用于所有的字段类型,至少从理论上面来说是这样的。我们访问一个字段的值,通常是这样的:

$node->field_myfield[‘und’][0][‘value’]

但是也有可能是这样的:

$node->field_myterm[‘und’][0][‘tid’]

或者:

$node->field_myuser[‘und’][0][‘uid’]

最后面的中括号里面的value,tid,uid就是这里所说的column,这样一说,相信大家更好理解了。最初实现这个功能的时候,数据库里面的存储结构也使用了column,但是出了问题,后来通过检查,发现,原来columnMYSQL的保留字,所以在schema里面的定义是这样的:

      'col' => array(

        'type' => 'varchar',

        'length' => 32,

        'not null' => TRUE,

        'default' => 'value'

      ),

   我把column调整为了col,当然程序里面的相应代码也做了调整。


Drupal版本:

41 “validate”链接

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

在字段管理界面,添加了一个“validate”链接,方便为字段添加验证。这里使用仍然是我们熟悉的hook_form_FORM_ID_alter, 当然这里面用到的一些API函数,可能很多人都不熟悉,我也不怎么熟悉,这些代码都是照抄过来的。

Beta4是一个比较经典的版本,到了这里,field validation从功能上已经相当完善了,包含了webform validation上面的所有功能。


Drupal版本:

42 Beta5的问世

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

做开源的东西,并不是完全没有出路,用的人多了,自然就会有人找上门来,Beata4问世不久,有个美国人,找上门来,想让我帮它写一个有关日期范围的验证,这个日期验证,我很早就思考过,当这个需求到来的时候,我便高兴地揽下了这个小活,160美金,美国人也觉得很便宜。这样便有了date validation这个子模块。这个日期时间范围的验证,我自己觉得写的相当漂亮的,而且很通用,美国人的要求,比我写的模块涵盖的功能,要小很多,而我直接写了一个通用的出来。


Drupal版本:

43 使用form_set_error

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

Beta5里面,有一个重大的改变,就是这段代码:

  /*

   $errors[$rule['field_name']][$langcode][$delta][] = array(

              'error' => 'date_range_'.$rule['ruleid'], 

              'message' => t($rule['error_message']),

            );

*/

if($flag){

  $error = $rule['field_name'].']['.$langcode.']['.$delta.']['.$rule['col'];

  $message = t($rule['error_message']);

  form_set_error($error,$message);

}

以前设置错误消息的时候,我总是使用$errors数组,设置,但是它有一个问题,对于emaillink这样的字段不起作用,就是说数组形式,只适用于一部分情况,还有一部分情况不适用。我开始不是很想使用form_set_error,但是这里为了解决这个问题,不得不这样。但是使用了form_set_error以后,又出现了一个新的问题,一个实体表单,嵌套到另一个表单里面的时候,form_set_error就不起作用了,不是不起作用,而是不能完全正确的工作了,为什么?因为这里我们假定了 表单元素的名字的结构是这样的:

$error = $rule['field_name'].']['.$langcode.']['.$delta.']['.$rule['col'];

一般情况下,都是正确的,但是在嵌套表单的情况下,就出了问题。有兴趣的,可以看一下field collection的嵌套形式。


Drupal版本:

44 实现hook_module_implements_alter

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.comBeta5module文件里面,还引入了这段代码:

/**

 * Implements hook_module_implements_alter().

 * 

 * Ensures the call to field_validation_form_field_ui_field_overview_form_alter()

 * function runs after any invocation of the form_alter() by other modules, e.g.

 * Field Group module.

 */

function field_validation_module_implements_alter(&$implementations, $hook) {

  if ($hook == 'form_alter' && array_key_exists('field_validation', $implementations)) {

    $group = $implementations['field_validation'];

    unset($implementations['field_validation']);

    $implementations['field_validation'] = $group;

  }

}

这段代码的作用是,让field_validation模块的钩子hook_form_alter实现,放在最后面执行,这样就不会和field group模块冲突。

我们经常会遇到这样的问题,多个模块同时实现了一个钩子,此时执行的顺序,有时候非常重要,比如hook_form_alter就属于这样的钩子。在以前的情况下,我们采用修改模块重量的方式,简单一些的话,就是直接使用phpmyadmin,打开数据库的system表,直接修改对应模块的重量。复杂一点的话,把它放到安装文件的hook_install钩子里面。

/**

 * Implements hook_install().

 */

function field_validation_install() {

  db_query("UPDATE {system} SET weight = 10 WHERE name = 'field_validation'");

}

    这里顺带说一下,在2.X版本里面,我们实现了对Feeds的集成,实现了hook_feeds_processor_targets_alter这个钩子,但是我们的实现,和Feeds模块自身的实现冲突了,所以我调整了一下模块的重量,通过调整重量,使得程序能够正常工作了.


Drupal版本:

45 Beta6是一个过渡

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

里面只包含一些代码格式的修正,我自己编写的代码,开始的时候,很不符合Drupal的编码规范,在beta6里面做了改进。为什么要发布beta6呢,因为我想把当前的一些改进做一个备份,接下来要实现验证规则的导入导出。


Drupal版本:

5 module文件

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

这是module文件的代码:

<?php

 

/**

 * @file

 * Add validation rules to webforms

 */

 

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';

 

/**

 * Implements hook_menu().

 */

function field_validation_menu() {

  $items = array();

 

foreach (entity_get_info() as $entity_type => $entity_info) {

    if ($entity_info['fieldable']) {

      foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) {

        if (isset($bundle_info['admin'])) {

          // Extract path information from the bundle.

          $path = $bundle_info['admin']['path'];

          if (isset($bundle_info['admin']['bundle argument'])) {

            $bundle_arg = $bundle_info['admin']['bundle argument'];

            $bundle_pos = (string) $bundle_arg;

          }

          else {

            $bundle_arg = $bundle_name;

            $bundle_pos = '0';

          }

          // This is the position of the %field_ui_menu placeholder in the

          // items below.

          $field_position = count(explode('/', $path)) + 1;

 

          // Extract access information, providing defaults.

          $access = array_intersect_key($bundle_info['admin'], drupal_map_assoc(array('access callback', 'access arguments')));

          $access += array(

            'access callback' => 'user_access',

            'access arguments' => array('administer site configuration'),

          );

 

          $items["$path/fields/%field_ui_menu/validation"] = array(

            'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),

            'title' => 'Validation',

            'page callback' => 'field_validation_manage',

            'page arguments' => array($field_position),

            'type' => MENU_LOCAL_TASK,

            'file' => 'field_validation.admin.inc',

          ) + $access;


          $items[$path.'/fields/%field_ui_menu/validation/add'] = array(

            'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),

            'title' => 'Add validation',

'page callback' => 'drupal_get_form',

'page arguments' => array('field_validation_manage_rule', $field_position, 'add'),

            'type' => MENU_CALLBACK,

            'file' => 'field_validation.admin.inc',

          ) + $access;


$items["$path/fields/%field_ui_menu/validation/edit"] = array(

            'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),

            'title' => 'Edit rule',

'page callback' => 'drupal_get_form',

'page arguments' => array('field_validation_manage_rule', $field_position, 'edit'),

            'type' => MENU_CALLBACK,

            'file' => 'field_validation.admin.inc',

          ) + $access;


$items["$path/fields/%field_ui_menu/validation/delete"] = array(

            'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),

            'title' => 'Delete rule',

'page callback' => 'drupal_get_form',

'page arguments' => array('field_validation_delete_rule', $field_position+3),

            'type' => MENU_CALLBACK,

            'file' => 'field_validation.admin.inc',

          ) + $access;

        }

      }

    }

  }


  return $items;

}

 

/**

 * Implements hook_theme().

 */

function field_validation_theme() {

  return array(

    'field_validation_manage_add_rule' => array(

      'variables' => array(

        'instance' => NULL,

      ),

    ),

    'field_validation_manage_overview' => array(

      'variables' => array(

        'rules' => NULL,

        'instance' => NULL,

      ),

    ),

  );

}

 

/**

 * Implements hook_field_attach_validate().

 */

function field_validation_field_attach_validate($entity_type, $entity, &$errors) {

  //ToDo;

list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);

$rules = field_validation_get_bundle_rules($entity_type, $bundle);

if($rules){

foreach ($rules as $rule) {

$field_name = $rule['field_name'];

      $field = field_info_field($field_name);

  $languages = field_available_languages($entity_type, $field);

foreach ($languages as $langcode) {

  $items = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();

$errors[$rule['field_name']][$langcode] = module_invoke_all("field_validation_validate", $rule['validator'], $items, $rule);

}


}

  }

}

 

/**

 * Theme the 'add rule' list

 */

function theme_field_validation_manage_add_rule($variables) {

  $instance = $variables['instance'];

  $output = '';

  $validators = field_validation_get_validators();

 

  if ($validators) {

    $output = '<h3>' . t('Add a validation rule') . '</h3>';

    $output .= '<dl>';

    foreach ($validators as $validator_key => $validator_info) {

      $item = '';

  $path = isset($_GET['q']) ? $_GET['q'] : '';

      $url = $path.'/add/' . $validator_key;

      $components = ' (' . implode(', ', $validator_info['component_types']) . ')';

      $item = '<dt>' . l($validator_info['name'], $url, array("query" => drupal_get_destination())) . '</dt>';

      $item .= '<dd>';

      if ($validator_info['description']) {

        $item .= $validator_info['description'] . ' ';

      }

      $item .= $components . '</dd>';

      $output .= $item;

    }

    $output .= '</dl>';

  }

  return $output;

}

 

/**

 * Implements hook_field_delete().

 */

function field_validation_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) {

  $rules = field_validation_get_field_rules($instance);

  if ($rules) {

    foreach (array_keys($rules) as $ruleid) {

      field_dynamic_delete_rule($ruleid);

    }

  }

}

 

/**

 * Save a validation rule. Data comes from the admin form

 * or nodeapi function in case of node clone

 */

function field_validation_rule_save($values) {

  // save rules data

  if ($values['action'] == 'add') {

    drupal_write_record('field_validation_rule', $values);

    $ruleid = $values['ruleid'];

    if ($ruleid) {

      module_invoke_all('field_validation', 'rule', 'add', $values);

    }

  }

 

  if ($values['action'] == 'edit') {

    drupal_write_record('field_validation_rule', $values, 'ruleid');

    $ruleid = $values['ruleid'];

    if ($ruleid) {

      module_invoke_all('field_validation', 'rule', 'edit', $values);

    }

  }

}


Drupal版本:

6 field_validation_field_attach_validate

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

这里面,最先实现的是field_validation_field_attach_validate,在这个钩子函数中,alpha1版本里面包含两大段注释代码,最下面的那段注释代码,就是我最初测试hook_field_attach_validate所用到的代码,在这个版中还保留着,幸好还保留着,这样可以留下一个成长的轨迹。最上面那段注释里面的代码,是最初的实现代码。把它的格式整理一下:

  // 获取一个bundle上面所有的字段实例.

  $instances = field_info_instances($entity_type, $bundle);

  //debug($instances);

  // 对每个字段实例进行迭代处理,

  foreach ($instances as $instance) {

    $field_name = $instance['field_name'];

    $field = field_info_field($field_name);

    // 获取字段上面可用的语言.

    $languages = field_available_languages($entity_type, $field);

    //对于每个语言进行迭代

    foreach ($languages as $langcode) {

      $items = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();

      debug($items);

      //查找这个字段实例上面,是否存在验证规则,如果存在验证规则,执行这些验证

      $rules = field_validation_get_field_rules($instance);

      if($rules){

        foreach ($rules as $rule) {

          module_invoke_all("field_validation_validate", $rule['validator'], $items, $rule, $errors);

        }

      }

    }

  }

这段代码也是可以工作的,为什么把它注释掉呢?因为我找到了更好的办法了。没有被注释掉的代码,就是更好的办法。在注释掉的代码中,我们首先获取这个实体上面都有哪些字段,然后对字段循环处理,分别检查每个字段上面是否存在验证规则,如果存在,再进行验证。   

实际上,大部分的字段上面都没有验证规则的,根据字段循环迭代,浪费了。改进的代码,逻辑是这样的,检查这个实体类型上面是否存在验证规则,如果存在,分别对这些规则进行验证,有多少规则验证多少,没有的话,跳过去。具体的代码实现,在后面多次的改进,变更,但是这个基本逻辑,却被保留了下来。这就是我们前面看到的代码,我这里加点注释说明:

 

  //获取bundle上面的所有验证规则,这个函数位于field_validation.rules.inc中,

//是最早实现的函数之一

  $rules = field_validation_get_bundle_rules($entity_type, $bundle);

  if($rules){

    //对于每个规则循环处理

    foreach ($rules as $rule) {

      $field_name = $rule['field_name'];

      $field = field_info_field($field_name);

      $languages = field_available_languages($entity_type, $field);

      foreach ($languages as $langcode) {

        $items = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();

//这里面仍然使用了module_invoke_all,但是这里是有返回值的,

//我们将返回值赋值给了$errors[$rule['field_name']][$langcode]

//这与最初的实现不同,最初的实现里面,是把$errors传递了过去,

//不过module_invoke_all是无法传递引用的,因此无法正常工作

//我经过调试,调试才通过这个办法解决了这个问题,当然这里还是有问题的

        $errors[$rule['field_name']][$langcode] = module_invoke_all("field_validation_validate", $rule['validator'], $items, $rule);

      }

    }

  }

我开始的时候,是没有意识到module_invoke_all无法传递引用的,这也是为什么我列出field_attach_validate这个API函数源代码的原因,里面的注释,说明了这一点,但是最初的时候,自己没有看注释,或者看的不仔细。实际调试的过程中,才发现问题,最后才恍然大悟。

我在这里,第一次实现了自己的Hook,这是我至今都值得骄傲的事情。很久以前,就想自己定义钩子,一直没有机会。尽管这里的钩子,是借鉴hook_webform_validation_validate而来的,但是终归也是自己第一次定义一个钩子函数,并且后来,还有其他开发者使用了这个钩子函数。


Drupal版本:

7 include_once

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

最上面的两个include_once,用来将文件field_validation.validators.incfield_validation.rules.inc加载进来,当加载field_validation.module文件的时候,就会自动的加载这两个文件。为什么要把它分成多个文件呢?这样做唯一的好处就是,逻辑更清楚一点,比如field_validation.validators.inc只放置验证器的定义。它并不能实现缓加载,也并不能提升性能。如果我们把field_validation.validators.inc里面的函数,直接放到module文件中,效果是一样的。很多模块都采用这种方式,或许有的人认为,它能够实现缓加载,但是实际是不行的。


Drupal版本:

8 field_validation_menu

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

field_validation_menu里面,最上面的那段代码,是我从field_ui模块里面复制过来的,最后实现UI的时候,考虑到字段的配置,它的路径不是固定的,不同实体类型的路径是不同的,而我的验证规则,是追加到字段上面去的,所以只好从field_ui模块复制代码。如果让我自己写,我是写不出来的。这里只需要理解就可以了,知道这段代码的出处。通过Hook_menu,我们为每个字段提供了一个验证概览页面,一个验证规则的添加页面,一个验证规则的编辑页面,还有验证规则的删除页面。除了这里的路径是动态的以外,并不比我们见到的其它菜单项复杂。


Drupal版本:

9 field_validation_theme

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

field_validation_theme这个钩子函数,是直接从webform_validation_theme抄袭过来的,里面注册的主题函数,也是改了一下名字而已。theme_field_validation_manage_add_rule,这个主题函数,也是重命名过来的,但是函数里面的内容,我做了稍微的调整,其实这个函数只在管理界面用到,放到field_validation.admin.inc文件里面更合适一点。因为theme_field_validation_manage_overview也是放在field_validation.admin.inc里面的。theme_field_validation_manage_add_rule里面有两段代码值得学习,一个是获取当前路径:

$path = isset($_GET['q']) ? $_GET['q'] : '';

另一个是生成一个链接,这里向链接传递了更多的选项:

l($validator_info['name'], $url, array("query" => drupal_get_destination()))


Drupal版本: