Drupal专业开发指南(Drupal6版)

这本书已经发布了许久,本来没有打算升级,春节期间,读了一遍以后,发现里面变动很大.而drupalbar上面的升级版,也迟迟没有动态.

为了不影响我的培训班的进行,将这个drupal5的版本升级到drupal6中,从今天开始吧,不过我是从后往前升级的,以避免重复的劳动.

Drupal6版,比5有了很多的改动,大多数地方被重写了,而且又新增了一些知识点.慢慢的升级,希望在我的培训班开始以后的3个月内,把它升级完成.

技术书籍的翻译,有一个难点,那就是你需要懂得它说得什么,首先要理解,其次要实践,它讲的东西,你会用,然后才能翻译出来,刚开始会觉得很容易.比如,但是后来你会发现越来越难,我翻译5的时候,打算2个月内结束战斗的,希望能够速战速决,2个月,每周10个工作日,加在一起,就是20天,全书400页,一天只需要翻译20页,就可以搞定了,但是实际上一直持续了半年之久.

所以不敢确定这个升级版,什么时候能够完全出来.希望越快越好吧.

 

最后在这里感谢,lullabot的VnDyk,虽然这里直接利用了他们的成果,但是应该没有损害他们的利益。

 

 

鉴于译文无法出版,也带来不了任何收益,本站开始公开连载译文,希望这份资料能够帮助更多的在Drupal门外徘徊的人.

Drupal版本:

 老葛的Drupal培训班 http://www.thinkindrupal.com

在一年多以前,我为本书的第一版写了序言。那时,在Drupal世界里缺少的就是一本开发用书。通过编写本书的第一版,John VanDyk 和Matt Westgate为Drupal的持续发展,做出了难以置信的贡献。在我遇到的Drupal开发者中,每人都有一本第一版的Drupal专业开发指南

 
    Drupal,通过它的开源本性,已经成为了一个伟大的软件,这比我预期的要好很多。Drupal开发者社区勇于创新,同时以极大的热情拥抱web开发的每个技术变革,并努力为web开发者提供近乎无限的可能性。在Drupal社区中,变是永恒的,也是我们成功的关键所在。
 
    在本书的第一版出版以后,我们发布了Drupal 6,这向前迈进了一大步,它包含了新的和改进了的API。事实上,有超过700多的开发者为Drupal6的核心代码贡献了力量。通过共同努力,我们对主题系统作了重要改进,对多语言网站提供了更好的支持,此外还改进了菜单系统、表单API、JavaScript,等等。最终的结果是,与Drupal5相比,Drupal6是一个更优秀的web应用开发平台。
 
    这可能让John和Matt失望了(对不起!),Drupal专业开发指南最初版本的所有章节,都已经或多或少的过时了。
 
    幸好,本书的第2版修正了所有的这些问题。本书覆盖了Drupal6下的各种功能和开发工具,为底层开发提供了深度视角,并揭示了Drupal6背后的设计理念。每当我们为Drupal发布一个新的主版本的时候,Drupal都会吸引更多的用户和开发者。所以,如果对于Drupal6还缺少什么的话,那么它就是本书了,我非常感谢John修订并扩展了本书。
 
    有了本书和Drupal源代码的一份拷贝以后,你就可以参与到Drupal社区之中,并为Drupal的开发贡献力量了。如果你指出了如何可以做的更好,比如与原有方式相比,使用了更少的代码或者更优雅的方式或者速度更快,那么请把它告诉我们,我们专注于Drupal核心的稳定、灵活、强大。我非常乐意去评估和提交你的Drupal核心补丁,我敢肯定其他的维护者也非常乐意这样。
 
Dries Buytaert
Drupal创始人和项目负责人
 

Drupal版本:

关于作者

老葛的Drupal培训班 http://www.thinkindrupal.com

 

JOHN VANDYK最初开始接触计算机,是在一个黑色的Bell & Howell Apple II上,为Little Brick Out,打印和检查BASIC代码,以增加paddle宽度。在发现Drupal以前,John参与了UserLand Frontier社区,并使用Ruby编写了自己的内容管理系统(和Matt Westgate)。
 
John是一名web架构师,效力于Lullabot,一个著名的Drupal教育和咨询公司。在此以前,John在Iowa州立科技大学的昆虫系工作,他是一名系统分析员和助理教授。他的硕士论文是关于鹿鸣的耐寒性的,他的博士论文关于正在研究的使用相片来创建3维虚拟昆虫。
 
John和他的妻子Tina生活在Ames, Iowa。他们在家里教育他们的一群孩子,他们已经习惯了在睡觉前听父亲讲段Drupal故事“节点修订本在多个关联之地上的历险记”。

Drupal版本:

技术审稿人

老葛的Drupal培训班 http://www.thinkindrupal.com

ROBERT DOUGLASS的Drupal探索始于2003年,他创建了自己的个人网站RobsHouse.net。在2005年,他与人合著了使用Drupal, phpBB, 和WordPress构建在线社区。作为第一本深度讲解Drupal的书籍,它为Drupal的初学者和熟手提供了颇有价值的指南。
    ROBERT还负责google夏季代码项目中的Drupal相关部分,他还在许多会议上宣传Drupal,同时在网上发表了许多Drupal相关的技术文章,另外他还是德国柏林Drupal用户小组的创始人。
 

    作为Acquia的高级顾问,ROBERT努力让各种各样的个人、组织能够方便高效的使用Drupal。ROBERT喜欢经典音乐和开源软件,并把它们看作是动力和精神的源泉。

Drupal版本:

关于译者

 老葛的Drupal培训班 http://www.thinkindrupal.com

老葛,thinkindrupal.com (原zhupou.cn)的站长,专职从事Drupal工作尽两年的时间了,Drupal专业开发指南第一版中文版的译者。从翻译第一版至今,站长给中国的Drupal社区带来了千余页的Drupal中文技术文档,帮助很多人特别是初学者掌握了许多基本的、高级的Drupal技能。
 
    站长经常参与北京的Drupal聚会,经常为大家讲解最新的Drupal技能。同时还完整的汉化了Ubercart简体中文包,为社区贡献了Ubercart下的支付宝模块uc_alipay。
 
    站长爱好中国象棋和远程的徒步有氧运动,经常在北京的西山徒步。
 
   现在住在深圳,在深圳发展。

Drupal版本:

致谢

老葛的Drupal培训班 http://www.thinkindrupal.com

首先,感谢我的家庭,在编写本书的过程中,特别是在“一个简单的修订”转变为了一个和第一版同样巨大的工程时,他们的理解和支持,给了我莫大的鼓舞。
 
Drupal是一个基于社区的工程。如果没有这么多人的共同努力,编写文档,提交错误报告,创建和检查改进,Drupal就不会像今天这样成功,当然也就不会有这本书了。
 
但是在这么多人当中,请允许我感谢那些对本书做出特别贡献的人。
 
它们包括#drupal IRC(internet relay chat)栏目所有成员,他们耐心的回答各种问题,包括Drupal是怎么工作的,为什么要用特定的方式编写代码,以及为什么有些代码是好的而另一些为什么是坏的,等等。很多人为本书作出了重要贡献,这包括,Brandon Bergren, Øivind Binde, Larry “Crell” Garfield, Dmitri Gaskin, Charlie Gordon, Gerhard Killesreiter, Greg Knaddison, Druplicon, Rob Loach, Chad Phillips,和Oleg Terenchuck.。对于那些做出贡献而在此遗漏的人表示歉意。
 
Robert Douglass, Károly Négyesi, Addison Berry, Angela Byron,Heine Deelstra, Jeff Eaton, Nathan Haug, Kevin Hemenway, Gábor Hojtsy, Barry Jaspan,Earl Miles, 和James Walker认真的检查了本书的全部或部分手稿,在这里,对他们表示特别的感谢。
 
感谢爱荷华州立大学(Iowa State University)的Joel Coats,感谢Lullabot的全体成员,感谢他们对本书的支持。
 
感谢Apress小组在示例代码需要不断修改时的容忍和理解,以及魔术般的将我的手稿转变为了一本书籍。
 
最后,感谢Dries Buytaert将Drupal贡献给了整个世界。
 

Drupal版本:

译者致谢

老葛的Drupal培训班 http://www.thinkindrupal.com

首先,感谢我的父母及亲人,感谢他们的从小到大的理解和支持。
   
    感谢eskalate的罗先生和曹先生,没有他们辛勤的市场开拓,就没有eskalate的Drupal团队,也就没有了译者的Drupal技能。
    感谢同事liu、dikers、deny、geoge、andy、shark、john、blade,感谢他们在Java和Drupal学习上的帮助与支持,很多Drupal技能是在与同事共同解决客户的问题时学到的。
    感谢jcob和jason,他们不辞辛劳的组织北京的Drupal聚会。
    最后,感谢JOHN VANDYK和Matt Westgate,感谢他们为Drupal编写了一本很棒的书籍。当然,也感谢一下Dries Buytaert,没有他就没有Drupal了。

 

Drupal版本:

译者序

老葛的Drupal培训班 http://www.thinkindrupal.com

去年的现在,经过半年的时间,终于基本上完整的译完了本书的第一版,尽管译文中错误千出,但是还是被很多网友传阅。最初翻译这本书的时候,仅仅是因为我在看XMLRPC那一章的时候,看了多遍,但是没有看懂,就一点儿一点儿的把它写在了笔记本上(这里的笔记本不是笔记本电脑,而是用钢笔写字的纸质的笔记本)。自己后来就把翻译好的文章输入到了电脑中,然后放在了我的博客上。这是第一篇,接着便有了完整的翻译整本书的冲动,从最初预计的两个月,利用周末的时间把它译完,一直持续了半年之久,才最终完整的收工。
       第一版的译文从未校对过,里面错别字很多,有近一半的译文是先翻译在笔记本上的,然后自己再将其输入到电脑中;而所有的译文,也都是我人工翻译的,限于译者的水平有限,个别地方实在不知道讲的什么,只好将每个英文单词翻译成汉语,然后再拼凑而成,所以给人留下了机器翻译的痕迹。很多地方,这样翻译完了,才明白原文的真实含义。翻译的过程,就是学习的过程。
       由于时间跨度太长,译者在开始的时候,也根本不大理解Drupal,所以第一版的翻译可能会让人感觉是两个人翻译的。由于工作的忙碌,自己没有时间去校对里面的各种错误,所以说“错误千出”一点也不过分。
       决定翻译第2版,是年后的事情,从最初预计的20天,到30天,再到40天,最后50天,又补了10天60天,这才基本完工。决定翻译时,我看到drupalbar的朋友也在翻译,本来想把翻译的合并在一起,这样节省一点时间,但是不久便搬家了,上网非常不便,这样就自己独自完成了整本的翻译,含正文的23章、两个附录,还有序言、导言、作者简介等。
译文是我重新翻译的,部分地方是在原有译文的基础上修订而成。
       所有的译文,都是在译者读懂了原文的真正含义以后,才动工翻译的,比如触发器一章的翻译,我是在前前后后读了11遍之后,才动手翻译的。安装轮廓、批处理API的翻译也是如此,读了10余遍之后,才动手翻译。菜单系统、数据库模式API、主题系统、表单API、文件系统、jQuery、本地化,以及模块的维护,这些要么是大改,要么是新增的,而其余的章节,也有这样那样或多或少的改动,这些都是需要花费时间,花费精力的。附录1中的数据库模式的翻译,也是独立完成的。
       除了最后的130页,其中包括第21章、第23章、附录1、第12章的部分、第13章、还有5页的表单API,未经校对以外,其余全部中文译文都经过3遍的校对;此外,这一版是完整的汉化,比如“管理➤站点构建➤模块”,这些全部采用中文,而不是英文,译者大量的使用、借鉴了简体/繁体中文包的译文。对于简体中文包中的疏漏,在译者看来的疏漏,部分也做了修正,如果你看到部分地方与简体中文包中不一致,那么这很有可能就是我的修正版,当然,我的修正也未必是完全正确的。
       第2版的译文历时60余日,中间面临来自这样或那样的压力,但是译者还是顶住了这些压力,一鼓作气,将其全部搞定,其中辛苦,只有亲身体验了才会得知。
 

       最后,限于译者的水平、精力有限,对于译文的不对不妥之处,欢迎指正。

Drupal版本:

导言

老葛的Drupal培训班  http://www.thinkindrupal.com

程序员的学习历程就是一个非常有趣的旅程。首先是,分别的去学习、摸索一个软件系统的各个独立的子模块,通过对这些模块的学习来理解整个系统。当你达到了一定的程度以后,接着你就开始研究系统的内核,尝试着编写自己的代码来操纵系统的行为。这就是我们如何学习的——多读别人的代码、多写自己的代码。
 
你坚持这一模式一段时间以后,你发现自己达到了一个新的高度,你可以从头构建一个自己的系统了。例如,你自己编写了一个内容管理系统,并把它部署到多个站点上,这样是不是很酷,觉得自己撬动了地球。
 
但是接着又会到达一个关键点,当你发现对你系统的维护比构建该特性所耗费的时间还要多时,你就到达了这一关键点。你想根据自己现在所掌握的知识来从头构建整个系统。你还发现,许多其它的系统出现了,你的系统能做的,它们能做甚至做的更好,你的系统不能做的,它们也能实现。而且它们还有一个社区,在这里,来自全球各地的开发者在一起努力的改进这个软件,这时,你终于发现,这些系统在大多数方面都优于你自己的系统。更让人难以置信的是,这个软件是免费的,并且是开源的。
 
这就是我们的经历,我想你可能也会遇到类似的情况。旅程的终点让人感到欣慰——成千上万的开发者在为同一个项目而努力。在这里,你找到了朋友;你编写了自己的模块;最重要的,和你自己单打独斗的时候一样,你仍然感觉到自己在做一件有意义的事情。
 
这本书适用于3种读者。首先,这里有大量的插图,包括各种图表和流程图;还有许多内容摘要,这为想了解Drupal是什么、Drupal能做什么的初学者提供了方便。其次,本书包含了大量的代码片段和示例模块。这适用于有些基础,想在Drupal框架之上做定制开发的读者。我们建议你,安装Drupal,在阅读本书的同时动手实践这些例子(最好再有一个调试器),这样你很快就会熟悉Drupal了。最后,本书包含了大量的评论、提示、还有对代码图片的详细解释,这将整本书有机的联系到了一起。这适用于想成为Drupal高手的人。
 
如果你是初学者,我们建议你从头逐章阅读本书,因为前面的是基础,是后面章节的预备条件。
 
最后,你可以从http://drupalbook.com 或者 www.apress.com下载到本书的示例代码,流程图和图片。
 
       祝您好运并欢迎来到Drupal社区!
 

Drupal版本:

第一章 Drupal的工作原理

老葛的Drupal培训班 http://www.thinkindrupal.com

在本章中,我们将为你展示Drupal的概貌。我们将会在以后章节中,对Drupal中的每个部分的工作原理进行详细的介绍。在这里,我们将讨论Drupal运行所用到的技术堆栈, Drupal包含的各种文件,和Drupal使用的各种不同的概念术语,比如节点,钩子,区块和主题。
 
      什么是Drupal
       Drupal是用来构建网站的。它是一个高度模块化,开源的web内容管理框架,并且非常注重合作,互动的重要性。它的特点包括可扩展性强,符合标准,追求简洁代码,内核精练。Drupal自带了一些基本的核心功能,其它的额外功能可通过安装模块核心可选模块或者第3方模块来实现。我们可以基于Drupal进行定制,但是定制是通过覆写核心模块或者增加模块来完成的,而不是修改核心组件中的代码。它还将内容管理和内容表示这二者进行了成功的分离。
     Drupal可被用来构建一个互联网门户;个人的,部门的,或者公司的网站;一个电子商务站点;一个资源分类站点;一个在线报纸;一个图库;一个内部网,Drupal的应用非常广泛,这里仅仅提到了其中的一部分。它甚至可用于远程教育。
    有一个专门的安全小组,他们通过对回应危害和发布安全更新来保证Drupal的安全。另外还有一个非营利性的组织,Drupal协会,它通过改进drupal.org网站的基础设施,组织Drupal聚会和各种活动,来推动Drupal的发展。Drupal的社区也非常活跃,里面包括各种用户,站点管理员,设计者,和web开发者,大家都在非常努力的工作着,并不断地改进着Drupal系统;可参看http://drupal.orghttp://groups.drupal.org

Drupal版本:

技术堆栈

老葛的Drupal培训班 http://www.thinkindrupal.com

Drupal的设计目标是,既可以运行在廉价的Web虚拟主机上,也可以适应高负载的分布式站点。前一个目标则意味着需要使用最流行的技术,而后者则意味着严谨的编码。Drupal的技术堆栈如图1-1所示。
 
 
图1-1 Drupal的技术堆栈
 
     操作系统位于技术堆栈的最底层,Drupal基本不用关心底层的操作系统。只要它支持PHP,就可以运行Drupal。
 
    Drupal最常用的web服务器是Apache,当然也可以使用其它的web服务器(包括微软的IIS)。由于Drupal和Apache的这种长期的友好关系,所以在Drupal的根目录下自带了一个.htaccess,用来确保Drupal安装的安全。可以使用Apache的mod_rewrite模块来实现简洁(Clean)URLs---将URL中的“?”,“&”以及其它奇怪的符号清除掉,在Drupal中去掉的是“?q=”。这一点特别重要,当从其它的内容管理系统或者静态文件中迁移到Drupal上时,依照Tim Berners-Lee(http://www.w3.org/Provider/Style/URI),内容的URL不需要改变,而不改变URI则有利于SEO。对于其它的web服务器,通过使用它的URL重写能力,也可以实现简洁URL。
 
    Drupal使用一个轻量级的数据库抽象层与堆栈的下一层次(数据库层)进行交互。这一抽象层能够处理SQL语句的安全清理;通过使用Drupal数据库API,你不须重构代码,便可以使用不同厂商的数据库。在Drupal中最常用的数据库是MySQL 和 PostgreSQL,不过对Microsoft SQL和Oracle的支持也在不断增加。
 

    Drupal使用的编程语言是PHP。因为PHP比较好学,所以大量的PHP代码都是由新手编写的。而新手的水平大家也知道,他们所写的代码总是存在这样或者那样的问题,这就给PHP的名声带来了比较坏的影响。然而, PHP也可以用于构建严谨的代码。Drupal核心中的所有代码都遵守了严格的编码规范(http://drupal.org/nodes/318),通过开源,其代码也经过了成千上万人的锤炼。对于Drupal来讲,PHP的入门门槛比较低,这就意味着有更多的人能够为Drupal贡献代码,通过开源,会有很多人对这些代码进行检查,这样就保证了代码的质量。通过向社区贡献代码,这样就可以收到他人的反馈,帮助,从而提高大家的技能。

Drupal版本:

核心

 老葛的Drupal培训班 http://www.thinkindrupal.com

Drupal的核心是由一个轻量级的框架构成的。当你从drupal.org下载Drupal时,得到的就是Drupal核心。它负责提供基本的功能,用以支持系统的其它部分。
     内核包括当Drupal接到请求时所要调用的系统引导指令的代码,一个Drupal常用函数库,和提供基本功能的模块比如用户管理、分类、和模板,如图1-2所示。
1-2 Drupal内核的概貌(没有展示完所有的核心功能)

 

Drupal版本:

后台管理界面

     老葛的Drupal培训班 http://www.thinkindrupal.com

    Drupal的后台管理界面与站点的前台部分紧密的集成在了一起,并且在默认情况下,使用相同的主题。第一个用户,也就是用户1,是一个超级用户,他对站点拥有全部权限。以用户1的身份登录后,你将在你的用户区块(参看“区块”部分)中看到管理站点的一个链接。点击这一链接,你将进入到Drupal的后台管理界面。根据用户对站点访问权限的不同,每个用户的区块都会有一个不同的链接。

Drupal版本:

模块

老葛的Drupal培训班 http://www.thinkindrupal.com

Drupal是一个完全模块化的框架。模块中包含了各种功能,而模块可被启用或者禁用(一些必须的模块不能被禁用)。有3种方式可以用来向Drupal站点添加特性:启用已有模块,安装Drupal第3方模块,编写自己的模块。这样,就可以根据站点的需要来添加相应的模块,需要的功能少,则所需的模块也就少;需要功能多,则所需的模块也就多。如图1-3所示。
1-3 通过启用其它模块来添加更多的功能
    新增的内容类型比如处方、日志、或者文件,新增的行为比如e-mail通知、P2P发布、和聚合,等等都是通过模块来实现的。Drupal使用了反转控制(IOC)设计模式,这样框架就会适时的调用相应的模块化功能了。这些为了模块完成它们的任务所提供的机会,被称为钩子

Drupal版本:

钩子

老葛的Drupal培训班 http://www.thinkindrupal.com

可以把钩子看做Drupal的内部事件。有时也将其称为回调函数,这是由于它们是根据函数命名约定来构建的,而不是注册一个事件监听器(listener),它们也不是真的被回调。模块通过使用钩子,就可以与Drupal的其它部分整合在一起了。
 
    假定有一个用户登录了你的Drupal站点。在用户登录时,Drupal调用用户钩子。这意味将调用所有的根据约定——“模块名”+“钩子名”——创建的函数。例如,评论模块中的comment_user(),本地化模块中的locale_user(),节点模块中的node_user(),还有任何其它具有类似名称的函数也都将被调用。如果你编写了一个名为spammy.module的定制模块,其中包含一个名为spammy_user()的函数,用来向用户发送电子邮件,那么你的这个函数也将被调用,倒霉的用户每次登录都将收到一封不请自来的电子邮件。
 
Drupal核心功能进行交互的最常用的方式,就是在模块中实现钩子。
 
提示 更多Drupal支持的钩子的信息,可参看在线文档http://api.drupal.org/api/6,查看“Drupal的组成部分”,接着“模块系统(Drupal钩子)”。
 

Drupal版本:

主题

老葛的Drupal培训班 http://www.thinkindrupal.com

当创建一个web页面,以发送给浏览器时,实际上主要考虑两个方面:把所需的数据收集起来,为这些数据添加HTML标签。在Drupal中,主题层用来负责创建HTML(或者JSON,XML等等),以传递给浏览器。Drupal可以使用多种流行的模板方式,比如Smarty,PHP模板属性语言 (PHPTAL),和PHPTemplate。
 
     这里需要记住的要点是,Drupal提倡将内容和标记区别开来。
 
    在Drupal中可以使用多种方式来为你的网站定制外观。最简单的方式是使用CSS来覆盖Drupal内置的类和ID。然而,如果你不想局限于此,并且想定制实际的HTML输出时,你会发现很容易就可以实现你的目标。Drupal的模板文件由标准的HTML和PHP组成。另外,Drupal页面的每个动态部分(比如盒子、列表、或者面包屑),都可以通过声明一个具有合适名字的函数进行覆写。接着,Drupal将使用你的函数来创建页面的该部分。

Drupal版本:

节点

老葛的Drupal培训班 http://www.thinkindrupal.com

    Drupal中的内容类型,都根源于一个唯一的基本类型,在这里称之为节点。无论它是一篇日志、一个处方,甚至一个工程任务,它的底层数据结构都是相同的。采用这种方式的优势在于它的扩展性。模块开发者可以为节点添加各种特性,比如评分、评论、文件附件、地理位置信息等等,而不用担心节点的具体类型,无管它是日志、处方还是其它。站点管理员可以根据内容类型混合和匹配功能;例如在日志而不是在处方上启用评论,或者仅为工程任务启用文件上传功能。
 
    节点还包含了一个基本的行为属性集,而所有其它的内容类型都继承了这一属性集。任何节点都可以被推到首页、发布或者未发布,或者甚至被搜索。正是由于这个统一的结构,后台管理界面才能够为节点提供了一个批量编辑页面。

Drupal版本:

区块

老葛的Drupal培训班 http://www.thinkindrupal.com

    区块是在你网站模板的特定位置上,可以启用或者禁用的信息。例如,一个区块可以用来显示你站点当前用户的在线人数。你可以区块中包含一些链接,用来指向站点的热门内容,或者包含一些即将到来的事件列表。区块一般放置在模板中的边栏、页首、或者页尾中。区块也可以用来显示特定类型的节点,一般仅用于首页,或者根据其它标准才这样实现。
 
    区块常常用于为当前用户显示定制的信息。例如,用户区块中仅包含了当前用户有权访问的后台管理界面的链接,比如“我的账号”。区块放到区域(比如页首,页脚,或者左右边栏等等)中,而区域则定义在站点的主题中。可以通过后台管理接口页面,对这些区域中区块的位置和可视性进行管理。

Drupal版本:

文件布局

老葛的Drupal培训班 http://www.thinkindrupal.com

通过了解Drupal默认安装的目录结构,能够学会一些重要的最佳实践,比如下载的模块和主题的放置位置,如何拥有不同的Drupal安装轮廓。一个Drupal默认安装的目录结构如图1-4所示。
1-4 Drupal默认安装的目录结构
 
    文件夹目录中的每一元素的详解如下:
 
includes :包含了Drupal常用的函数库。
 
misc:用来存储Drupal安装中可用的JavaScript,和其它各种图标和图片文件。
 
modules:包含了所有核心模块,其中一个模块对应一个文件夹。最好不要乱动这个文件夹(包括profiles和sites以外的其它目录)下面的任何东西,你要添加的其它模块须放到sites目录下。
 
profiles:包含一个站点的不同安装轮廓。如果在这个子目录下面,除了默认的轮廓以外,还有其它的轮廓,那么在你第一次安装你的Drupal站点时,Drupal将向你询问想要安装哪一个轮廓。安装轮廓的主要目的是,用来自动的启用核心的或者第3方的模块。比如一个电子商务轮廓,它将自动把Drupal安装成为一个电子商务平台。
 
scripts:包含了许多脚本,这些脚本可用于语法检查,代码清洁,从命令行运行Drupal,使用cron处理特定情况等等。在Drupal的请求生命周期中,用不到它;里面包含一些shell和Perl的实用脚本。
 
sites:包含了你对Drupal所进行的修改,包括设置、模块、主题等形式(参看图1-5)。你从第3方模块库中下载的模块,或者你自己编写的模块,都放在sites/all/modules下面。这使得你对Drupal所进行的任何修改都保存在单个文件夹里。在目录sites下面有一个名为default的子目录,里面包含了你的Drupal站点的默认的设置文件--- default.settings.php。Drupal安装器,将会基于你提供的信息来修改这些原始设置,并为你的站点创建一个settings.php文件。站点的部署人员,通常会拷贝默认目录,并将其重命名为你站点的URL,所以你最终的设置文件就位于sites/www.example.com/settings.php.
 
sites/default/files:Drupal默认是不包含这个文件夹,但是当你需要上传文件接着提供对外访问时,就需要用到这个目录了。一些示例包括,定制的logo,启用用户头像,或者向你的站点上传其它媒体文件时,你就用到了这个文件夹。运行Drupal的web服务器需要具有对这个子目录进行读和写的权限。如果可以的话,Drupal的安装器将会为你自动的创建这个子目录,并检查是否设置了相应的权限。
 
themes:包含了Drupal的模板引擎和默认主题。你下载的或者创建的其它主题,不能放在这里;应该放在sites/all/ themes中。
 
cron.php:用来执行周期性任务,比如清理过期缓存数据,以及计算统计信息。
 
index.php:处理请求的主入口。
 
install.php: Drupal安装器的主入口。
 
update.php: Drupal版本升级后,用来更新数据库模式(schema)。
 
xmlrpc.php: 用来接收XML-RPC请求,如果你的网站不打算接收XML-RPC请求的话,那么可以将其从中删除。
 
robots.txt:它是搜索引擎爬虫排除标准的默认实现。
 
    在这里没有列出的其它文件都是文档文件。
1-5 sites文件夹用来存储你Drupal所做的修改

Drupal版本:

服务一个请求

老葛的Drupal培训班 http://www.thinkindrupal.com

Drupal收到一个请求时都发生了什么呢?如果对此能有一个概括性的了解,那么对以后的学习将会很有帮助,所以本部分将对这一框架提供一个简要的介绍。如果你自己也想追踪一下的话,使用一个好的调试器,从index.php开始就可以了,Drupal的大多数请求都从这里开始。对于一个简单web页面,这里所列的序列看起来有些复杂,但这也是灵活性所必需的。
Web服务器的角色
     Drupal运行在web服务器上,通常是Apache上。如果web服务器识别Drupal的.htaccess文件,那么将初始化一些PHP设置,并启用简洁URL。基本上对Drupal的所有调用都是从index.php开始的。例如,调用http://example.com/foo/bar,将会经历以下步骤:
1.首先,基于Drupal的.htaccess文件中的mod_rewrite规则,对输入URL进行检查,并将基路径从路径中分离出去。在我们的例子中,路经就为foo/bar。
2.将这个路径指定给URL查询参数q。
4.Drupal把foo/bar作为Drupal内部路径,并在index.php中开始进行处理。
 
    这一流程的结果就是,Drupal对http://example.com/foo/barhttp://example.com/index.php?q=foo/bar的处理方式是一样的,因为对于Drupal来说,这两者的内部路径是一样的。这就使得Drupal可以使用不带“?q=”的URL了。这些URL被称为简洁URL。
 
    在备选的web服务器中,比如微软的IIS,可以使用一个ISAPI模块比如ISAPI_Rewrite来实现简洁URL。IIS 7及以后的版本可能会内置对重写的支持。

Drupal版本:

引导指令流程

老葛的Drupal培训班 http://www.thinkindrupal.com

引导指令流程
    对于每个请求,Drupal引导指令本身都会经历一系列的引导指令阶段。这些阶段定义在bootstrap.inc中,在接下来的部分中,我们将会为你描述处理流程。
 
初始配置
     在这一阶段,将会填充Drupal的内部配置数组,并建立站点的基URL($base_url)。通过include_once()来解析settings.php文件,任何已被覆写的变量或者字符串都可被应用了。详情请参看sites/all/default/default.settings.php文件中的“变量覆写”和“字符串覆写”部分。
 
前期页面缓存
    在有些情况下,可能会需要更高水平的性能,甚至在建立数据库连接之前,可能就需要要调用缓存系统了。前期页面缓存阶段,会让你(使用include())包含一个PHP文件,里面带有一个名为page_cache_fastpath()的函数,该函数接收内容并将其返回给浏览器。通过将page_cache_fastpath变量设置为TRUE,就可以启用早期页面缓存阶段了,而包含进来的文件则通过将cache_inc变量设置为文件的路径来定义。具体示例可参看缓存一章。
 
初始化数据库
    在数据库阶段期间,将决定数据库的类型,将建立初始化链接以供数据库查询使用。
 
基于主机名/ID地址进行访问控制
    Drupal支持基于主机名/ IP地址来禁止主机(对站点的访问)。在访问控制阶段,会快速的检查请求是否来自一个被禁的主机;如果是,那么将拒绝访问。
 
初始化会话处理
    Drupal利用了PHP内置的会话处理,但是它使用自己的基于数据库的会话处理器,覆写了PHP的一些处理器。在会话阶段,将初始化或者重新构建会话。代表当前用户的全局对象$User也会在这一阶段初始化,不过出去效率的原因,并不是对象的所有属性都是可用的(当需要时,可以通过明确的调用函数user_load()来加载这些属性)。
 
后期页面缓存
    在后期页面缓存阶段,Drupal会加载足够的支持代码,来决定是否需要从页面缓存中提供一个页面。这包括,把来自于数据库的设置合并到在初始化配置阶段创建的数组中,并且加载或者解析模块代码。如果在会话中显示请求来自于匿名用户,并启用了页面缓存,那么将从缓存中返回页面,执行将在此停止。
 
语言判定
    在语言判定阶段,将会初始化Drupal的多语言支持,并基于站点和用户的设置,来决定为当前页面使用哪一个语言。Drupal支持多种方式来判定语言,比如路径前缀和域名层的语言判断。
 
路径

     在路径阶段,将加载处理路径和路径别名的代码。该阶段使得用户可读的URL被转化为Drupal路径,并处理Drupal内部路径的缓存和查找操作。

Drupal版本:

完成

老葛的Drupal培训班 http://www.thinkindrupal.com

完成
    该阶段是引导指令的最后一个阶段,它包括加载一个通用函数库,主题支持,和支持回调映射,文件处理,Unicode,PHP图片工具集,表单的创建和处理,自动排序的表格,和结果集的分页。在这里将设置Drupal定制的错误处理器,并加载所有启用了的模块。最后Drupal调用init钩子,这样在对请求正式开始处理以前,将有机会通知相应的模块。
 
    一旦Drupal整个引导指令完成以后,那么框架中的所有部分现在都可以使用了。现在就可以获得浏览器的请求并将它委托给一个处理函数。在URLs和处理函数之间的映射,是使用一个回调登记来完成的,这个回调登记负责URL映射和访问控制。模块使用菜单钩子来注册它们的回调函数(更多信息,参看第4章)
 
    对于浏览器请求的URL,如果Drupal为其找到一个存在的映射回调函数,并且用户有权访问此回调函数,那么控制权将转移给回调函数。
 
处理一个请求
    回调函数完成了流程所要做的事情,并收集完成请求所需要的数据。例如,收到了一个对内容的请求比如http://example.com/q=node/3,URL将被映射到node.module里面的函数node_page_view()上。进一步的处理包括,将从数据库中取回该节点的数据,并将它放到一个数组中。接着,就到了主题化的时候了。
 
主题化数据
    主题化涉及到将已被取回,操作过或者创建了的数据转化为HTML(或者XML以及其它输出格式)。Drupal将使用管理员选用的主题,来为网页提供一个合适的外观,并将生成的HTML返回给web浏览器。

Drupal版本:

总结

老葛的Drupal培训班 http://www.thinkindrupal.com

总结
    读完本章后,你应该能大致的了解Drupal的工作原理,并对当Drupal为一个请求服务时都发生了什么有个概念。对于该服务处理流程的各个部分,我们将会在后面章节中作出详细介绍。

Drupal版本:

第2章 创建一个drupal模块(Module)

老葛的Drupal培训班  http://zhupou.cn

在许多开源的应用中,你可以通过修改源代码来定制应用。这是一种方法, 用来获得你想要的功能;但是在drupal中,一般不赞成这样做,只有在万不得已的情况下才使用这一手段。修改源代码,意味着随着Drupal的每次更新,你必须要做更多的工作----你必须测试一下你的修改是否还能正常工。代替的,Drupal的设计从底层便考虑了模块化和扩展性。

     Drupal是个非常精简的框架,用于构建各种应用,其默认安装通常被称作为Drupal核心。如果你需要向Drupal核心添加功能的话,那么可以通过启用模块来实现,而模块则是一些包含PHP代码的文件。核心模块放置在你的Drupal安装的子目录modules下面。现在看一下这个子目录,然后导航到“管理➤站点构建 ➤模块”,比较一下子目录下的模块与管理界面上模块列表中的模块。

      在本章,我们将从头开始创建一个模块;在你创建模块时,你将学到模块必须遵守的一些标准。我们需要一个现实的目标,所以让我们考虑以下现实中的注释问题。当用户在Drupal网站上浏览内容时,如果管理员启用了评论模块,那么用户可能会对内容发表评论。但是如果是在一个网页上添加一个注释(一种仅有用户可见的笔记类型),那该怎么样?这对私密的内容评审可能非常有用(我知道这看起来可能有点做作,但是大家还是容忍一下吧)。

Drupal版本:

创建相应的文件

老葛的Drupal培训班 Think in Drupal

首先我们要做的是为模块起一个名字。名字“annotate”看起来还是比较合适的—简洁而生动。接着,我们需要找个地方来放置这个模块。我们可以把这个模块放在核心模块所在的目录中去,不过这样的话,我们需要记住哪些是核心模块,哪些是我们的模块,这样一来,就增加了维护成本。让我们把它放在目录sites/all/modules下面,以将其与核心模块区分开来。

    如果sites/all/modules不存在,那么首先需要创建它。接着在sites/all/modules下面在创建一个名为custom的目录,然后在sites/all/modules/custom下面创建一个名为annotate的目录。这样就可以将你开发的自定义模块与你下载的第3方模块区分开了。如果有一天,你需要将你的网站委托给另一个开发者,那么这一点还是很有帮助的,不过是否将它们区分开来,取决于你的决定。我们创建的是一个子目录,而不是一个annotate.module文件,这是因为在我们的模块中,除了模块文件以外,我们还需要一些其它的文件。比如我们需要一个README.txt文件,用来向其他用户解释我们的模块是做什么的,以及如何使用它,还有一个annotate.info文件用来向Drupal提供一些关于我们模块的信息。准备好了吗?现在让我们正式开始。
 我们的annotate.info文件内容如下:
 
; $Id$
name = Annotate
description = Allows users to annotate nodes.
core = 6.x
package = Pro Drupal Development
 
 
这个文件的格式非常简单,在这里一个键对应一个值。我们从版本管理系统(CVS)的标识标签开始。如果我们想和其他用户分享这一模块,通过将它提交到Drupal的第3方模块资源库中,这个值将会被CVS自动替换。接着,我们为Drupal提供了一个名称和一个描述,用来显示在网站的模块管理部分。我们明确的定义了我们的模块所兼容的Drupal主版本;在这里,就是版本6.x。Drupal 6以及以后的版本将不允许启用不兼容的模块。模块是按组来显示的,而组的划分是由包(package)决定的;这样,如果我们有3个不同的模块,它们都有package=Pro Drupal Development,那么它们将被放在同一组中。除了前面所列的这些,我们还可以指定一些可选的值。我们再看一个例子,下面的这个模块,它需要PHP5.2,依赖于论坛和分类模块:
; $Id$
name = Forum confusion
description = Randomly reassigns replies to different discussion threads.
core = 6.x
dependencies[] = forum
dependencies[] = taxonomy
package = "Evil Bob's Forum BonusPak"
php = 5.2
 
注意 你可能会想,为什么我们需要一个单独的.info文件呢?为什么不在我们的主模块中写一个函数来返回这些元数据呢?这是因为在加载模块管理页面时,它将不得不加载并解析每一个模块,不管有没有启用,这比平时需要更多的内存并且可能超出分配给PHP的内存上限。
通过使用.info文件,可以更快的加载信息并使用最小的内存。
 
       现在我们准备好创建一个实际的模块了。在你的sites/all/modules/custom/annotate子目录下面创建一个名为annotate.module的文件。在文件的开始出使用PHP的开始标签和一个CVS标识标签,并紧跟一个注释:
<?php
// $Id$
/**
 * @file
 * Lets users add private annotations to nodes.
 *
 * Adds a text field when a node is displayed
 * so that authenticated users may make notes.
 */
 
    首先,让我们看一下注释的风格。我们从/**开始,在接下来的每一行中缩进一格并以*开头,最后以*/结束。令牌@file意味着在接下来的一行是一个描述,给出这个文件的用途。模块api.module,Drupal的自动化文档提取器和格式器,可以使用这一行描述来找出这个文件的用途。空了一行以后,我们为可能检查(并且改进)我们代码的程序员提供了一个更长的描述。注意,我们在这里有意的不使用结束标签 ?>;这对于PHP来说是可选的,如果包含了它,就可能导致文件的尾部空格问题(参看http://drupal.org/node/545)。
 
注意 为什么我们在这里这么详细的讲述每一个细节?这是因为,如果来自世界各地的成百上千的人开发同一个项目的话,如果大家采用一种标准的方式,将会节省大量的时间。关于Drupal的代码风格的更详细的内容可以从Drupal开发用户手册的“代码标准”一节中找到(http://drupal.org/node/318)。
 
    下面我们要做的就是定义一些设置,这样我们就可以使用一个基于web的表单来选择哪些节点类型可以添加注释。这需要两步。首先我们定义一个路径,用来访问我们的设置。然后我们创建设置表单。

Drupal版本:

实现一个钩子

老葛的Drupal培训班 Think in Drupal

回想一下,我们曾经说过Drupal是建立在钩子系统之上,有时候钩子也被称为回调。在执行的过程中,Drupal询问模块看它们是不是想要做些事情。举例来说,为了判定哪一个模块负责当前的请求,它向所有的模块询问是否提供了相应的路径。通过创建一个所有模块的列表,并且调用每个模块中名为:模块名+_menu的函数,来实现这一点。当它遇到我们的annotate模块时(应该会比较早的遇到,因为模块列表默认是按照字母顺序排列的),它调用函数annotate_menu(),后者返回一个包含菜单项的数组。每一项(我们这里只有一项)都以路径为键,在这里就是admin/settings/annotate。菜单项的值是一个数组,里面包含的键和值是用来描述在该路径被请求时Drupal要做什么的。这方面的更多详细,可参看第4章
,该章讲述了Drupal的菜单/回调系统。下面给我们模块添加点内容:
 
/**
* Implementation of hook_menu().
*/
function annotate_menu() {
    $items['admin/settings/annotate'] = array(
        'title' => 'Annotation settings',
        'description' => 'Change how annotations behave.',
        'page callback' => 'drupal_get_form',
        'page arguments' => array('annotate_admin_settings'),
        'access arguments' => array('administer site configuration'),
        'type' => MENU_NORMAL_ITEM,
        'file' => 'annotate.admin.inc',
    );
 
    return $items;
}
    此时不要过于关注这里的具体细节。这段代码说,“当用户访问页面http://example.com/?q=admin/settings/annotate时,调用函数drupal_get_form,并向它传递了一个表单ID annotate_admin_settings,在文件annotate.admin.inc中查找描述该表单的函数。只有具有管理站点配置权限的用户才有权查看这个菜单。”当需要显示表单时,Drupal就会让我们提供一个表单定义(一会儿就对这一点详细讲解)。当Drupal完成了向所有的模块询问它们的菜单项时,它就为正被请求的路径找到一个菜单,根据这个菜单就会找到一个要被调用的函数。
 
注意 如果你对钩子机制感兴趣的话,参看文件includes/module.inc里面的函数module_invoke_all()。
 
    现在你应该清楚我们为什么把它叫作hook_menu()或者菜单钩子了。可以通过在钩子的名字前加上你的模块名来创建Drupal钩子。
 
提示 Drupal的钩子几乎允许你修改这个软件的任何方面。你可以在Drupal的API文档站点(http://api.drupal.org)上,找到Drupal钩子的完整列表和它们的使用说明。
 

Drupal版本:

添加特定于模块的设置(1)

     Drupal有多种不同的节点类型(在用户界面称之为内容类型),比如Story和Page。我们想将注释的使用限定在特定的一些节点类型上。为了实现这一点,我们需要创建一个页面,在里面告诉我们的模块我们想注释哪些节点类型。在该页面,我们将呈现一组复选框,每一个复选框就对应一个已有的内容类型。这样终端用户就可以通过选中或者取消选中复选框(如图2-1所示),就可以决定哪些内容类型可被注释。这样的页面就是一个管理页面,只有在需要的时候才加载和解析合成该页面的代码。因此,我们把代码放在了一个单独的文件中,而不是放在我们的annotate.module文件里,而对于每个web请求,都会加载和运行annotate.module文件。由于我们告诉了Drupal,在文件annotate.admin.inc中查找我们的设置表单,所以创建文件sites/all/modules/custom/annotate/annotate.admin.inc,并向里面添加以下代码:

 
<?php
// $Id$
 
/**
 * @file
 * Administration page callbacks for the annotate module.
 */
 
/**
 * Form builder. Configure annotations.
 *
 * @ingroup forms
 * @see system_settings_form().
 */
function annotate_admin_settings() {
    // Get an array of node types with internal names as keys and
    // "friendly names" as values. E.g.,
    // array('page' => 'Page', 'story' => 'Story')
    $options = node_get_types('names');
 
    $form['annotate_node_types'] = array(
        '#type' => 'checkboxes',
        '#title' => t('Users may annotate these content types'),
        '#options' => $options,
        '#default_value' => variable_get('annotate_node_types', array('page')),
        '#description' => t('A text field will be available on these content types          to make user-specific notes.'),
    );
 
    return system_settings_form($form);
}
 
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

添加特定于模块的设置(2)

   在Drupal中,表单被表示为一个嵌套的树状结构;也就是说,一个数组的数组。这个结构向Drupal的表单呈现引擎(rendering engine)描述了表单是如何表示的。为了可读性,我们将数组中的每个元素单独成行。每一个表单属性都以”#”开头,并作为数组的键。我们首先声明了表单元素的类型为checkboxes,这意味着通过使用一个带键的数组来构建多个复选框。我们在变量$options中已经得到了带键的数组。

 
    我们将选项(options)赋值为node_get_types('names'),该函数方便的返回了一个键值数组,里面包含了当前Drupal中可用的节点类型。它的输出看起来像这个样子:
'page' => 'Page', 'story' => 'Story'
 
数组的键就是节点类型在Drupal中的内部名字,而把可读性的名字(显示给用户的)放到了右边。如果你的Drupal中有一个名为“Savory Recipe”的节点类型,那么数组看起来应该这样:
'page' => 'Page', 'savory_recipe' => 'Savory Recipe', 'story' => 'Story'
 
 因此,在我们的web表单中,为节点类型page和story生成了相应的复选框。
 
    我们通过定义属性#title的值,为表单元素设置了一个标题。
 
注意 显示给用户的任何文本(比如我们表单字段的#title和#description属性),都放在了t()函数中,这个函数在Drupal中是用来翻译字符串的。通过把所有文本经过一个字符串翻译函数的处理,那么将你的模块本地化为一个不同的语言将会非常简单。我们没有在菜单项中使用该函数,这是因为菜单项会被自动翻译。
 
    下一个指示,#default_value,将是这个表单元素的默认值。由于checkboxes是一个多值的表单元素(也就是说,存在多于一个的复选框),所以#default_value的值将会是一个数组。
 
老葛的Drupal培训班 Think in Drupal

Drupal版本:

添加特定于模块的设置(3)

老葛的Drupal培训班 Think in Drupal

这里值得讨论一下#default_value的值:

variable_get('annotate_nodetypes', array('page'))
 
  Drupal允许程序员使用特定的一对函数:varialble_get()和varialble_set()来存储和取回任意值。值将被存储到数据库表variables中,并且在处理一个请求的任意时候都是可用的。由于在处理每个请求时都会取回这些值,所以这种方法不能用来存储大量的数据。对于配置属性这样简单数值的存储,它却是一个非常方便的系统。注意我们传递给varialble_get()的是一个描述我们的值的键(所以我们可以取回它),和一个默认值。在这种情况下,默认值是一个数组,里面包含了允许注释的节点类型。在默认情况下,我们允许对节点类型page进行注释。
 
提示 当使用system_settings_form()时,表单元素(在这里就是annotate_node_types)的名字必须匹配variable_get()中所用的键。
 
    最后我们提供一个描述,用来告诉站点管理员关于这个字段的一些更细节的信息。
   
    保存你刚创建的文件,然后导航到“管理➤站点构建 ➤模块”。在标题为pro Drupal Development的组中,在模块列表的最后,你应该能够看到你的模块了(如果没有的话,那么仔细的检查你的annotate.info和annotate.module文件;并确保它们位于sites/all/modules/custom目录中)。继续前进,启用你的新模块。
 
    现在导航到“管理➤设置 ➤注释”,我们将看到annotate.module所显示的配置表单了(如图2-1所示)。
  
2-1annotate.module生成的配置表单。
 

  仅用了几行代码,我们就为我们的模块提供了一个可用的配置表单,它将自动的保存和记住我们的设置!好的,尽管代码中的一行有点太长了,但是没有关系,你现在应该能够感受到撬动Drupal的力量了。

Drupal版本:

添加数据输入表单(1)

为了让用户可以为一个web页面输入笔记,我们需要为它提供一个地方,专门用来输入笔记。下面我们在annotate.module中为笔记添加一个表单:

    (译者注:这里的笔记就是注释的意思)
 
/**
* Implementation of hook_nodeapi().
*/
function annotate_nodeapi(&$node, $op, $teaser, $page) {
    global $user;
    switch ($op) {
        // The 'view' operation means the node is about to be displayed.
        case 'view':
            // Abort if the user is an anonymous user (not logged in) or
            // if the node is not being displayed on a page by itself
            // (for example, it could be in a node listing or search result).
            if ($user->uid == 0 || !$page) {
                break;
            }
            // Find out which node types we should annotate.
            $types_to_annotate = variable_get('annotate_nodetypes', array('page'));
            // Abort if this node is not one of the types we should annotate.
            if (!in_array($node->type, $types_to_annotate)) {
                break;
            }
 
            // Add our form as a content item.
            $node->content['annotation_form'] = array(
                '#value' => drupal_get_form('annotate_entry_form', $node),
                '#weight' => 10
            );
            break;
    }
}

老葛的Drupal培训班 Think in Drupal

Drupal版本:

添加数据输入表单(2)

   这个看起来有点复杂,所以让我们详细的分析一下。首先要注意的是,我们在这里实现了Drupal的另一个钩子。这次是nodeapi钩子,在drupal对节点进行各种处理时将会调用该钩子,这样其它的模块(比如我们的)在处理继续往下以前可以修改节点。我们通过变量$node将节点传递过来。注意第一个参数前面的&,这意味着它实际上是对$node对象的一个引用,这点非常好,因为我们在这里对$node所做的任何修改都将被保存下来。由于我们的目标是追加一个表单,所以我们非常高兴地看到我们可以修改节点。

 
     我们仍然需要一些信息----在我们的代码被调用时在Drupal中将发生什么。这些信息保存在了参数$op中,它可以是insert(节点正被创建),delete(节点正被删除),或者一个其它的值。当前,我们只有当节点正准备显示出来时,才想对其进行修改。在这种情况下,变量$op的值就是view。我们在这里使用了switch控制语句,这样我们就可以非常容易的添加其它情况,并且能够方便的看到在每种情况下我们的模块将做什么。
 
     接下来,我们快速的检查了一些我们不想显示注释字段的情况。一种情况是$user对象的用户ID为0时,这意味着查看节点的用户此时没有登录(注意,在这里我们使用关键字global 将$user 对象包含了进来,这样我们就可以测试当前用户是否登录了)。另一种情况是当参数$page不为TRUE时,我们想阻止表单的显示。如果它为FALSE,这意味着,这个节点并不是单独显示的,而是显示在一个列表中,比如说一个搜索引擎的结果中,或者一个最近更新的节点列表中。在这些情况下,我们不需要添加任何东西。我们使用break语句来跳出switch语句从而阻止对页面的修改。
 
 在我们为web页面添加注释表单以前,我们需要检查一下,将要进行显示的节点的类型是不是我们在设置页面所启用的类型中的一个,所以我们取回了在我们实现设置钩子时所保存的节点类型数组。我们将它保存到了变量$types_to_annotate中去。对于variable_get()中的第2个参数,我们在这里声明了一个默认数组,用于站点管理员还没有访问我们模块的设置页面来输入设置的情况。下面要做的就是检查一下,我们所要处理的节点的类型是不是包含在$types_to_annotate中。同样,如果节点类型不是我们想要注释的,我们将使用break语句来跳出switch语句。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

添加数据输入表单(3)

   我们最后要做的就是创建表单,并把它添加到$node对象中。首先,我们需要定义一个表单,这样我们就有了要添加的东西。我们将在annotate.module中的一个单独的函数中完成这件事,它唯一的责任就是定义表单:

/**
* Define the form for entering an annotation.
*/
function annotate_entry_form($form_state, $node) {
    // Define a fieldset.
    $form['annotate'] = array(
        '#type' => 'fieldset',
        '#title' => t('Annotations'),
    );
 
    // Define a textarea inside the fieldset.
    $form['annotate']['note'] = array(
        '#type' => 'textarea',
        '#title' => t('Notes'),
        '#default_value' => isset($node->annotation) ? $node->annotation : '',
        '#description' => t('Make your personal annotations about this content              here. Only you (and the site administrator) will be able to see them.')
    );
 
    // For convenience, save the node ID.
    $form['annotate']['nid'] = array(
        '#type' => 'value',
        '#value' => $node->nid,
    );
 
    // Define a submit function.
    $form['annotate']['submit'] = array(
        '#type' => 'submit',
        '#value' => t('Update'),
    );
    return $form;
}
 
    这个函数有两个参数。第一个参数是$form_state,Drupal会将它自动的传递给所有的表单函数。我们现在先把它忽略掉;更多详细,可参看第10章,那里是专门讨论表单API的。第2个参数是$node对象,我们在前面的nodeapi钩子实现中,将它传递给了drupal_get_form()。
 
       我们创建表单的方式,和我们在函数annotate_admin_settings()中使用的一样,都是创建一个键值数组----只是这次我们想把文本输入框和提交按钮放到一个字段集中,这样在web页面中就能将它们组织到一块了。首先,我们创建一个数组,将它的#type设为‘fieldset’,并为它提供一个标题。然后我们创建一个描述文本域(textarea)的数组。注意,textarea数组的键是fieldset数组中的一员。换句话说,我们使用$form['annotate']['note']替换了$form['note']。这样,Drupal将把这个文本域元素当作字段集元素中的一员。最后,我们创建了提交按钮,然后返回了我们的表单数组。
 
  现在让我们回到annotate_nodeapi()上,通过向节点的内容添加一个值和一个重量,我们将表单添加到了页面的内容上。值包含了要显示的内容,重量告诉Drupal把它显示到哪里,这里的位置是相对于节点中其它的内容的。我们想把注释表单放到页面的下面,所以我们为它分配了一个相对较大的重量10.我们要显示的是我们的表单,所以我们调用函数drupal_get_form(),以将我们的表单从一个描述如何创建它的数组转化为最终的HTML表单。注意,在这里我们是如何将$node对象传递给表单函数的;我们需要使用它来得到以前的注释以预先填充表单。
在你的web浏览器中,创建并查看一个Page节点,你应该可以看到添加在节点后面的注释表单了(如图2-2所示):
     2-2出现在drupal web 页面上的注释表单
 

  当我们点击更新按钮时,将会发生什么呢?什么都没有,因为我们还没有为输入的表单内容编写任何逻辑代码呢。现在就让我们添加它。但是在我们继续以前,我们需要考虑一下,我们将把用户输入的数据存储到哪里呢?

老葛的Drupal培训班 Think in Drupal

Drupal版本:

把数据存储到数据库表中(1)

存储模块所用数据的最常用方式,就是为这个模块的数据创建一个单独的数据库表。这将使得该数据与drupal核心数据库表独立开来。当你决定为模块创建哪些字段时,你应该问问自己:需要存储什么数据呢?如果我要对这个表进行查询,那么我需要使用什么字段和索引?最后,还要考虑一下,我在将来对这个模块可能会作哪些扩展?

    我们需要存储的数据也就是:注释的文本,注释所用到的节点的数字ID,和编写注释的用户的用户ID。保存一个时间戳也会非常有用,这样我们可以根据时间戳,来显示一列最近更新的注释。最后,我们对这张表进行查询的主要问题是,“在节点上,该用户做了哪些注释?”我们将在uid和nid字段上创建一个联合索引,从而使我们最常用的查询跑得尽可能快。我们表的SQL语句如下所示:
CREATE TABLE annotate (
    uid int(10) NOT NULL,
    nid int(10) NOT NULL,
    note longtext NOT NULL,
    when int(11) NOT NULL default '0',
    PRIMARY KEY (uid, nid),
);
老葛的Drupal培训班 Think in Drupal

Drupal版本:

把数据存储到数据库表中(2)

老葛的Drupal培训班 Think in Drupal

我们可以把这段sql语句放到我们模块的README.txt文件中,这样我们就省事了,但是想要安装这个模块的其他用户就麻烦了,他们需要手工的将数据库表添加到他们的数据库中。换种方式,我们知道,在你启用你的模块时,Drupal能帮你创建相应的数据库表;我们这里将利用Drupal的这一点。我们将创建一个特殊的文件;文件的名字将使用你的模块名,而后缀则使用.install,所以对于annotate.module,这个文件名应该为annotate.install。创建文件sites/all/modules/custom/annotate/annotate.install,并输入以下代码:

 
<?php
// $Id$
 
/**
 * Implementation of hook_install().
 */
function annotate_install() {
    // Use schema API to create database table.
    drupal_install_schema('annotate');
}
 
/**
 * Implementation of hook_uninstall().
 */
function annotate_uninstall() {
    // Use schema API to delete database table.
    drupal_uninstall_schema('annotate');
    // Delete our module's variable from the variables table.
    variable_delete('annotate_node_types');
}
 
/**
 * Implementation of hook_schema().
 */
function annotate_schema() {
    $schema['annotations'] = array(
        'description' => t('Stores node annotations that users write.'),
        'fields' => array(
            'nid' => array(
                'type' => 'int',
                'unsigned' => TRUE,
                'not null' => TRUE,
                'default' => 0,
                'description' => t('The {node}.nid to which the annotation                      applies.'),
            ),
            'uid' => array(
                'type' => 'int',
                'unsigned' => TRUE,
                'not null' => TRUE,
                'default' => 0,
                'description' => t('The {user}.uid of the user who created the                      annotation.')
            ),
            'note' => array(
                'description' => t('The text of the annotation.'),
                'type' => 'text',
                'not null' => TRUE,
                'size' => 'big'
            ),
            'created' => array(
                'description' => t('A Unix timestamp indicating when the annotation
                    was created.'),
                'type' => 'int',
                'not null' => TRUE,
                'default' => 0
            ),
        ),
        'primary key' => array(
            'nid', 'uid'
        ),
    );
 
    return $schema;
}

Drupal版本:

把数据存储到数据库表中(3)

老葛的Drupal培训班 Think in Drupal

在第一次启用注释模块时,drupal会查找文件annotate.install并运行函数annotate_install(),它将读取我们在模式钩子中所描述的模式。我们描述了我们想让Drupal创建的数据库表及其字段,而Drupal将它们转化为了我们当前所用数据库的标准SQL。这方面的更多信息,可参看第5章。如果一切顺利的话,这样就创建了数据库表。让我们这就尝试一下。由于我们在前面还不带数据库表的时候,就启用了该模块,所以我们需要重新安装这个模块,它现在多了一个.install文件。需要按照以下步骤进行重装:
1. 导航到“管理➤站点构建 ➤模块”,先将这个模块禁用。
2.在管理界面“管理➤站点构建 ➤模块”上,找到卸载标签,然后将模块卸载掉。这样Drupal就会删除与这个模块有关的数据库表。
3. 启用该模块。这次,在模块被启用时,Drupal将创建相关的数据库表。
 
 
提示:如果在你的.install文件中不小心包含了一个错别字,或者由于其它原因导致执行失败,那么导航到“管理➤站点构建 ➤模块”来禁用你的模块,并使用卸载标签来卸载模块的数据库表,这样drupal就能完整地删除你的模块和它的数据库表了。如果前面的办法无效的话,那么还有最后的手段,那就是在数据库的system表中直接删除该模块的记录。
 

Drupal版本:

把数据存储到数据库表中(4)

Drupal创建了用来存储数据的annotations表以后,我们需要修改一下我们的代码。其一,我们将需要添加一些逻辑代码,这样在用户输入注释并且点击更新按钮以后,它可以用来负责对输入数据的处理工作。我们的表单提交函数如下所示:

 
/**
 * Handle submission of the annotation form and saving
 * of the data to the database.
 */
function annotate_entry_form_submit($form, $form_state) {
    global $user;
 
    $note = $form_state['values']['note'];
    $nid = $form_state['values']['nid'];
 
    db_query('DELETE FROM {annotations} WHERE nid = %d AND uid = %d',
        $nid, $user->uid);
    db_query("INSERT INTO {annotations} (nid, uid, note, created) VALUES
        (%d, %d, '%s', %d)", $nid, $user->uid, $note, time());
    drupal_set_message(t('Your annotation has been saved.'));
}
 
 由于我们在一个节点上只允许一个用户有一个注释,所以我们可以安全的删除以前的注释(如果有的话),然后把我们自己的插入到数据库中。对于我们与数据库的交互,需要注意以下几点。首先,我们不需要考虑数据库连接,这是因为Drupal在它的引导指令中已经为我们完成了这一工作。第二,在我们使用一个数据库表时,我们需要把它放到花括号里{}.这样就可以无缝的实现数据库表的前缀化(关于表前缀化的更多详细,可参看文件sites/default/settings.php中的注释)。第三,我们在查询语句中使用了占位符,并为其提供了相应的变量,这样Drupal内置的查询安全清理机制就可以帮助我们阻止SQL注入攻击。占位符%d用于数字,而占位符%s用于字符串。最后,我们使用drupal_set_message()来将一条消息隐藏在用户的会话中,在用户查看的下一个页面时,它就会被Drupal作为一个通知显示给用户。这样,用户就获得一些反馈信息。
 
老葛的Drupal培训班 Think in Drupal
 

Drupal版本:

把数据存储到数据库表中(5)

最后,我们需要修改nodeapi钩子中代码,这样,如果已经存在了一个注释,那么它将被从数据库中取出,并用来预先填充我们的表单。在我们把我们的表单分配给$node->content的代码前面,我们添加以下代码,这里用粗体将其标出了:

 
/**
* Implementation of hook_nodeapi().
*/
function annotate_nodeapi(&$node, $op, $teaser, $page) {
    global $user;
    switch ($op) {
        // The 'view' operation means the node is about to be displayed.
        case 'view':
            // Abort if the user is an anonymous user (not logged in) or
            // if only the node summary (teaser) is being displayed.
            if ($user->uid == 0 || !$page) {
                break;
            }
            // Find out which node types we should annotate.
            $types_to_annotate = variable_get('annotate_node_types', array('page'));
 
            // Abort if this node is not one of the types we should annotate.
            if (!in_array($node->type, $types_to_annotate)) {
                break;
            }
 
           // Get the current annotation for this node from the database
           // and store it in the node object.
           $result = db_query('SELECT note FROM {annotations} WHERE nid = %d
               AND uid = %d', $node->nid, $user->uid);
           $node->annotation = db_result($result);
 
            // Add our form as a content item.
            $node->content['annotation_form'] = array(
                '#value' => drupal_get_form('annotate_entry_form', $node),
                '#weight' => 10
            );
            break;
 
        case 'delete':
            db_query('DELETE FROM {annotations} WHERE nid = %d', $node->nid);
            break;
    }
}
 
 我们首先查询数据库以取回这个用户对这个节点所做的注释。接着,我们使用了db_result(),这个函数是用来从结果集中取出第一条记录的第一个字段。由于我们仅允许一个用户对同一节点只能做一个注释,所以结果集中也只有一行记录。
 
    我们还在nodeapi钩子中的delete操作下添加了逻辑代码,这样当一个节点被删除时,该节点上的所有注释也都将被删除。
 

  测试一下你的模块。它现在应该能够保存和取回注释了。现在可以喘口气了---你已经从头创建了一个模块。在通往Drupal专业开发者的道路上,你已经迈出了关键的一步。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

定义你自己的管理部分

       Drupal有多个管理设置的类别,比如内容管理和用户管理,都出现在主管理页面上。如果你的模块需要一个自己的类别,那么你可以非常容易的创建一个。在这个例子中,我们创建一个名为“Node annotation”的新类别。为了实现这一点,我们修改我们的菜单钩子以定义新类别:

 
/**
 * Implementation of hook_menu().
 */
function annotate_menu() {
       $items['admin/annotate'] = array(
              'title' => 'Node annotation',
              'description' => 'Adjust node annotation options.',
              'position' => 'right',
              'weight' => -5,
              'page callback' => 'system_admin_menu_block_page',
              'access arguments' => array('administer site configuration'),
              'file' => 'system.admin.inc',
              'file path' => drupal_get_path('module', 'system'),
       );
       $items['admin/annotate/settings'] = array(
              'title' => 'Annotation settings',
              'description' => 'Change how annotations behave.',
              'page callback' => 'drupal_get_form',
              'page arguments' => array('annotate_admin_settings'),
              'access arguments' => array('administer site configuration'),
              'type' => MENU_NORMAL_ITEM,
              'file' => 'annotate.admin.inc',
       );
 
       return $items;
}
 
      现在我们代码生成的结果改变了,多了一个新类别,而我们模块的设置链接也包含在了里面,如图2-3所示:
2-3 指向注释模块设置的链接现在作为一个单独的类别出现了
 
     如果你是一步一步跟着做的,那么你需要清除菜单缓存来查看链接的显示。
有多种方式:可以直接清空cache_menu表,或者使用Drupal的开发模块(devel.module)所提供的“重构菜单”链接,或者导航到“管理➤站点配置 ➤性能”并点击“清除缓存数据”按钮。
 
提示 开发模块(http://drupal.org/project/devel)是专门用来支持Drupal开发的。它能帮你快速的访问许多开发功能,比如清空缓存,查看变量,追踪查询语句,以及更多。它是专业开发的必备品。如果你还没有安装它的话,那么需要下载它,并将文件夹放在sites/all/modules/devel,接着启用该模块,然后导航到“管理➤站点构建 ➤区块”,启用它的开发区块。
 
     我们使用两步就可以建立我们的新类别了。首先,我们添加一个菜单项,用来描述类别头部。这个菜单项有一个唯一的路径(admin/annotate)。我们声明:它应该放在右栏中,重量为-5,这样它就恰好位于“站点配置”类别的上面,从而方便了截图,如图2-3所示的。
 
     第二步是告诉Drupal,把指向注释设置的实际链接放在类别“Node annotation”的内部。我们通过修改原有菜单项的路径来实现这一点,以前的路径为admin/settings/annotate,现在被替换为了admin /annotate/settings。在以前,菜单项是“站点配置”类别路径admin/settings的孩子,如表2-1所示。当Drupal重新构造菜单树时,它查找路径来为父菜单项和子菜单项建立继承关系,由于admin /annotate/settings是admin /annotate的孩子,这决定了要像图2-3那样显示。将模块菜单项嵌套在如表2-1所示的任意一个路径下,将使模块出现在Drupal管理页面中该类别的下面。
 
     当然,这仅仅是一个例子,在真实场景下,为了创建一个新的类别,你必须有充分的理由,否则管理员(通常是你自己)面对太多类别时,会犯困的。
2-1 管理类别的路径
路径                      类别
admin/content           内容管理
admin/build              站点构建
admin/settings           站点配置
admin/user                用户管理
admin/logs                日志
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

为用户呈现一个设置表单(1)

在注释模块中,我们允许管理员选择哪些节点类型支持注释(如图2-1所示)。让我们深入学习一下这是如何工作的。

 
    当一个站点管理员想要修改注释模块的设置时,我们需要显示一个表单,让管理员可以从我们所给的选项中进行选择。在我们的菜单项中,我们把页面回调设置为drupal_get_form(),把页面参数设置为一个包含annotate_admin_settings的数组。这意味着,当你访问http://example.com/?q=admin/annotate/settings时,调用 drupal_get_form('annotate_admin_settings')将被执行,它主要是告诉Drupal构建由函数annotate_admin_settings()定义的表单。
 
老葛的Drupal培训班 Think in Drupal

Drupal版本:

为用户呈现一个设置表单(2)

老葛的Drupal培训班 Think in Drupal

下面让我们看一下定义表单的函数,它为节点类型定义了一个复选框(参看图2-1),并且增加了另外两个选项。函数位于sites/all/modules/custom/annotate/annotate.admin.inc中:

 
/**
 * Form builder. Configure annotations.
 *
 * @ingroup forms
 * @see system_settings_form().
 */
function annotate_admin_settings() {
    // Get an array of node types with internal names as keys and
    // "friendly names" as values. E.g.,
    // array('page' => 'Page', 'story' => 'Story')
    $options = node_get_types('names');
 
    $form['annotate_node_types'] = array(
        '#type' => 'checkboxes',
        '#title' => t('Users may annotate these content types'),
        '#options' => $options,
        '#default_value' => variable_get('annotate_node_types', array('page')),
        '#description' => t('A text field will be available on these content types
            to make user-specific notes.'),
    );
 
    $form['annotate_deletion'] = array(
        '#type' => 'radios',
        '#title' => t('Annotations will be deleted'),
        '#description' => t('Select a method for deleting annotations.'),
        '#options' => array(
            t('Never'),
            t('Randomly'),
            t('After 30 days')
        ),
       // Default to Never
        '#default_value' => variable_get('annotate_deletion', 0)   
    );
 
    $form['annotate_limit_per_node'] = array(
        '#type' => 'textfield',
        '#title' => t('Annotations per node'),
        '#description' => t('Enter the maximum number of annotations allowed per
           node (0 for no limit).'),
        '#default_value' => variable_get('annotate_limit_per_node', 1),
        '#size' => 3
    );
 
    return system_settings_form($form);
}
 
    我们添加了一个单选按钮,用来选择什么时候应该删除注释;添加了一个文本输入框,用来限制一个节点上所允许的注释数量(这些模块增强特性的实现,留给大家作为练习)。在这里,我们自己没有管理表单的处理流程,而是使用了函数system_settings_form()来让系统模块为表单添加一些按钮,并让它来管理表单的验证和提交。图2-4给出了的当前表单的样子。
2-4 使用了复选框,单选按钮,文本输入框的增强表单

Drupal版本:

验证用户提交的设置

  如果由函数system_settings_form()为我们负责保存表单数值,那么我们如何才能判定在“Annotations per node”字段中输入的是一个数字?我们可以钩住表单提交的处理过程么?当然可以了。我们只需在sites/all/modules/custom/annotate/annotate.admin.inc中定义一个验证函数,如果我们发现有任何异常的话,就使用这个函数来设置一个错误消息。

 
/**
* Validate the annotation configuration form.
*/
function annotate_admin_settings_validate($form, $form_state) {
    $limit = $form_state['values']['annotate_limit_per_node'];
    if (!is_numeric($limit)) {
        form_set_error('annotate_limit_per_node', t('Please enter a number.'));
    }
}
 
    现在,当Drupal处理这个表单时,它将回调annotate_admin_settings_validate()来进行验证。如果我们检测到输入了无效数据的话,那么我们将为发生错误的字段设置一个错误信息,这反映为在页面上就是显示一个警告信息,并将包含错误的字段进行高亮显示,如图2-5所示:
图2-5 验证脚本设置了一个错误信息
 
    Drupal是怎么知道要调用我们的函数呢?我们对函数的命名采用了特殊的方式,使用表单定义函数的名字(annotate_admin_settings)+ _validate。对于Drupal是如何判定要调用哪个验证函数的详细解释,可参看第10章。
 
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

存储设置

   在前面的例子中,修改设置并点击“保存配置”按钮,可以正常工作。如果点击了“重置为默认值”按钮,那么各个字段将被重置为它们的默认值。下面部分将描述如何实现这一点。

 
使用Drupal的variables表
  首先,让我们看一下字段“Annotations per node”(“每个节点的注释数”)。它的#default_value键是这样设置的:
variable_get('annotate_limit_per_node', 1)
 
  Drupal在数据库中有一个名为variables的表,并且键-值对可以使用variable_set($key,$value)来存储,使用variable_get($key,$default)来取回。所以我们实际上说的是,“将字段‘Annotations per node’的默认值设置为数据库表variables中存储的变量annotate_limit_per_node的值,如果该值不存在,那么使用1作为默认值”。所以当点击“重置为默认值”按钮时,Drupal将从variables表中删除键annotate_limit_per_node对应的当前条目,并使用默认值1.
 
警告variables表中存储和取回设置时,为了避免命名空间的冲突,你应该让你的表单字段的名字和变量的键(如上例中的annotate_limit_per_node)的名字相同。命名方式为:你的模块名加上一个描述性的名称。表单字段和变量的键应该同时使用该名字。
 
 
    由于 Annotations will be deleted”字段是一个单选按钮,所以它看起来复杂了一点。这个字段的#option如下所示:
'#options' => array(
    t('Never'),
    t('Randomly'),
    t('After 30 days')
)
 
    当PHP遇到一个没有键的数组时,它默认的为其插入数字键,所以这个数组在内部实际上就是:
'#options' => array(
    [0] => t('Never'),
    [1] => t('Randomly'),
    [2] => t('After 30 days')
)
 
    当我们为这个字段设置默认值时,我们使用:
'#default_value' => variable_get('annotate_deletion', 0) //默认为Never
 
    这意味着,当起作用时,默认为数组的项目0,也就是t('Never')。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

使用variable_get()来取回存储的值

当你的模块取回已存储的设置时,应该使用variable_get():

// Get stored setting of maximum number of annotations per node.
$max = variable_get('annotate_limit_per_node', 1);
 
    注意,在这里为variable_get()使用了默认值,就是在没有存储值可用的情况下使用(可能管理员还没有访问设置页面)。
 
老葛的Drupal培训班 Think in Drupal

Drupal版本:

篇外话

我们将与开源社区分享这一模块,这是自然的,所以需要创建一个README.txt文件,然后把它放到annotation的目录下,和annotate.info,annotate.module,annotate.install文件放在一起。README.txt文件一般包含的信息有,谁编写了这个模块,以及如何使用这个模块。这里不需要包含许可证信息,这是因为所有上传到drupal.org的模块都将采用GPL许可,而drupal.org上的打包脚本将会为模块自动添加一个LICENSE.txt文本。接下来,你就可以把它上传到drupal.org上的第3方模块资源库中了,然后创建一个项目页面,用来追踪社区中其他用户的反馈。

老葛的Drupal培训班 Think in Drupal

 

Drupal版本:

总结

老葛的Drupal培训班 Think in Drupal

当读完这一章后,你应该可以处理以下任务:

从头创建一个Drupal模块。
理解如何钩住Drupal代码的执行。
存储和取回特定模块的设置。
使用Drupal的表单API来创建和处理一些简单的表单。
使用你模块的数据库表来存储和取回数据。
Drupal的主管理页面创建一个新的管理类别。
定义一个表单,使得管理员可以选择选项,使用复选框,文本输入字段,和单选按钮。
验证设置,如果验证失败,则返回一个错误消息。
理解Drupal是如何使用内置的持久化变量系统来存储和取回设置的
 

Drupal版本:

第3章 drupal 钩子,动作,和触发器

使用Drupal时,一个常见的目标就是,当一个事件发生时需要做些东西。例如,站点管理员可能希望在一个消息发布以后收到一封电子邮件。或者当用户在评论中使用了违禁词语,那么就会被自动封号。本章将描述如何使用Drupal的事件钩子,从而当那些事件发生时,能够运行自己的代码。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

理解事件和触发器

老葛的Drupal培训班 Think in Drupal

Drupal在运行自己的业务时,需要处理一系列的事件。而这些内部事件实际就是一些时机,模块在此时能够与Drupal的处理流程进行交互。表3-1给出了一些Drupal事件。
 
3-1. Drupal事件示例
事件                  类型
创建一个节点            节点
删除一个节点            节点
查看一个节点            节点
创建一个用户帐号        用户
更新用户个人资料        用户
登录                    用户
登出                    用户
 
    Drupal开发者将这些内部事件称为钩子,这是因为当一个事件发生时,Drupal允许模块从该点钩进Drupal的执行路径。我们在前面的章节中,应该也见到了一些钩子。在模块开发中,一般都会涉及到这种情况----判定对哪个Drupal事件做出反应,也就是说,你需要在你的模块中实现哪些钩子。
 
    假定你有一个刚刚起步的网站,而网站所在的主机则被你放到了你的地下室中,开始都有点简陋。一旦你的网站有了人气,你可能就会打算把它卖给一个大公司,继而一夜暴富。在网站迈向成功的期间,你可能想监督用户的每次登录,用户的每次登录,都能给你带来一点希望。你决定,当有一个用户登录时,你的计算机就会发出嘟嘟嘟嘟的声音。不过你的小猫也住在地下室,为了避免嘟嘟声打扰了小猫的美梦,你决定使用一个简单的日志条目来模拟嘟嘟声。你快速的编写了一个.info文件,并将其放在了sites/all/modules/custom/beep/beep.info:
 
; $Id$
name = Beep
description = Simulates a system beep.
package = Pro Drupal Development
core = 6.x
 
    接着,该编写sites/all/modules/custom/beep/beep.module了:
<?php
// $Id$
/**
* @file
* Provide a simulated beep.
*/
function beep_beep() {
    watchdog('beep', 'Beep!');
}
 
    这将向Drupal的日志中写入一条消息“Beep!”(“嘟嘟!”)。现在已经不错了。接着,我们需要告诉Drupal当用户登录时发出嘟嘟声。通过在我们的模块中实现hook_user(),并将逻辑添加到login操作中,我们就可以完成目标了:
 
/**
* Implementation of hook_user().
*/
function beep_user($op, &$edit, &$account, $category = NULL) {
    if ($op == 'login') {
        beep_beep();
    }
}
    简单吧!如果添加了新内容时,也要发出嘟嘟声,那该怎么办呢?通过在我们的模块中实现hook_nodeapi(),并将逻辑添加到insert操作中,这样也就完成目标了:
/**
* Implementation of hook_nodeapi().
*/
function hook_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
    if ($op == 'insert') {
        beep_beep();
    }
}
 
    如果我们想在添加评论时,也让Drupal发出嘟嘟声,那又该怎么办呢?好的,我们可以实现hook_comment(),并将逻辑添加到insert操作中,但是让我们暂停一下,并好好的思考一下。其实我们在重复的做着同样的一件事情。如果有那么一个图形化的用户界面,在那里我们可以将嘟嘟这个动作关联到我们想要的钩子和操作上,那该多好啊?这就是Drupal内置的触发器模块所要实现的功能。它允许你将一些动作与特定事件关联起来。在代码中,事件就是一个唯一的钩子操作联合体,比如“用户钩子, 登录操作”或者“nodeapi钩子, 插入操作”。当这些操作发生时,trigger.module就会让你触发一个动作。
 
    为了避免概念的混淆,让我们明确给出我们所用的术语:
事件:一个一般的编程概念,这个术语一般理解为:从系统的一个构件向其它构件发送一个消息。
钩子:这个编程技术,用在Drupal中,就是允许模块“钩进”执行流程。
操作:它指的是在钩子内部运行的一个具体的流程。例如,登录操作就是用户钩子中的一个操作。

触发器:它指的是,一个具体的钩子操作联合体与一个或多个动作的关联。例如,嘟嘟这个动作可以与用户钩子的登录操作关联起来。

理解动作

    一个动作就是Drupal要做的一些事情。下面是一些例子:

• 将节点推到首页
• 将节点从未发布状态改为发布状态
• 删除一个用户
• 发送一封电子邮件
    这里面的每一种情况,都包含了一个定义明确的任务。程序员可能会注意到,前面列表中所给的这些动作与PHP函数有点类似。例如,你可以调用includes/mail.inc中的drupal_mail()函数来发送一封电子邮件。动作听起来与函数类似,其实动作就是函数。它们是一些特殊的函数:Drupal可以通过自省将其与事件关联起来(我们一会儿将对此详细介绍)。现在,让我们看看触发器模块。
 
老葛的Drupal培训班 Think in Drupal
 

Drupal版本:

触发器用户界面

老葛的Drupal培训班 Think in Drupal

导航到“管理➤站点构建 ➤模块”,并启用触发器模块。接着导航到“管理➤站点构建 ➤触发器”。你将看到的界面应该与图3-1所示的类似。
3-1.触发器分配界面
    注意顶部横向的标签。它们对应于Drupal钩子!在图3-1中,我们查看的是nodeapi钩子的各种操作。它们的命名都很容易理解;比如,nodeapi钩子的delete操作就标注为“在删除文章之后”。对于钩子中的每个操作,在操作发生时,都可以为其分配一个动作,比如“将文章推到首页”。而每个可用的动作都列在了名为“选择一个动作”的下拉选择框中。
 
注意 不是所有的动作对所有的触发器都可用,这是因为有些动作在特定的上下文中没有任何意义。例如,在触发器“在删除文章之后”中,你就不能使用“将文章推到首页”这个动作。根据你的安装,有些触发器可能会显示“没有为该触发器可用的动作”。
 
    表3-2给出了一些触发器名字和它们对应的钩子和操作。
 
3-2. Drupal 6中,钩子,操作,触发器的对应关系
钩子       操作       触发器名字
comment     insert      在保存新的评论之后
comment     update      在更新评论之后
comment     delete      在删除评论后
comment     view        当评论正在被注册用户查看时
cron        run         cron 运行时
nodeapi     presave     当保存新文章或更新文章时
nodeapi     insert      在保存新文章之后
nodeapi     update      在更新文章之后
nodeapi     delete      在删除文章之后
nodeapi     view        在内容被注册用户查看时
taxonomy    insert      在将新术语存储到数据库之后
taxonomy    update      在将更新过的术语存储到数据库之后
taxonomy    delete      在删除一个术语后
user        insert      在用户帐户创建之后
user        update      在用户资料更新之后
user        delete      在用户被删除之后
user        login       在用户登录之后
user        logout      在用户退出之后
user        view        当用户资料被浏览时
 

Drupal版本:

你的第一个动作

如果将我们的嘟嘟函数转化为一个完整的动作,那么我们需要做哪些工作呢?这有两个步骤:

1. 通知Drupal该动作所支持的触发器。
2. 创建你自己的动作函数。
 
    第一步就是实现hook_action_info()。下面给出beep模块中该钩子的实现:
/**
* Implementation of hook_action_info().
*/
function beep_action_info() {
    $info['beep_beep_action'] = array(
        'type' => 'system',
        'description' => t('Beep annoyingly'),
        'configurable' => FALSE,
        'hooks' => array(
            'nodeapi' => array('view', 'insert', 'update', 'delete'),
            'comment' => array('view', 'insert', 'update', 'delete'),
            'user' => array('view', 'insert', 'update', 'delete', 'login'),
            'taxonomy' => array('insert', 'update', 'delete'),
        ),
    );
    return $info;
}
 
    该函数的名字为beep_action_info(),在这里,和其它的钩子实现一样,我们使用了:模块名(beep)+钩子名(action_info)。我们将返回一个数组,数组中的一个条目就对应我们的模块中的一个动作。由于我们只编写了一个动作,所以只有一个条目,它的键就是执行动作的函数的名字:beep_beep_action()。为了在阅读代码时,方便的识别哪个函数是个动作,我们在我们的beep_beep()函数的名字后面追加了_action,这样就成了beep_beep_action()。
 
让我们仔细的看一下数组中的键:
• type: 这是你编写的动作的类型。Drupal使用该信息,将动作归类到触发器分配界面的下拉选择框中。可能的类型包括system, node, user, comment, 和taxonomy。在判定你编写的动作的类型时,你需要好好的想一想,“这个动作作用于什么对象呢?”(如果答案不确定,或者是“各种不同的对象!”,那么可以使用system类型)。
• description:这是该动作的描述性名字,它显示在触发器分配界面的下拉选择框中。
• configurable:这个是用来判定该动作是否带有参数的。
• hooks: 在这个钩子数组中,每个条目都是用来列举该动作所支持的操作的。Drupal使用这一信息,来判定该动作在触发器分配界面中的位置。
 
    我们已经向Drupal描述了我们的动作,所以让我们继续:
/**
* Simulate a beep. A Drupal action.
*/
function beep_beep_action() {
    beep_beep();
}
 
    这也不是太难吧,不是么?在继续往下以前,由于我们将使用触发器和动作来取代直接的钩子实现,所以让我们回过头来将beep_user()和beep_nodeapi()删除。
 
老葛的Drupal培训班 Think in Drupal

Drupal版本:

分配该动作

现在,让我们重新回到“管理➤站点构建 ➤触发器”。如果你正确的完成了前面所给的这些东西,那么你的动作将会出现在这个用户界面,如图3-2所示。

3-2.该动作出现在了触发器用户界面的下拉选择框中
老葛的Drupal培训班 Think in Drupal

Drupal版本:

修改动作所支持的触发器

老葛的Drupal培训班 Think in Drupal

如果你修改了该动作所支持的触发器,那么你就能够在用户界面中看到相应的变化。例如,如果你将beep_action_info()改成如下的内容,那么你的“Beep”动作只能用于触发器“在删除文章之后”:

/**
* Implementation of hook_action_info().
*/
function beep_action_info() {
    $info['beep_beep_action'] = array(
        'type' => 'system',
        'description' => t('Beep annoyingly'),
        'configurable' => FALSE,
       'hooks' => array(
           'nodeapi' => array('delete'),
        ),
    );
    return $info;
}

Drupal版本:

支持所有触发器的动作

 

如果你不想将你的动作限定在特定的触发器或者触发器集上,那么通过以下声明,你的动作就支持所有的触发器了:
/**
* Implementation of hook_action_info().
*/
function beep_action_info() {
    $info['beep_beep_action'] = array(
        'type' => 'system',
        'description' => t('Beep annoyingly'),
        'configurable' => FALSE,
        'hooks' => array(
           'any' => TRUE,
        ),
    );
    return $info;
}
老葛的Drupal培训班 Think in Drupal
 

 

Drupal版本:

高级动作(1)

动作主要有两种类型:带有参数的动作和不带参数的动作。我们前面所写的“嘟嘟”动作就是不带参数的动作。当动作执行时,它嘟嘟一下,这就完事了。但是许多时候,动作可能需要更多一点的上下文。例如,一个“发送电子邮件”动作,需要知道将电子邮件的发收件人以及邮件的标题和正文。这种需要为其在配置表单中做些设定的动作,就是高级动作,也称为可配置动作

 
    简单的动作不带参数,也不需要配置表单,并且能够自动出现在触发器分配界面(访问“管理➤站点构建 ➤模块”以后)。如果你想告诉Drupal你的动作是一个高级动作的话,你需要进行以下步骤:在你模块的hook_action_info()实现中将configurable键设置为TRUE;提供一个表单用来配置该动作;提供一个可选的验证处理器和一个必须的提交处理器来处理配置表单。表3-3对简单动作和高级动作的区别进行了总结。
 
3-3.简单和高级动作的不同之处的总结
                     简单动作          高级动作
参数                    无*                 必须的
配置表单                无                  必须的
可用性                  自动                需使用动作管理界面创建动作实例
hook_action_info()      FALSE               TRUE
configure的值
* 如果需要的话,可以使用$object和$context参数。
 
    让我们创建一个可以嘟嘟多次的高级动作。我们可以使用一个配置表单来指定该动作嘟嘟的次数。首先,我们需要告诉Drupal这个动作是可配置的。让我们在beep.module的action_info钩子实现中,为我们的新动作添加一个条目:
/**
* Implementation of hook_action_info().
*/
function beep_action_info() {
    $info['beep_beep_action'] = array(
        'type' => 'system',
        'description' => t('Beep annoyingly'),
        'configurable' => FALSE,
        'hooks' => array(
            'nodeapi' => array('delete'),
        ),
    );
    $info['beep_multiple_beep_action'] = array(
       'type' => 'system',
       'description' => t('Beep multiple times'),
       'configurable' => TRUE,
       'hooks' => array(
           'any' => TRUE,
       ),
    );
    return $info;
}
老葛的Drupal培训班 Think in Drupal

Drupal版本:

高级动作(2)

让我们快速的检查一下,我们的实现是否正确,导航到“管理➤站点配置➤动作”。不错,动作出现在了高级动作的下拉选择框中了,如图3-3所示。

 
3-3.新动作显示在了高级动作的下拉选择框中
 
    现在,我们需要提供一个表单,这样管理员就可以选择嘟嘟多少次了。通过使用Drupal的表单API,来定义一个或多个字段,我们就可以实现这一点。我们还需要编写表单的验证函数和提交函数。它们的名字是基于hook_action_info()中所定义的动作ID的。我们当前讨论的动作的动作ID为beep_multiple_beep_action,所以按照约定,我们在后面追加_form,这样就得到了表单定义的函数名字beep_multiple_beep_action_form。Drupal期望的验证函数名字为:动作ID+ _validate(beep_multiple_beep_action_validate);提交函数的名字为:动作ID+ _submit(beep_multiple_beep_action_submit)。
 
/**
* Form for configurable Drupal action to beep multiple times.
*/
function beep_multiple_beep_action_form($context) {
    $form['beeps'] = array(
        '#type' => 'textfield',
        '#title' => t('Number of beeps'),
        '#description' => t('Enter the number of times to beep when this action
            executes.'),
        '#default_value' => isset($context['beeps']) ? $context['beeps'] : '1',
        '#required' => TRUE,
    );
    return $form;
}
 
function beep_multiple_beep_action_validate($form, $form_state) {
    $beeps = $form_state['values']['beeps'];
    if (!is_numeric($beeps)) {
        form_set_error('beeps', t('Please enter a numeric value.'));
    }
    else if ((int) $beeps > 10) {
        form_set_error('beeps', t('That would be too annoying. Please choose fewer
            than 10 beeps.'));
    }
}
 
function beep_multiple_beep_action_submit($form, $form_state) {
    return array(
        'beeps' => (int) $form_state['values']['beeps']
    );
}
老葛的Drupal培训班 Think in Drupal

Drupal版本:

高级动作(3)

老葛的Drupal培训班 Think in Drupal

第一个函数向Drupal描述了表单。我们只定义了一个文本输入框字段,这样管理员就可以输入嘟嘟的次数了。当管理员选择添加一个高级动作“嘟嘟多次”时,如图3-3所示,Drupal将会使用我们的表单字段来呈现一个完整的动作配置表单,如图3-4所示。

3-4.动作“嘟嘟多次”的动作配置表单
 
    Drupal向动作配置表单添加了一个描述字段。该字段的值是可编辑的,它将用来替代我们在action_info钩子中定义的默认描述。这是有意义的,因为我们可以创建一个高级动作用来嘟嘟两次并将其描述为“嘟嘟两次”,然后创建另一个用来嘟嘟五次并将其描述为“嘟嘟五次”。这样,在将动作分配给一个触发器时,我们就指出了这两个高级动作之间的区别。这样,高级动作的描述对于管理员来说就是很有意义的。
 
提示 这两个动作,“嘟嘟两次”和“嘟嘟五次”,可以看作是“嘟嘟多次”动作的实例。
 
    验证函数和Drupal中其它的表单验证函数一样(关于表单验证的更多详细,请参看第10章)。在这里,我们对用户的输入作了检查,以确保用户输入的是一个数字并且该数字不是特别大。
 
    提交函数的返回值是特定于动作配置表单的。它应该是一个数组,其中以我们关心的字段为键。这个数组中的值在动作运行时可供动作使用。描述是由系统自动处理的,所以我们只需要返回我们提供的字段就可以了,在这里也就是,嘟嘟的次数。
 
    最后,该编写高级动作本身了:
/**
* Configurable action. Beeps a specified number of times.
*/
function beep_multiple_beep_action($object, $context) {
    for ($i = 1; $i < $context['beeps']; $i++) {
        beep_beep();
    }
}
    你会注意到这个动作有两个参数,$object和$context。而我们前面所写的简单动作中,就没有带参数,二者在这一点上有点不同。
 
注意 简单动作也可以和高级动作一样,带有参数。由于PHP会忽略掉传递给函数但是没有出现在函数签名中的参数,如果我们需要了解当前的上下文信息,那么可以简单的将我们的简单动作的函数签名从beep_beep_action()改为beep_beep_action($object, $context)。所有的动作都可以使用$object和$context参数。
 

Drupal版本:

在动作中使用上下文

老葛的Drupal培训班 Think in Drupal

我们在前面已经看到,动作的函数签名的一般形式为example_action($object,$context)。下面让我们学习一下这些参数的具体含义。

• $object: 许多动作都是作用于Drupal的一个内置对象的:节点、用户、分类术语、等等。当trigger.module执行动作时,被作用的对象就会通过参数$object传递给动作。例如,如果一个动作被设置为在新节点创建时执行的话,那么$object参数包含的就是节点对象。
• $context: 一个动作可以在许多不同的上下文中被调用。通过在hook_action_info()中定义hooks键,动作就可以声明它们所支持的触发器。但是支持多个触发器的动作,需要使用一些方式来判定它们被执行时所处的上下文。这样,根据上下文的不同,动作会做出不同的反应。
 

Drupal版本:

触发器模块是如何准备上下文的

老葛的Drupal培训班 Think in Drupal

让我们设定一个场景。假定你有一个网站,是用来呈现争议性问题的。下面是它的业务模型:用户通过付费注册进来,并只能在网站上发布一条评论。一旦他们发布了评论,他们就会被封号,直到再次付费后才被解封。我们不关心这样的网站是否有经济前景,这里主要考虑的是:如何使用触发器和动作来实现它。我们需要一个动作来阻止当前用户。检查一下user.module,我们看到Drupal已经为我们提供了这个动作:
 
/**
* Implementation of hook_action_info().
*/
function user_action_info() {
    return array(
        'user_block_user_action' => array(
            'description' => t('Block current user'),
            'type' => 'user',
            'configurable' => FALSE,
            'hooks' => array(),
        ),
        'user_block_ip_action' => array(
            'description' => t('Ban IP address of current user'),
            'type' => 'user',
            'configurable' => FALSE,
            'hooks' => array(),
        ),
    );
}
 
    然而,这些动作却没有显示在触发器分配页面,为什么呢?这是因为它们的hooks键是一个空数组,也就是它们不支持任何钩子。如果我们能只改一下hooks键,那不就可以了?不错,可以这样做,让我们往下看。

Drupal版本:

使用drupal_alter()修改已有的动作

Drupal运行action_info钩子时,每个模块都可以声明它所提供动作,Drupal还给了模块一个机会,让它们修改该信息----包括其它模块提供的信息。下面让我们修改“阻止当前用户”这个动作,让它可以用于评论插入这个触发器:

 
/**
* Implementation of hook_drupal_alter(). Called by Drupal after
* hook_action_info() so modules may modify the action_info array.
*
* @param array $info
* The result of calling hook_action_info() on all modules.
*/
function beep_action_info_alter(&$info) {
    // Make the "Block current user" action available to the
    // comment insert trigger. If other modules have modified the
    // array already, we don't stomp on their changes; we just make sure
    // the 'insert' operation is present. Otherwise, we assign the
    // 'insert' operation.
    if (isset($info['user_block_user_action']['hooks']['comment'])) {
        array_merge($info['user_block_user_action']['hooks']['comment'],
            array('insert'));
    }
    else {
        $info['user_block_user_action']['hooks']['comment'] = array('insert');
    }
}
 
    最终的结果就是,“阻止当前用户”这个动作现在可被分配了,如图3-5所示。
3-5.将动作“阻止当前用户”分配给评论插入触发器
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

建立上下文

由于我们已经分配了动作,所以当一个新评论被发布时,当前用户将被阻止。让我们仔细的看一下,这里都发生了什么。我们已经知道,Drupal是通过触发钩子来向模块通知特定事件的。在这里触发的就是评论钩子。由于当前是一个新评论正被添加进来,所以当前的特定操作就是insert操作。触发器模块实现了评论钩子。在这个钩子内部,它对数据库进行查询,来获取分配到这个特定触发器上的所有动作。数据库就会将我们分配的动作“阻止当前用户”返回给该钩子。现在,触发器模块就可以执行该动作了,它符合标准的动作函数签名example_action($object, $context)。

 
    但是我们又有了一个问题。当前要被执行的动作是一个用户类型的动作,而不是评论类型的。所以它期望接收到的对象是一个用户对象!但是在这里,一个用户动作在一个评论钩子的上下文中被调用了。与评论相关的信息被传递给了钩子,而传递的不是与用户相关的信息。那么我们该怎么办呢?实际上发生的是,触发器模块会判定我们的动作是一个用户动作,并加载用户动作所需的$user对象。下面是来自modules/trigger/trigger.module的代码,它给出了这是如何实现的:
 
/**
* When an action is called in a context that does not match its type,
* the object that the action expects must be retrieved. For example, when
* an action that works on nodes is called during the comment hook, the
* node object is not available since the comment hook doesn't pass it.
* So here we load the object the action expects.
*
* @param $type
* The type of action that is about to be called.
* @param $comment
* The comment that was passed via the comment hook.
* @return
* The object expected by the action that is about to be called.
*/
function _trigger_normalize_comment_context($type, $comment) {
    switch ($type) {
    // An action that works with nodes is being called in a comment context.
    case 'node':
        return node_load($comment['nid']);
 
    // An action that works on users is being called in a comment context.
    case 'user':
        return user_load(array('uid' => $comment['uid']));
    }
}
 
    当为我们的用户动作执行前面的代码时,匹配的是第2种情况,所以将会加载用户对象并接着执行我们的用户钩子。评论钩子所知道的信息(比如,评论的标题)将会通过$context参数传递给动作。注意,动作是如何查找用户ID的----首先在对象中查找,其次在上下文中查找,最后使用全局变量$user:
/**
* Implementation of a Drupal action.
* Blocks the current user.
*/
function user_block_user_action(&$object, $context = array()) {
    if (isset($object->uid)) {
        $uid = $object->uid;
    }
    elseif (isset($context['uid'])) {
        $uid = $context['uid'];
    }
    else {
        global $user;
        $uid = $user->uid;
    }
    db_query("UPDATE {users} SET status = 0 WHERE uid = %d", $uid);
    sess_destroy_uid($uid);
    watchdog('action', 'Blocked user %name.', array('%name' =>
        check_plain($user->name)));
}
 
    动作必须要聪明一点,因为当它们被调用时它们并不知道发生了什么。这就是为什么,动作最好是直接的,甚至是原子的。触发器模块总是将当前的钩子和操作放在上下文中,通过上下文将其传递过来。它们的值存储在$context['hook'] 和$context['op']中。这种方式是向动作传递信息的标准方式。
 
老葛的Drupal培训班 Think in Drupal

Drupal版本:

检查上下文

将钩子和操作放在上下文中,这一点非常有用。我们举个例子,动作“发送电子邮件”就大量的利用了这一点。这个动作的类型为system,它可以被分配给许多不同的触发器。

 
    动作“发送电子邮件”在合成电子邮件期间,允许将特定的令牌替换掉。例如,你可能想在邮件的正文中包含一个节点的标题,或者想把节点的作者作为电子邮件的收件人。但是根据该动作分配给的触发器的不同,该收件人可能并不可用。例如,如果是在用户钩子中发送的电子邮件,由于没有节点可用,所以更谈不上让节点作者作为收件人了。modules/system/system.module中的动作“发送电子邮件”,它首先会花点时间来检查上下文从而判定有什么可用。下面,它将确保当前有一个节点,这样就可以利用节点相关的各种属性了:
 
/**
* Implementation of a configurable Drupal action. Sends an e-mail.
*/
function system_send_email_action($object, $context) {
    global $user;
    switch ($context['hook']) {
        case 'nodeapi':
            // Because this is not an action of type 'node' (it's an action
            // of type 'system') the node will not be passed as $object,
            // but it will still be available in $context.
            $node = $context['node'];
            break;
        case 'comment':
            // The comment hook provides nid, in $context.
            $comment = $context['comment'];
            $node = node_load($comment->nid);
        case 'user':
            // Because this is not an action of type 'user' the user
            // object is not passed as $object, but it will still be
            // available in $context.
            $account = $context['account'];
            if (isset($context['node'])) {
                $node = $context['node'];
            }
            elseif ($context['recipient'] == '%author') {
                // If we don't have a node, we don't have a node author.
                watchdog('error', 'Cannot use %author token in this context.');
                return;
            }
            break;
        default:
            // We are being called directly.
            $node = $object;
    } ...
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

动作的存储

老葛的Drupal培训班 Think in Drupal

动作就是在给定时间运行的函数。简单的动作不带有可配置的参数。例如,我们创建的动作“嘟嘟”只会简单的嘟嘟一下。它不需要任何其它的信息(当然,如果需要的话还是可以使用$object和$context。)将这个动作与我们创建的高级动作相比,那么动作“嘟嘟多次”就需要知道嘟嘟的次数了。而其它的高级动作,比如动作“发送电子邮件”,可能需要更多的信息:电子邮件的收件人,电子邮件的主题,等等。这些参数都需要存储在数据库中。
 
表actions
    当管理员创建一个高级动作的实例时,在配置表单输入的信息将被序列化并保存到actions表的parameters字段中。简单动作“嘟嘟”的数据库记录应该是这样的:
aid: 'beep_beep_action'
type: 'system'
callback: 'beep_beep_action'
parameters:
description: Beep
 
    相反,动作“嘟嘟多次”的一个实例对应的数据库记录应该是这样的:
aid: 2
type: 'system'
callback: 'beep_beep_action'
parameters: (serialized array containing the beeps parameter with its value, i.e.,
the number of times to beep)
description: Beep three times
 
    在一个高级动作被执行前,parameters字段中的内容将被反序列化,并被包含在$context参数中,从而传递给该动作。所以,在我们的动作“嘟嘟多次”的实例中,在beep_multiple_beep_action()中通过$context['beeps'] 就可以取得嘟嘟的次数了。
 
动作ID
    注意,在前面的部分中,两条记录的动作ID之间的不同。简单动作的动作ID就是实际的函数名字。但是,很明显,对于高级动作,因为可能会存储一个动作的多个实例,所以我们在这里不能为其使用函数名作为标识。因此在这里使用了一个数字动作ID(存放在数据库表actions_aid中的)。
 
    动作执行引擎,会基于动作ID是不是数字,来判定是否需要为其取出存储的参数。如果它不是数字,那么动作就被简单的执行了,这样就不需要再查询数据库了。这是一个非常迅速的判定;Drupal在index.php中就使用了同样的方式,来区分内容和菜单常量。
 

Drupal版本:

直接使用actions_do()来调用一个动作

触发器模块仅是调用动作的一种方式。你可能想写一个单独的模块,它需要自己负责动作的调用和参数的准备。如果是这样的话,那么推荐使用actions_do()来调用动作。函数的签名如下:

actions_do($action_ids, &$object, $context = array(), $a1 = NULL, $a2 = NULL)
 
让我们学习一下里面的参数:
• $action_ids: 要执行的动作,既可以是单个动作ID,也可以是一个包含动作ID的数组。
• $object: 该动作要作用的对象,如果存在的话。
• $context:一个关联数组,里面包含了动作可能想要使用的信息,对于高级动作里面会包含配置参数。
• $a1 and $a2:可选的额外参数,如果传递给了actions_do(),那么也将会传递给该动作。
 
    下面是我们如何使用actions_do()来调用我们的简单动作“嘟嘟”的:
$object = NULL; // $object is a required parameter but unused in this case
actions_do('beep_beep_action', $object);
 
    而下面则是我们如何调用高级动作“嘟嘟多次”的:
$object = NULL;
actions_do(2, $object);
 
    或者,我们还可以绕过获取存储的参数这一步,从而这样调用它:
$object = NULL;
$context['beeps'] = 5;
actions_do('beep_multiple_beep_action', $object, $context);
 
注意 一些中坚的PHP开发者可能会疑惑,“有必要使用动作么?为什么不直接调用该函数,或者仅仅实现一个钩子?为什么需要把参数隐藏在上下文中,直接使用传统的PHP参数不也能实现吗?”答案是,通过编写一个带有非常一般的函数签名的动作,那么就可以实现代码的重用,这样就方便了站点管理员。站点管理员,可能并不懂得PHP,如果他想在添加节点时实现发送电子邮件的功能,那么它就不需要雇佣一个PHP程序员了。他只需要简单的将动作“发送电子邮件”分配到触发器“在保存新文章之后”上,就能实现想要的功能了,这样就不再需要麻烦他人了。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

使用hook_hook_info()定义你自己的触发器

Drupal是怎么知道,有哪些触发器是可以显示在触发器用户界面的?按照典型的方式,它能够让模块通过钩子声明该模块所实现的钩子。例如,这里是来自comment.module的hook_hook_info()实现。定义触发器描述的地方就是hook_hook_info()的实现:

 
/**
* Implementation of hook_hook_info().
*/
 
function comment_hook_info() {
    return array(
        'comment' => array(
            'comment' => array(
                'insert' => array(
                    'runs when' => t('After saving a new comment'),
                ),
                'update' => array(
                    'runs when' => t('After saving an updated comment'),
                ),
                'delete' => array(
                    'runs when' => t('After deleting a comment')
                ),
                'view' => array(
                    'runs when' => t('When a comment is being viewed by an
                        authenticated user')
                ),
            ),
        ),
    );
}
 
    如果我们安装了一个名为monitoring.module的模块,它向Drupal引入了一个新的名为monitoring(监控)的钩子,它可以这样描述该钩子下面的两个操作(overheating(过热)和freezing(过冷)):
/**
* Implementation of hook_hook_info().
*/
function monitoring_hook_info() {
    return array(
        'monitoring' => array(
            'monitoring' => array(
                'overheating' => array(
                    'runs when' => t('When hardware is about to melt down'),
                ),
                'freezing' => array(
                    'runs when' => t('When hardware is about to freeze up'),
                ),
            ),
        ),
    );
}
    在启用了监控模块以后,Drupal就能够看到新的hook_hook_info()实现,并修改触发器页面,为新钩子包含一个单独的标签,如图3-6所示。当然,模块本身仍然需要使用module_invoke()或者module_invoke_all()来触发钩子,以及负责触发相应的动作。在这个例子中,该模块需要调用module_invoke_all('monitoring', 'overheating')。它接着需要实现hook_monitoring($op),并使用actions_do()来触发动作。对于一个简单的具体实现,可参看modules/trigger/trigger.module中的trigger_cron()。
 
3-6.新定义的触发器以一个标签的形式显示在了触发器用户界面
 
    尽管一个模块可以定义多个新钩子,但只有与模块名字匹配的钩子才会在触发器界面创建一个标签。在我们的例子中,监控模块定义了监控钩子。如果它还定义了一个不同的钩子,那么该钩子既不会出现在监控标签下,也不会独自拥有一个标签。然而,对于那些与模块名字不匹配的钩子,仍然可以使用路径http://example.com/?q=admin/build/trigger/hookname来直接访问。
 
老葛的Drupal培训班 Think in Drupal

Drupal版本:

向已有钩子中添加触发器

老葛的Drupal培训班 Think in Drupal

有时候,如果你的代码新增了一个操作的话,那么你可能想要在一个已有的钩子上添加触发器。例如,你可能想向nodeapi钩子添加一个操作。假定你编写了一个模块,用来存档旧节点并将其迁移到数据仓库中。由于这个操作是作用于节点的,所以你可能想在nodeapi钩子下面添加一个archive操作,这样对于内容的所有操作,都会显示在触发器界面的同一个标签下。下面的代码用来添加一个额外的触发器:

 
/**
* Declare a new trigger, to appear in the node tab.
*/
function archiveoffline_hook_info() {
    $info['archiveoffline'] = array(
        'nodeapi' => array(
            'archive' => array(
                'runs when' => t('When the post is about to be archived'),
            ),
        ),
    );
    return $info;
}
    导航到触发器管理页面“管理➤站点构建 ➤触发器”,在触发器列表的最后,我们看到了新增的触发器,如图3-7所示。
3-7.额外的触发器(“当文章即将被存档”)出现在了用户界面
 
    Drupal的菜单系统将使用hook_hook_info()实现中的第一个键,来自动在触发器管理页面创建一个标签。Drupal将使用模块的.info文件中定义的模块名字作为标签的名字(参看图3-7中没有用到的Archive Offline标签)。但是我们的新触发器不需要放在它自己的标签下;通过将我们的操作添加到nodeapi钩子中,我们有意地将新触发器放在了内容标签下。我们可以使用hook_menu_alter()来删除不想要的标签(该钩子的更多详细,可参看第4章)。下面的代码将自动创建的标签,从类型MENU_LOCAL_TASK(Drupal默认将其作为标签显示)改为了类型MENU_CALLBACK,这样Drupal就不再显示它了:
 
/**
* Implementation of hook_menu_alter().
*/
function archiveoffline_menu_alter(&$items) {
    $items['admin/build/trigger/archiveoffline']['type'] = MENU_CALLBACK;
}
 
    为了让archiveoffline_menu_alter()函数起作用,我们需要访问“管理➤站点构建 ➤模块”,这样菜单将被重建。

Drupal版本:

总结

    读完本章后,你应该能够

• 理解如何将动作分配给触发器
• 编写一个简单的动作
• 编写一个高级动作和它的相关配置表单。
• 使用动作管理页面,来创建和重命名高级动作的实例。
• 理解什么是上下文
• 理解动作是如何使用上下文来修改它们的行为的。
• 理解动作的存储、取回、和执行。
• 定义你自己的钩子并将它们显示为触发器。
老葛的drupal培训班 Think in Drupal

Drupal版本:

第4章 Drupal 菜单系统

老葛的Drupal培训班 http://zhupou.cn

Dru    Drupal的菜单系统很复杂,但是也很强大。术语“菜单系统”可能有点用词不当了。下面的理解可能会更恰当一些,那就是将菜单系统看作一个拥有3种主要功能的系统:1、回调映射,2、访问控制,3、菜单定制。菜单系统的基本代码位于includes/menu.inc中,而可选代码则位于modules/menu,后者可用来启用菜单的一些特性比如自定义菜单等等.
                在本章中,我们将探索一下什么是回调映射以及它是如何工作的,看一下如何通过访问控制来保护菜单项,学习如何使用菜单通配符,并逐条列出了各种内置的菜单项类型。在本章的最后,给出了如何覆写,添加,和删除已有的菜单项,这样你就可以随心所欲的定制Drupal了。
 

Drupal版本:

回调映射

当一个Web浏览器向Drupal发送一个请求时,它向Drupal传递了一个URL。通过这一信息,Drupal必须指出要运行哪段代码以及如何处理这一请求。这也就是通常所说的路由或者分发。Drupal截掉URL的基部分并使用后面的部分,后者被称之为Drupal路径。举例来说,如果URL是http://example.com/?q=node/3,则Drupal路径就为node/3。如果你使用了Drupal的简洁URL特性,那么在浏览器中的URL就是http://example.com/node/3,但是在你的web服务器中,在Drupal收到这个URL以前,web服务器已将其重写为http://example.com/?q=node/3;所以,对于Drupal来说,这两者是一样的。在前面的例子中,不管是否启用了简洁URL,其Drupal路径都是node/3。此方面的更多详细,可参看第一章中的“Web服务器的角色”。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

将URL映射为函数

通常采用的方式如下所示:Drupal请求所有启用的模块来提供一个菜单项数组,每个菜单项都包含了一个数组,其中以路径为键,里面还包含了路径的一些相关信息。一个模块必须提供的一段信息就是页面回调page callback)。在这里,回调就是一个PHP函数的名称,当一个浏览器请求一个特定的路径时就会调用它。当一个请求到达时,Drupal将执行以下步骤:

    1.建立Drupal路径。如果路径是一个真实路径的别名,Drupal将找出真实路径并使用它来代替别名。比如,如果管理员使用别名http://example.com/?q=about 来代替http://example.com/?q=node/3(比如,使用路径模块),那么Drupal将会使用node/3作为内部路径。
    2.Drupal使用menu_router表来追踪路径与回调函数之间的映射,使用menu_links表来追踪菜单项链接。首先会检查是否需要重新构建menu_router和menu_links表,不过一般在Drupal安装或者更新以后,就很少再会重新构建了。
    3.计算出menu_router表中的哪个条目对应于Drupal路径,并构建出一个路由项,来描述即将被调用的回调。
    4. 加载需要传递给回调的对象。
    5. 检查用户是否有权访问该回调。如果没有的话,返回一个“拒绝访问”消息
    6. 根据当前语言,将菜单项的标题和描述本地化。
    7. 加载需要的.inc文件
    8. 调用回调并返回结果,index.php将调用theme_page(),从而为浏览器返回一个网页。
 
该流程的图示可参看图4-1和4-2。
4-1.菜单分发流程的概览
 
4-2. 菜单路由器和链接的构建流程概览
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

创建一个菜单项

老葛的Drupal培训班 Think in Drupal

通过在你的模块中使用菜单钩子来钩住这一流程。这样你就可以定义包含在menu_router表中的菜单项。让我们构建一个名为menufun.module的示例模块,通过它来学习菜单系统。我们将Drupal路径menufun映射到PHP函数menufun _hello()上。首先,我们需要一个名为menufun.info的文件,位于sites/all/modules/custom/menufun/menufun.info:

 
; $Id$
name = Menu Fun
description = Learning about the menu system.
package = Pro Drupal Development
core = 6.x
 
    接着我们创建sites/all/modules/custom/menufun/menufun.module文件,它包含了我们的hook_menu()实现,以及我们想要运行的函数:
<?php
// $Id$
/**
* @file
* Use this module to learn about Drupal's menu system.
*/
/**
* Implementation of hook_menu().
*/
function menufun_menu() {
    $items['menufun'] = array(
        'page callback' => 'menufun_hello',
        'access callback' => TRUE,
        'type' => MENU_CALLBACK,
    );
 
    return $items;
}
/**
* Page callback.
*/
function menufun_hello() {
    return t('Hello!');
}
 
Enabling the module at Administer ä Site building äModules causes the menu item to
be inserted into the router table, so Drupal will now find and run our function when we go to http://example.com/?q=menufun, as shown in Figure 4-3.
 
    在“管理➤站点构建 ➤模块”中,启用该模块,这样就会将该菜单项插入到menu_router表中,这样,当我们访问http://example.com/?q=menufun时,Drupal就可以找到菜单项并运行我们的函数了,如图4-3所示。
 
    这里需要注意的要点是,我们定义了一个路径,并将其映射到了一个函数上。该路径是一个Drupal路径。我们使用该路径作为$items数组的键。你还会注意到这个路径的名字和模块的名字是一样的。这里主要是用来保证有一个干净的URL命名空间。实际上,你可以在这里定义各种路径。
   
4-3.菜单项使得Drupal能够找到和运行menufun_hello()函数。
 

Drupal版本:

定义一个标题

前面所写的hook_menu()实现,还是非常简单的。让我们在里面添加一些键,这样就和我们通常所用的差不多了。

 
function menufun_menu() {
    $items['menufun'] = array(
       'title' => 'Greeting',
        'page callback' => 'menufun_hello',
        'access callback' => TRUE,
        'type' => MENU_CALLBACK,
    );
 
    return $items;
}
 
    我们为我们的菜单项给出了一个标题,当在浏览器中显示该页面时,它会自动用作页面标题(如果你想在后面的代码执行时覆写页面标题的话,你可以使用drupal_set_title())。保存了这些改动以后,刷新你的浏览器,却并没有显示出来我们定义的标题。为什么呢?这是因为Drupal将所有的菜单项存储在了menu_router表中,尽管这里我们的代码改动了,但是数据库还没有变。我们需要告诉Drupal来重新构建menu_router表。这里有两种方式。最简单的就是安装开发者模块(http://drupal.org/project/devel),并在“管理➤站点构建 ➤区块”中启用devel区块。devel区块中包含了一个名为重构菜单的选项。点击它将会重构menu_router表。如果你没有安装开发者模块的话,直接访问“管理➤站点构建 ➤模块”,也能实现同样的效果。作为显示该页面的准备工作的一部分,Drupal重构了菜单表。从现在起,我假定大家知道知道了每次代码修改后都需要重构菜单,以后就不再对此进行单独说明了。
 
    重构后,我们的页面如图4-4所示。
4-4.菜单项的标题显示在了页面和浏览器标题栏中。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

页面回调参数(1)

有时,你可能希望向映射到该路径上的函数提供更多的信息。首先,路径中的其它部分将会自动传递过来。让我们修改一下我们的函数:

 
function menufun_hello($first_name = '', $last_name = '') {
    return t('Hello @first_name @last_name',
        array('@first_name' => $first_name, '@last_name' => $last_name));
}
 
    现在,如果我们访问http://example.com/?q=menufun/John/Doe,我们将得到如图4-5所示的输出。
 
4-5.路径的其余部分传递给了回调函数。
 
    注意,这里的URL中的其它的各个部分是如何作为参数传递给我们的回调函数的。
 
    通过向$items数组中添加可选的'page arguments'(页面参数)键,你还可以在菜单钩子中定义页面回调参数。定义页面参数非常有用,这样你就可以从不同的菜单项中调用同一个回调函数了,并通过页面参数为回调提供隐藏的上下文。让我们在我们的菜单项中定义一些页面参数:
 
function menufun_menu() {
    $items['menufun'] = array(
        'title' => 'Greeting',
        'page callback' => 'menufun_hello',
       'page arguments' => array('Jane', 'Doe'),
        'access callback' => TRUE,
        'type' => MENU_CALLBACK,
    );
 
    return $items;
}
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

页面回调参数(2)

老葛的Drupal培训班 Think in Drupal

你定义在页面参数中的回调参数将会首先传递给回调函数(也就是说,在传递给回调函数的参数列表中,它放在最前面),其次才是从路径中生成的参数。来自URL的参数仍然可用;为了访问它们,你可以修改回调函数的签名,从而添加来自于URL的参数。所以,对于我们修改后的菜单项,并使用下面的函数签名,那么$first_name将是Jane(页面参数数组的第一项),$last_name将是Doe(页面参数数组的第二项),$a将是John(来自于URL),而$b将是Doe(来自于URL)。
 
function menufun_hello($first_name = '', $last_name = '', $a = '', $b = '') {...}
 
    让我们做一下测试,将Jane Doe放到页面参数中,把John Doe放在URL中,然后看看结果。访问http://example.com/?q=John/Doe,你将看到如图4-6所示的结果(如果你没有得到这些结果,那你一定忘了重构你的菜单了)。
4-6.向回调函数传递和显示参数
 
    在页面回调参数的数组中,键被忽略了,所以你不能使用键来映射函数参数;在这里,只有顺序才是有意义的。回调参数通常是变量,并常用在动态菜单项中。
 

Drupal版本:

放在其它文件中的页面回调

如果你没有特别指定的话,那么Drupal会假定你把页面回调放在了.module文件中。在Drupal6中,对于每个页面请求,为了尽可能的降低为其加载的代码总量,许多模块被拆分成了多个部分。如果回调函数不在当前的.module文件中的话,可以使用菜单项中的file键,来指定哪个文件包含了该函数。我们在第2章中编写注释模块的时候,就用到了file键。

 

    如果你定义了file键,那么Drupal将会在你的模块目录下查找该文件。如果你的页面回调是由其它模块提供的话,也就是说该文件不在你的模块目录中,那么你需要告诉Drupal在查找该文件时所用的文件路径。使用file path键,就可以轻松的实现这一点了。我们在第2章的“定义你自己的管理部分”就用到了它。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

向导航区块中添加一个链接

老葛的Drupal培训班 Think in Drupal

我们把我们的菜单项的类型声明为了MENU_CALLBACK。通过将该类型改为MENU_NORMAL_ITEM,这样就不再将路径简单的映射到一个回调函数上了,而是让Drupal把它作为一个菜单包含进来。
 
提示 因为MENU_NORMAL_ITEM是Drupal的默认菜单类型,所以在本节中的代码里,type键可被忽略。我将会在后面的例子中忽略它。
 
function menufun_menu() {
    $items['menufun'] = array(
        'title' => 'Greeting',
        'page callback' => 'menufun_hello',
        'page arguments' => array('Jane', 'Doe'),
        'access callback' => TRUE,
       'type' => MENU_NORMAL_ITEM,
    );
 
    return $items;
}
 
    菜单项现在显示在了导航区块中,如图4-7所示。
4-7.菜单项显示在了导航区块中
 
If we don’t like where it is placed, we can move it down by increasing its weight. Weight is another key in the menu item definition:
如果我们觉得它放的不是地方,通过增加它的重量,我们还可以将它往下移动。重量是菜单项定义中的另一个键:
 
function menufun_menu() {
    $items['menufun'] = array(
        'title' => 'Greeting',
        'page callback' => 'menufun_hello',
        'page arguments' => array('Jane', 'Doe'),
        'access callback' => TRUE,
       'weight' => 5,
    );
 
    return $items;
}
The effect of our weight increase is shown in Figure 4-8. Menu items can also be relocated without changing code by using the menu administration tools, located at Administer ä Site building äMenus (the menu module must be enabled for these tools to appear).
我们的重量增加后的效果,如图4-8所示。在管理界面“管理➤站点构建 ➤菜单”中(你需要启用菜单模块),使用菜单管理工具,不需要修改代码,也可以调整菜单项之间的相对顺序。
 
4-8.菜单项的重量越大,在导航区块中的位置就越往下。
 

Drupal版本:

菜单嵌套

老葛的Drupal培训班 Think in Drupal

到目前为止,我们仅仅定义了一个静态菜单项。让我们再添加一个与它相关的子项:
 
function menufun_menu() {
    $items['menufun'] = array(
        'title' => 'Greeting',
        'page callback' => 'menufun_hello',
        'access callback' => TRUE,
        'weight' => -10,
    );
    $items['menufun/farewell'] = array(
       'title' => 'Farewell',
       'page callback' => 'menufun_goodbye',
       'access callback' => TRUE,
    );
 
    return $items;
}
 
/**
* Page callback.
*/
function menufun_hello() {
return t('Hello!');
}
/**
* Page callback.
*/
function menufun_goodbye() {
    return t('Goodbye!');
}
 
    Drupal将会注意到第2个菜单项(menufun/farewell)的路径是第一个菜单项路径(menufun)的孩子。因此,在显示菜单时(转化为HTML),Drupal将会缩进第2个菜单项,如图4-9所示。它还在页面的顶部正确的设置了面包屑,以指示嵌套关系。当然,根据设计者的要求,主题可将菜单或面包屑显示成所要的样式。
 
4-9.嵌套菜单
 

Drupal版本:

访问控制

到目前为止,在前面的所有例子中,我们都简单的将菜单项的access callback键设置为了TRUE,这意味着所有的用户都可以访问我们的菜单。一般情况下,通过在模块中使用hook_perm()来定义权限,并使用一个函数来测试这些权限,从而实现对菜单的访问控制。这里所用的函数的名字定义在菜单项的access callback键中,它一般为user_access。让我们定义一个名为receive greeting的权限;如果用户没有哪个角色具有该权限的话,当他/她访问页面http://example.com/?q=menufun时,将会收到一个“拒绝访问”消息。

 
/**
* Implementation of hook_perm().
*/
function menufun_perm() {
    return array('receive greeting');
}
 
/**
* Implementation of hook_menu().
*/
function menufun_menu() {
    $items['menufun'] = array(
        'title' => 'Greeting',
        'page callback' => 'menufun_hello',
       'access callback' => 'user_access',
       'access arguments' => array('receive greeting'),
        'weight' => -10,
    );
    $items['menufun/farewell'] = array(
        'title' => 'Farewell',
        'page callback' => 'menufun_goodbye',
    );
 
    return $items;
}
 
 
    在前面的代码中,是根据user_access('receive greeting')的返回结果来判定是否允许访问的。这样,菜单系统就相当于一个门卫,它会基于用户的角色来判定哪些路径可以访问,哪些路径不可以访问。
 
提示 user_access()函数是默认的访问回调。如果你没有定义访问回调的话,那么访问参数将被菜单系统传递给user_access()。
 
    子菜单一般不会继承父菜单项的访问回调和访问参数。所以必须为每个菜单项定义access arguments键。如果访问回调不是user_access的话,那么还需要定义access callback键。不过也有例外,那就是类型为MENU_DEFAULT_LOCAL_TASK的菜单项,它可以继承父菜单项的访问回调和访问参数,不过为了清晰起见,对于这些默认的本地任务菜单项,最好能够为其明确的定义这些键。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

标题的本地化和定制

Drupal支持多语言,它使用t()函数来翻译字符串。所以你可能会想,菜单项中的title键应该是这样定义的:

'title' => t('Greeting') // No! don't use t() in menu item titles or descriptions.
 
    然而,你想错了。菜单字符串是以原始字符串的形式存储在menu_router表中的,而菜单项的翻译则被推迟到了运行时进行。真实情况是,Drupal有一个默认的翻译函数(t()函数),它被指定用来翻译菜单标题。你将在后面看到,如何将默认翻译函数修改为你选择的函数,以及如何向该函数传递参数。负责翻译的函数被称为title callback(标题回调),而传递过来的参数则被称为title arguments(标题参数)。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

定义标题回调

如果没有在菜单项中定义标题回调的话,Drupal将默认使用t()函数。我们也可以明确地给出这个回调函数的名字,在title callback键中声明它就是了:

 
function menufun_menu() {
    $items['menufun'] = array(
        'title' => 'Greeting',
       'title callback' => 't',
        'description' => 'A salutation.',
        'page callback' => 'menufun_hello',
        'access arguments' => array('receive greeting'),
    );
}
 
注意 不管title callback键的值如何,description键总是使用t()来翻译的。这里没有描述回调键。
 
    嗯。如果我们为标题回调声明自己的函数,那会怎么样呢?那就让我们看看吧:
 
function menufun_menu() {
    $items['menufun'] = array(
        'title' => 'Greeting',
       'title callback' => 'menufun_title',
        'description' => 'A salutation.',
        'page callback' => 'menufun_hello',
        'access callback' => TRUE,
    );
 
    return $items;
}
 
/**
* Page callback.
*/
function menufun_hello() {
    return t('Hello!');
}
/**
* Title callback.
*/
function menufun_title() {
    $now = format_date(time());
    return t('It is now @time', array('@time' => $now));
}
 
    如图4-10所示,通过使用一个自定义标题回调,就可以实现在运行时设置菜单项标题了。但是,如果想让菜单项标题和页面标题不一样时,那该怎么办呢?简单。我们可以使用drupal_set_title()来设置页面标题:
 
function menufun_title() {
    drupal_set_title(t('The page title'));
    $now = format_date(time());
    return t('It is now @time', array('@time' => $now));
}
 
4-10.标题回调设置了菜单项的标题
    这样就将页面和菜单项的标题分离了开来,如图4-11所示。
4-11. 将菜单项和页面的标题分离开来
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

标题参数

Drupal的翻译函数,可以接受一个字符串和一个用来替换的数组(更多关于t()函数的详细,参看第18章),例如:

t($string, $keyed_array);
t('It is now @time', array('@time' => $now));
 
    那么,如果菜单项的title键就是传递给t()的字符串,那么用来替换的数组放在哪里呢?这个问题问得好。title arguments键就是负责这件事的:
function menufun_menu() {
    $items['menufun'] = array(
       'title' => 'Greeting for Dr. @name',
       'title callback' => 't',
       'title arguments' => array('@name' => 'Foo'),
        'page callback' => 'menufun_hello',
        'access callback' => TRUE,
    );
 
    return $items;
}
 
    在运行时,翻译函数运行了,占位符也被填充了,如图4-12所示。
4-12.标题参数被传递给了标题回调函数。
 
    不过这种替换也有一个缺点。因为定义在菜单钩子中的菜单项是在菜单构建流程期间保存在数据库中的,所以title arguments中的代码是在菜单构建时执行的,而不是在运行时。如果你想在运行时修改你的菜单的话,最好使用title callback键;定义在这里的函数将在运行时运行。
 
警告 title arguments键中的值必须是字符串。整数将被清除掉;因此,'title arguments' => array('@name' => 3)将不起作用,而'title arguments' => array('@name' => '3')则能正常工作。这是因为整数具有特殊含义,你将在接下来看到。
 
老葛的Drupal培训班 Think in Drupal

Drupal版本:

菜单项中的通配符

到目前为止,我们在菜单项中所用的都是普通的Drupal路径名字,比如menufun 和menufun/farewell。但是Drupal还经常使用这样的路径,比如user/4/track或node/15/edit,在这些路径中,有一部分是动态的。现在,让我们来看看动态路径是如何工作的。

 
基本通配符
    %字符在Drupal菜单项中是一个特殊的字符。它意味着“从这到下一个/字符之间的字符串”。下面是一个使用了通配符的菜单项:
 
function menufun_menu() {
    $items['menufun/%'] = array(
        'title' => 'Hi',
        'page callback' => 'menufun_hello',
        'access callback' => TRUE,
    );
    return $items;
}
 
    这个菜单项适用于的Drupal路径可以有menufun/hi, menufun/foo/bar, menufun/
123, 和menufun/file.html。但是它不适用于路径menufun;对于后者,因为它只包含了一个部分,而menufun/%只匹配具有两部分的字符串,所以你需要为其单独创建一个菜单项。注意,尽管%通常是用来指定一个数字的(比如,user/%/edit用于user/2375/edit),但是它能匹配该位置上的任何文本。
 
注意 在路经中带有通配符的菜单项,即便是将菜单项的类型设置为MENU_NORMAL_ITEM,它也不会显示在导航菜单中。原因很明显:由于路径中包含了一个通配符,所以Drupal不知道如何为该路径构建URL。这是一般情况下的规律,也有例外的情况,因为你可以告诉Drupal使用什么URL,更多详细,可参看本章后面的“使用to_arg()函数为通配符构建路径”。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

通配符和页面回调参数

老葛的Drupal培训班 Think in Drupal

菜单路径中的通配符,不影响将URL中的额外部分作为参数传递给页面回调,这是因为通配符只匹配到下一个/字符。继续使用我们的menufun/%路径作为例子,对于URL http://example.com/?q=menufun/foo/Fred,通配符所匹配的字符串就是foo,而对于路径中的最后一部分(Fred),它将作为一个参数传递给页面回调。

Drupal版本:

使用通配符的值

老葛的Drupal培训班 Think in Drupal

为了使用路径中匹配的那部分,可以在page arguments键中指定该路径部分的位置:
function menufun_menu() {
    $items['menufun/%/bar/baz'] = array(
        'title' => 'Hi',
        'page callback' => 'menufun_hello',
        'page arguments' => array(1), // The matched wildcard.
        'access callback' => TRUE,
    );
    return $items;
}
 
/**
* Page callback.
*/
function menufun_hello($a = NULL, $b = NULL) {
    return t('Hello. $a is @a and $b is @b', array('@a' => $a, '@b' => $b));
}
 
我们的页面回调函数menufun_hello()所收到的参数,如图4-13所示。
 
4-13.第一个参数来自于匹配的通配符,第2个参数来自于URL的最后部分。
 
    第一个参数,$a,是通过页面回调传递过来的。用于页面回调的条目array(1)的意思是,“不管路径中的部分1是什么,请将它传递过来”。我们是从0开始算起的,所以部分0就是'menufun',部分1就是通配符所匹配的任何东西,部分2就是'bar',依次类推。第2个参数,$b,也被传递了过来,它的传递原理我们在前面已经学过了,那就是Drupal路径后面的一部分将会作为参数传递给回调函数(参看本章前面的“页面回调参数”一节)。
 

Drupal版本:

通配符和参数替换

在实际中,一个Drupal路径的一部分通常是用来查看或者修改一个对象的,比如一个节点对象或者一个用户对象。例如,路径node/%/edit用来编辑一个节点,而路径user/%则用来根据用户ID来查看用户的相关信息。让我们看一下后者的菜单项,你可以在modules/user/user.module中的hook_menu()实现中找到它。这个路径匹配的URL应该看起来是这样的http://example.com/?q=user/2375。在你的Drupal站点上点击查看“我的帐号”页面,就能看到这样的URL了。

 
 
$items['user/%user_uid_optional'] = array(
    'title' => 'My account',
    'title callback' => 'user_page_title',
    'title arguments' => array(1),
    'page callback' => 'user_view',
    'page arguments' => array(1),
    'access callback' => 'user_view_access',
    'access arguments' => array(1),
    'file' => 'user_pages.inc',
);
 
 
暂停一下!路经user/%user_uid_optional是怎么一回事呢?我们在这里详细的解释一下:
1.使用/字符将路径切分成各个部分。
2.在第2部分中,匹配从%到下一个可能的/字符之间的字符串。在这里,该字符串就是user_uid_optional。
3. 向该字符串上追加_load,来生成一个函数的名字。在这里,该函数的名字就是user_uid_optional_load。
4. 调用该函数,并将Drupal路径中通配符的值作为参数传递给它。所以,如果URL为http://example.com/?q=user/2375, Drupal路径为user/2375,而通配符匹配的第2部分就是2375,那么调用的就是user_uid_optional_load('2375')。
5. 使用这个调用所返回的结果来替换通配符。这里的标题参数为array(1),在标题回调被调用时,我们没有传递Drupal路径中的部分1(2375),而是传递了user_uid_optional_load('2375')返回的结果,也就是一个用户对象。我们可以把它看作,Drupal路径中的一部分被它所表示的对象替换了。
6. 注意,页面回调和访问回调也将使用替换对象。所以,在前面的菜单项中,user_view_access()用于访问控制,user_view()则用于生成页面内容,对于这两者,都会传递进来用户2375的用户对象。
 
提示 对于Drupal路径,比如node/%node/edit,如果你将%node看作是一个通配符%,并在其右边加上了一个注释的话(这里为node),那么会更容易的理解对象替换。换句话说,node/%node/edit实际上就是node/%/edit,外加了一个隐藏指令:为通配符匹配的内容运行node_load()。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

向加载函数传递额外的参数

如果需要向加载函数传递额外的参数,那么可以使用load arguments键来定义它们。下面是来自节点模块的例子:一个用来查看节点修订本的菜单项。在这里需要向加载函数,也就是node_load(),传递节点ID和修订本的ID。

 
$items['node/%node/revisions/%/view'] = array(
    'title' => 'Revisions',
    'load arguments' => array(3),
    'page callback' => 'node_show',
    'page arguments' => array(1, NULL, TRUE),
    'type' => MENU_CALLBACK,
);
 
    菜单项为load arguments键指定了array(3)。这意味着,除了节点ID通配符的值会自动传递给加载函数以外,还会向加载函数传递一个额外的参数。因为array(3)里面有个元素,也就是整数3;我们在“使用通配符的值”一节中已经讲过,这意味着将会使用路径中的部分3。位置和路径参数的示例URL http://example.com/?q=node/56/revisions/4/view,如表4-2所示。
 
4-2.当查看页面http://example.com/?q=node/56/revisions/4/view时,Drupal路径node/%node/revisions/%/view的位置和参数
 
位置   参数       来自URL的值
0       node        node
1       %node       56
2       revisions  revisions
3       %           4
4       view        view
 
    因此,定义了load arguments键,这就意味着将会调用node_load('56', '4'),而不是node_load('56')了。
 
    当页面回调运行时,加载函数会将'56'替换为加载了的节点对象,所以页面回调将会是node_show($node, NULL, TRUE)。
老葛的Drupal培训班 Think in Drupal

Drupal版本:

特殊的,预定义的加载参数:%map和%index

老葛的Drupal培训班 Think in Drupal

有两个特殊的加载参数。%map令牌将当前Drupal路径作为数组进行传递。在前面的例子中,如果%map作为一个加载参数传递过来的话,那么其值就为array('node', '56', 'revisions', '4', 'view')。如果加载函数的参数是通过引用传递的话,那么加载函数可以修改%map中对应的值。例如,在modules/user/user.module中,user_category_load($uid, &$map, $index)就是这样用来处理类别名字中字符“/”的。
 
    %index令牌在加载函数中指的是通配符的位置。对于前面的例子,由于通配符的位置为1,如表4-2所示,所以该令牌的值就为1。

Drupal版本:

使用to_arg()函数为通配符构建路径

老葛的Drupal培训班 Think in Drupal

还记不记得我曾经说过,对于包含通配符的Drupal路径,Drupal无法为其创建一个有效的链接,比如user/%(毕竟,Drupal怎么会知道如何替换%呢)?不过这一点并非完全正确。我们可以定义一个帮助函数,来为通配符生成一个替身,这样,在Drupal构建链接时就可以使用替身了。在菜单项“我的帐号”中,使用以下步骤来生成“我的帐号”链接的路径:
    1. Drupal路径最初为user/%user_uid_optional。
    2. 当构建链接时,Drupal会查找一个名为user_uid_optional_to_arg()的函数。如果没有定义该函数的话,那么Drupal就不知道如何构建路径,因此也就无法显示链接了。
    3. 如果找到了该函数,那么Drupal将会使用该函数返回的结果来替代链接中的通配符。user_uid_optional_to_arg()函数返回了当前用户的用户ID,假定你就是用户4,那么Drupal就会将“我的帐号”链接到http://example.com/?q=user/4
 
    to_arg()函数不是特定于一个给定路径的。换句话说,对于任何页面,在构建链接期间都会运行to_arg()函数,而不仅仅是对于匹配Drupal路径的一个特定页面。“我的帐号”链接显示在所有的页面,而不仅仅是页面http://example.com/?q=user/3

 

Drupal版本:

通配符和to_arg()函数的特殊情况

老葛的Drupal培训班 Think in Drupal

Drupal在为一个菜单项构建链接时,它要查找的to_arg()函数,是基于Drupal路径中通配符后面的字符串生成的。它可以是任意的字符串,例如:
/**
* Implementation of hook_menu().
*/
function_menufun_menu() {
    $items['menufun/%a_zoo_animal'] = array(
        'title' => 'Hi',
        'page callback' => 'menufun_hello',
        'page arguments' => array(1),
        'access callback' => TRUE,
        'type' => MENU_NORMAL_ITEM,
        'weight' => -10
    );
    return $items;
}
 
function a_zoo_animal_to_arg($arg) {
    // $arg is '%' since it is a wildcard
    // Let's replace it with a zoo animal.
    return 'tiger';
}
 
    这样,链接“Hi”就会出现在导航区块中了。该链接的URL为http://example.com/?q=menufun/tiger。通常,你不会像这个简单的例子中这样,使用一个静态字符串来替换通配符,而是使用动态的东西,比如当前用户的uid或者当前节点的nid。

Drupal版本:

改变其它模块的菜单项

老葛的Drupal培训班 Think in Drupal

Drupal重构menu_router表和更新menu_link表时(比如,当一个新模块被启用时),通过实现hook_menu_alter(),模块就可以修改任意的菜单项。例如,“登出”菜单项通过调用user_logout()将当前用户登出,它将销毁用户的会话并将用户重定向到站点的首页。由于user_logout()函数位于modules/user/user.pages.inc,所以该Drupal路径的菜单项中定义了file键。所以,通常情况下,当一个用户点击了导航区块中的“登出”链接,Drupal会加载文件modules/user/user.pages.inc并运行user_logout()函数。
/**
* Implementation of hook_menu_alter().
*
* @param array $items
* Menu items keyed by path.
*/
function menufun_menu_alter(&$items) {
    // Replace the page callback to 'user_logout' with a call to
    // our own page callback.
    $items['logout']['page callback'] = 'menufun_user_logout';
    // Drupal no longer has to load the user.pages.inc file
    // since it will be calling our menufun_user_logout(), which
    // is in our module -- and that's already in scope.
    unset($items['logout']['file']);
}
 
/**
* Menu callback; logs the current user out, and redirects to drupal.org.
* This is a modified version of user_logout().
*/
function menufun_user_logout() {
    global $user;
 
    watchdog('menufun', 'Session closed for %name.', array('%name' => $user->name));
 
    // Destroy the current session:
    session_destroy();
    // Run the 'logout' operation of the user hook so modules can respond
    // to the logout if they want to.
    module_invoke_all('user', 'logout', NULL, $user);
 
    // Load the anonymous user so the global $user object will be correct
    // on any hook_exit() implementations.
    $user = drupal_anonymous_user();
 
    drupal_goto('http://drupal.org/');
}
    在我们的hook_menu_alter()实现运行以前,logout路径的菜单项应该是这样的:
array(
    'access callback' => 'user_is_logged_in',
    'file' => 'user.pages.inc',
    'module' => 'user',
    'page callback' => 'user_logout',
    'title' => 'Log out',
    'weight' => 10,
)
    当我们修改了它以后,就变成了这样:
array(
    'access callback' => 'user_is_logged_in',
    'module' => 'user',
    'page callback' => 'menufun_user_logout',
    'title' => 'Log out',
    'weight' => 10,
)

Drupal版本:

改变其它模块的菜单链接

模块通过实现hook_menu_link_alter(),就可以在Drupal将一个菜单项保存到menu_link表时,来修改该链接了。下面是如何将“登出”菜单项的标题改为“Sign off”(“结束”)的。

 
/**
* Implementation of hook_link_alter().
*
* @param $item
* Associative array defining a menu link as passed into menu_link_save().
* @param $menu
* Associative array containing the menu router returned from
* menu_router_build().
*/
function menufun_menu_link_alter(&$item, $menu) {
    if ($item['link_path'] == 'logout') {
        $item['link_title'] = 'Sign off';
    }
}
 
    这个钩子可以用来修改一个链接的标题和重量。如果你需要修改一个菜单项的其它属性的话,比如访问回调,那么需要使用hook_menu_alter()。
 
注意hook_menu_link_alter()中对菜单项所做的修改,是无法在用户界面“管理➤站点构建 ➤菜单”中再进行覆写的。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

菜单项的种类

当你在菜单钩子中添加一个菜单项时,经常用到的一个键就是type。如果你没有定义类型,那么将会使用默认类型MENU_NORMAL_ITEM。Drupal将根据你指定的类型来对菜单项进行不同的处理。每一个菜单项类型都有一系列的标记或者属性组成。表4-2列出了菜单项类型的标记。

 
4-2. 菜单项类型标记
二进制        十六进制   十进制        常量
000000000001    0x0001      1               MENU_IS_ROOT
000000000010    0x0002      2               MENU_VISIBLE_IN_TREE
000000000100    0x0004      4               MENU_VISIBLE_IN_BREADCRUMB
000000001000    0x0008      8               MENU_LINKS_TO_PARENT
000000100000    0x0020      32              MENU_MODIFIED_BY_ADMIN
000001000000    0x0040      64              MENU_CREATED_BY_ADMIN
000010000000    0x0080      128             MENU_IS_LOCAL_TASK
 
    例如,常量MENU_NORMAL_ITEM拥有标记MENU_VISIBLE_IN_TREE和MENU_VISIBLE_IN_BREADCRUMB,如表4-3所示。看一下不同的标记在单个常量中是如何表示的,你看出来了吗?
 
4-3.菜单项类型MENU_NORMAL_ITEM的标记
二进制            常量
000000000010        MENU_VISIBLE_IN_TREE
000000000100        MENU_VISIBLE_IN_BREADCRUMB
000000000110        MENU_NORMAL_ITEM
 
    因此,MENU_NORMAL_ITEM拥有下列标记:0000000001104-4展示了可用的菜单项类型和它们所表示的标记
 
 
4-4. 菜单项类型表示的标记
菜单标记              菜单类型常量
                      MENU_                 MENU_            MENU_            MENU_
                            NORMAL_    MENU_      SUGGESTED_             LOCAL_           DEFAULT_
                            ITEM       CALLBACK  ITEM*            TASK             LOCAL_TASK
MENU_IS_ROOT
MENU_VISIBLE_IN_TREE        X
MENU_VISIBLE_IN_BREADCRUMB  X          X          X
MENU_LINKS_TO_PARENT                                                                X
MENU_MODIFIED_BY_ADMIN
MENU_CREATED_BY_ADMIN
MENU_IS_LOCAL_TASK                                                 X                X
*这个常量是由位逻辑运算符OR和0x0010创建的
 
    当你定义自己的菜单项类型时,应该使用哪一个常量呢?查看表4-4,在里面看一下你想启用哪些标记,然后使用包含这些标记的常量。对于每个常量的详细描述,参看includes/menu.inc里面的注释。最常用的为MENU_CALLBACK, MENU_LOCAL_TASK, 和 MENU_DEFAULT_LOCAL_TASK。更多详细,请仔细阅读里面的注释。
 
老葛的Drupal培训班 Think in Drupal

Drupal版本:

常见任务

老葛的Drupal培训班 Think in Drupal

在这一部分,我们给出了一些常见问题的典型解决办法,这些问题都是程序员使用菜单时会经常遇到的。

 
分配回调而无须向菜单添加一个链接
    通常,你可能想将一个URL映射到一个函数上,而不需要创建一个可见的菜单项。例如,你可能在web表单中有一个JavaScript函数,它需要从Drupal中得到一列状态,所以你需要将这个URL映射到一个PHP函数上,而不需要将它放到导航区块中。你可以通过将你的菜单项的类型指定为MENU_CALLBACK来实现这一点,就像本章中的第一个例子那样。

Drupal版本:

将菜单项显示为标签

Drupal的公认晦涩的菜单行话来说,一个显示为标签的回调被认为是一个本地任务,它的菜单类型为MENU_LOCAL_TASK或者MENU_DEFAULT_LOCAL_TASK.本地任务的标题应该是一个简短的动词,比如“添加”或者“列出”。它通常作用在一些对象上,比如节点,或者用户。你可以把一个本地任务想象为一个关于菜单项的语义声明,通常显示为一个标签(tab)----这和<strong>标签类似,后者也是一个语义声明,通常用来显示加粗的文本。

 
    为了显示标签,本地任务必须有一个父菜单项。一个常用的实践是将一个回调指定到一个根路径上,比如milkshake,然后将本地任务指定到扩展了该路径的子路径上,比如milkshake/prepare,milkshake/drink,等等。Drupal内建的主题支持两级标签本地任务。(底层系统可以支持多级的标签,但是为了显示更多的层级,你需要让你的主题为此提供支持。)
 
    标签的显示顺序是由菜单项标题的字母顺序决定的。如果这种顺序不是你想要的,那么你可以为你的菜单项添加一个weight键,然后它们将按照重量进行排序。
 
    下面的例子所示的代码将会生成了两个主标签和位于默认本地任务下面的两个次标签。创建sites/all/modules/custom/milkshake/milkshake.info文件,如下所示:
 
; $Id$
name = Milkshake
description = Demonstrates menu local tasks.
package = Pro Drupal Development
core = 6.x
 
    接着,创建sites/all/modules/custom/milkshake/milkshake.module文件:
<?php
// $Id$
 
/**
* @file
* Use this module to learn about Drupal's menu system,
* specifically how local tasks work.
*/
 
/**
* Implementation of hook_perm().
*/
function milkshake_perm() {
    return array('list flavors', 'add flavor');
}
 
/**
* Implementation of hook_menu().
*/
function milkshake_menu() {
    $items['milkshake'] = array(
        'title' => 'Milkshake flavors',
        'access arguments' => array('list flavors'),
        'page callback' => 'milkshake_overview',
        'type' => MENU_NORMAL_ITEM,
    );
    $items['milkshake/list'] = array(
        'title' => 'List flavors',
        'access arguments' => array('list flavors'),
        'type' => MENU_DEFAULT_LOCAL_TASK,
        'weight' => 0,
    );
    $items['milkshake/add'] = array(
        'title' => 'Add flavor',
        'access arguments' => array('add flavor'),
        'page callback' => 'milkshake_add',
        'type' => MENU_LOCAL_TASK,
        'weight' => 1,
    );
    $items['milkshake/list/fruity'] = array(
        'title' => 'Fruity flavors',
        'access arguments' => array('list flavors'),
        'page callback' => 'milkshake_list',
        'page arguments' => array(2), // Pass 'fruity'.
        'type' => MENU_LOCAL_TASK,
    );
    $items['milkshake/list/candy'] = array(
        'title' => 'Candy flavors',
        'access arguments' => array('list flavors'),
        'page callback' => 'milkshake_list',
        'page arguments' => array(2), // Pass 'candy'.
        'type' => MENU_LOCAL_TASK,
    );
    return $items;
}
 
function milkshake_overview() {
    $output = t('The following flavors are available...');
    // ... more code here
    return $output;
}
 
function milkshake_add() {
    return t('A handy form to add flavors might go here...');
}
 
function milkshake_list($type) {
    return t('List @type flavors', array('@type' => $type));
}
    图 4-14给出了在Bluemarine主题下的效果图
4-14.本地任务和标签化菜单
 
    注意,页面的标题来自于父回调,而不是来自于默认本地任务。如果你想使用一个不同的标题,那么可以使用drupal_set_title()来设置它。
 
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

隐藏已有菜单项

老葛的Drupal培训班 Think in Drupal

如何隐藏已有的菜单项?只需要修改它们的链接项的hidden属性就可以了。假定由于一些原因,你想要删除“创建内容”这个菜单项。那么可以使用我们熟悉的hook_menu_link_alter():
/**
* Implementation of hook_menu_link_alter().
*/
function menufun_menu_link_alter(&$item, $menu) {
    // Hide the Create content link.
    if ($item['link_path'] == 'node/add') {
        $item['hidden'] = 1;
    }
}
 

Drupal版本:

使用menu.module

启用Drupal的菜单模块,就可以为管理员提供一个方便的用户界面,来定制已有的菜单,比如导航菜单或者一级/二级链接菜单,或者来添加新菜单。当位于includes/menu.inc中的menu_rebuild()函数运行时,表示菜单树的数据结构将被存储在数据库中。当你启用模块时,当你禁用模块时,或者当其它的一些能够影响菜单树结构的事情发生时,都会运行menu_rebuild()函数。数据将被保存到数据库的menu_router表中,而关于链接的信息则被保存到了menu_links表中。

 
    在为一个页面构建链接的流程期间,Drupal首先会基于从模块的菜单钩子实现中获取的路径信息和从menu_router表中获取的路径信息,来构建菜单树;接着,它会使用来自于数据库的菜单信息对前面的信息进行覆盖。这样一来,你就可以使用menu.module来修改菜单树的父亲、路径、标题、和描述了----实际上你并没有真正的修改底层的菜单树,而是创建了一些数据来覆盖在菜单树的上面。
 
注意 菜单树类型,比如MENU_CALLBACK 或者DEFAULT_LOCAL_TASK,在数据库中是以数字的形式存储的。
 
    menu.module还在节点表单上加了一部分,用来将当前文章添加为一个菜单项。
老葛的Drupal培训班 Think in Drupal
 

Drupal版本:

常见错误

现在你刚刚在你的模块中实现了菜单钩子,但是你的回调没有响应,你的菜单没有显示,或者就是不能正常工作,也就是出了问题。那么对于下面这些地方,你需要好好的检查一下:

• 你的access callback键所指的函数,返回的是否为FALSE?
• 在你菜单钩子的结尾处,你是不是忘记添加return $items;一行了?
• 你是不是不小心将access arguments或者page arguments的值设置为了一个字符串,而不是一个数组?
• 你是不是忘记了清空你的菜单缓存并重建菜单了?
• 如果你想通过将菜单类型指定为MENU_LOCAL_TASK从而将其显示为标签,那么你有没有指定一个带有回调函数的父菜单项?
• 如果你使用的是本地任务,那么在一个页面上的标签是不是少于两个阿(为了显示,必须多于等于两个)?
老葛的Drupal培训班 Think in Drupal
 

Drupal版本:

总结

老葛的Drupal培训班 Think in Drupal

当读完这一章后,你应该可以:

URL映射到函数上
理解访问控制的工作原理
理解如何在路径中使用通配符
创建带有标签(本地任务)的页面,标签能映射到函数上
通过代码来修改已有的菜单项
更多阅读,可参看menu.inc中的注释。当然,还可参看http://drupal.org/node/102338http://api.drupal.org/?q=api/group/menu/6
 

Drupal版本:

第5章 Drupal的数据库层

老葛的Drupal培训班 http://zhupou.cn

     Drupal的正常工作依赖于数据库。在Drupal内部,在你的代码与数据库之间存在着一个轻量级的数据库抽象层。在本章中,你将学习这一数据库抽象层是如何工作的,以及如何使用它。你将看到如何通过模块来修改查询语句。接着,你将看到如何;连接其它的数据库(比如一个遗留数据库)。最后,你将学习Drupal的模式API,以在你模块的.install文件中包含数据库表的创建和更新语句。
 

Drupal版本:

定义数据库参数

在建立数据库连接时,通过查看你站点的settings.php文件,Drupal就会知道需要连接哪个数据库以及所用的用户名和密码。这个文件一般位于sites/example.com/settings.php 或者sites/default/settings.php。定义数据库连接的代码,如下所示:

 
$db_url = 'mysql://username:password@localhost/databasename';
 
    这个例子中使用的是MySQL数据库。使用PostgreSQL的用户需要将前缀“mysql”替换为“pgsql”。显然,这里使用的用户名和密码对于你的数据库来说必须是有效的。它们是数据库的有效证件,但不是Drupal的,在你使用数据库工具建立数据库帐号时就可创建它们(用户名和密码)。Drupal的安装器会向你询问用户名和密码(如果没有预先设置的话),这样它就会为你构建settings.php文件中的$db_url字符串。
老葛的Drupal培训班 Think in Drupal
 

Drupal版本:

理解数据库抽象层

老葛的Drupal培训班 Think in Drupal

使用一个数据库抽象层API时,你可能感觉不到它的好,直到有一天你决定不再使用它的时候,你才能发现它的全部优点。你是否曾经遇到过这样的项目,它需要修改数据库系统的代码,你花费了大量的时间,通过仔细的审查每段代码,来将它们改为特定数据库的函数和查询?有了数据库抽象层,你就不需要再考虑不同数据库系统之间函数名的细微差别,只要你使用的是符合ANSI SQL的语句,那么你就不再需要为不同的数据库编写单独的查询语句了。举例来说,在Drupal中没有直接使用mysql_query()或者pg_query(),而是使用的db_query(),这样就将业务层和数据库层隔离了开来。
  Drupal的数据库层是轻量级的,它主要用于两个目的。第一个目的是使你的代码不会绑定在特定的数据库上。第二个目的是清理用户向查询语句中提交的数据,以阻止SQL注入攻击。这一层是建立在以下原理之上的:使用sql比重新学习一门新的抽象层的语言更方便。
    Drupal还有一个模式API,它允许你以一种通用的方式向Drupal描述你的数据库模式(也就是,你将使用哪些表和字段),然后让Drupal将其翻译成所用数据库的特定sql语句。当我们学习.install文件时,将会对其进行详细的讨论。
    通过检查你的settings.php文件内部的变量$db_url,Drupal来判定要连接的数据库的类型。例如,如果$db_url的开始部分为“mysql”,那么Drupal将包含进来includes/database.mysql.inc. 如果$db_url的开始部分为“pgsql”,那么Drupal将包含进来includes/database.pgsql.inc.图5-1给出了这一机制。
    作为一个例子,让我们比较一下db_fetch_object()在MySQL和PostgreSQL抽象层中不同之处:
// From database.mysqli.inc.
function db_fetch_object($result) {
if ($result) {
return mysql_fetch_object($result);
}
}
// From database.pgsql.inc.
function db_fetch_object($result) {
if ($result) {
return pg_fetch_object($result);
}
}
    如果你所用的数据库还未被支持,你可以通过为你的数据库实现相应的包装函数来建立你自己的数据库驱动器。更多详细,可参看本章最后部分的”编写你自己的数据库驱动器”。
图5-1 通过检查变量$db_url,Drupal判定需要包含进来哪个数据库文件。
   

Drupal版本:

连接到数据库

老葛的Drupal培训班 Think in Drupal

作为Drupal的正常的引导指令流程的一部分,Drupal将会自动的建立数据库连接,所以你不需要为此担心。
    如果你需要在Drupal外部使用数据库连接(比如,你在编写一个单独的PHP脚本或者有段处于Drupal之外的PHP代码,它们需要访问Drupal的数据库),那么可以使用下面的方式。
// Make Drupal PHP's current directory.
chdir('/full/path/to/your/drupal/installation');
 
// Bootstrap Drupal up through the database phase.
include_once('./includes/bootstrap.inc');
drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE);
 
// Now you can run queries using db_query().
$result = db_query('SELECT title FROM {node}');
...
 
警告Drupal中,通常会在sites目录下配置多个文件夹,这样站点从测试迁移到线上时就不用修改数据库密码信息了。例如,对于测试数据库服务器,你可以使用sites/staging.example.com/settings.php文件来放置数据库的密码信息,而对于在线的数据库服务器,你可以使用sites/www.example.com/settings.php文件。在建立本节所示的连接时,因为这里没有涉及到HTTP请求,所以Drupal总是使用sites/default/settings.php文件。
 

Drupal版本:

执行简单的查询

老葛的Drupal培训班 Think in Drupal

Drupal的函数db_query()是用来为已建立的数据库连接执行查询语句的。这些查询语句包括SELECT, INSERT, UPDATE, 和 DELETE
 
    当你编写SQL语句的时候,你需要注意一些特定于Drupal的语法。首先,表名应放在花括号之间,这样以来,在需要的情况下,就可以为表名添加前缀了,从而保证表名的唯一性。虚拟主机托管商常常会限制了用户的数据库个数,而这一约定则可以让用户在已有的数据库上安装Drupal,通过在他们的settings.php文件中声明数据库前缀来避免表名的冲突。下面是一个简单查询的例子,用来取回角色2的名字:
$result = db_query('SELECT name FROM {role} WHERE rid = %d', 2);
 
 注意,占位符%d的使用。在Drupal中,查询语句通常会使用占位符,而实际的值则作为参数跟在后面。占位符%d将被后面参数值(在这里就是2)自动的替换掉。占位符越多,那么参数就会越多,两者是相对应的:
 
db_query('SELECT name FROM {role} WHERE rid > %d AND rid != %d', 1, 7);
 
在数据库中执行时,前面的一行将转化为如下形式:
 
SELECT FROM role WHERE rid > 1 and rid != 7
 
    用户提交的数据应该作为单独的参数传入,这样这些值就可以被清理,从而阻止SQL注入攻击。Drupal使用printf语法(参看http://php.net/printf)来实现占位符对查询语句中实际值的替换。根据用户提交信息的数据类型,可以选用不同的占位符。
5-1列出了数据库查询的占位符及其含义。
 
5-1数据库查询的占位符及其含义
占位符         含义
------------------------------------------------------------------------------
%s              字符串
%d              整数
%f              浮点数
%b              二进位数据;不要包含在' '
%%     插入一个%符 (比如,SELECT * FROM {users} WHERE name LIKE '%%%s%%')
 
    db_query()的第一个参数总是查询语句本身。剩下的参数都是一些动态值,用来验证和入到查询字符串中。可以将这些值放在一个数组中,或者将每个值都作为一个独立的参数。后者更常用一些。
    我们应该注意到,使用这个语法,TRUE, FALSE 和NULL将会被自动转换为了等价的数字形式(0或1)。一般情况下,都不会因此出现问题。
    让我们看一些例子。在这些例子中,我们使用一个名为joke的数据库表,它包含了3个字段:节点ID(整数),版本ID(整数),还有包含笑话妙语的文本字段(关于joke模块的更多信息,参看第7章)。
    让我们从一个简单的查询入手。 从joke表中取出所有字段的所有记录,需要满足的条件为----字段vid的整数值等于$node->nid的值:
 
db_query('SELECT * FROM {joke} WHERE vid = %d', $node->vid);
 
    向joke表中插入一行记录。新纪录中包含两个整数和一个字符串值。注意字符串值的占位符位于单引号中;这将帮助阻止SQL注入攻击。由于查询语句本身包含了单引号,所以我们在查询的外面使用了双引号:
db_query("INSERT INTO {joke} (nid, vid, punchline) VALUES (%d, %d, '%s')",
$node->nid, $node->vid, $node->punchline);
 
    修改joke表中的所有记录,需要满足的条件为----字段vid的整数值等于$node->nid的值。通过设置字段puchline等于$node->punchline包含的字符串值来修改所有的这些记录:
db_query("UPDATE {joke} SET punchline = '%s' WHERE vid = %d", $node->punchline,
$node->vid);
 
    从joke表中删除所有记录,需要满足的条件为----字段vid的整数值等于$node->nid的值:
db_query('DELETE FROM {joke} WHERE nid = %d', $node->nid);

 

Drupal版本:

取回查询结果

有多种方式用于取回查询结果,这依赖于你的需求,你是需要单独的一行还是需要整个结果集,或者你打算获得一定范围内的结果集,是为了内部使用还是想将其分页显示。

 
获得单个值
如果你需要的仅仅是来自数据库的单个值,那么你可以使用db_result()来取回该值。下面是一个例子,用来取回未被管理员禁用的注册用户总数(不包含匿名用户):
$count = db_result(db_query('SELECT COUNT(uid) FROM {users} WHERE status = 1
AND uid != 0'));
 
获得多行
在大多数情况下,你需要从数据库中返回的都是多个字段。下面是一个典型的迭代模式,用于遍历整个结果集:
$type = 'blog';
$status = 1; // In the node table, a status of 1 means published.
$sql = "SELECT * FROM {node} WHERE type = '%s' AND status = %d";
$result = db_query(db_rewrite_sql($sql), $type, $status);
while ($data = db_fetch_object($result)) {
    $node = node_load($data->nid);
    print node_view($node, TRUE);
}

    前面的代码片段将输出类型为blog的所有已发布节点(表node中的字段status的值,为0时意味着未发布,为1时意味着已发布)。我们接下来就会讲解db_rewrite_sql()。函数db_fetch_object()从结果集中取出一行作为一个对象。如果想将取出的结果作为一个数组的话,那么可以使用db_fetch_array()。前者更为常用,因为与数组相比,大多数开发者都绝前者的语法更简明一些。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

获得限制范围内的结果

你可能会想,对于一个有很多日志条目的站点,比如说有10,000个,那么运行前面的代码将会非常危险。我们将对这个语句的结果进行限制,仅仅取回10个最新发布的日志:

$type = 'blog';
$status = 1; // In the node table, a status of 1 means published.
$sql = "SELECT * FROM {node} n WHERE type = '%s' AND status = %d ORDER BY
n.created DESC";
$result = db_query_range(db_rewrite_sql($sql), $type, $status, 0, 10);
while ($data = db_fetch_object($result)) {
    $node = node_load($data->nid);
    print node_view($node, TRUE);
}
    我们没有将语句传递给db_query()并使用LIMIT条件语句,在这里我们使用了函数db_query_range()。为什么呢?因为并非所有的数据库都支持LIMIT语法,所以我们需要使用db_query_range()作为包装函数。
    注意,我们将这些用来填充占位符的变量放在了范围的前面(也就是将type和status放在了0,10之前,如前面的例子所示)。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

将结果分页显示

老葛的Drupal培训班 Think in Drupal

我们可以使用一个更好的方式来显示这些日志:分页显示。我们可以使用Drupal的分页器来实现这一点(如图5-2)。让我们再次取回所有的日志条目,只是这次我们将其进行分页显示,在页面的底部,包含了指向其它结果页面的链接和“first和“last”的链接。
 
$type = 'blog';
$status = 1;
$sql = "SELECT * FROM {node} n WHERE type = '%s' AND status = %d ORDER BY
n.created DESC";
$pager_num = 0; // This is the first pager on this page. We number it 0.
$result = pager_query(db_rewrite_sql($sql), 10, $pager_num, NULL, $type,
    $status);
while ($data = db_fetch_object($result)) {
    $node = node_load($data->nid);
    print node_view($node, TRUE);
}
// Add links to remaining pages of results.
print theme('pager', NULL, 10, $pager_num);
 
    虽然pager_query()实际上不属于数据库抽象层,但是当你需要创建一个带有导航的分页显示时,它还是很有用的。最后一行调用的是theme('pager'),它用来显示指向其它页面的导航链接,你不需要向theme('pager')中传递结果的总数,因为总数在调用pager_query()时已被记录下来了。
 
图 5-2. Drupal的分页器,为结果集包含了内置的导航链接
 

Drupal版本:

模式(Schema)API

(译者注:Schema被翻译成了模式, Schema module 翻译成了模式模块, schema definition翻译成了模式定义,这里面的句子有点绕口^_^)

    通过数据库抽象层, Drupal可以支持多个数据库(MySQL, PostreSQL,等等) 。对于那些需要创建自己的数据库表的模块,可以使用模式定义来向Drupal描述表结构。接着,Drupal将定义翻译成适合数据库的语法。
 
使用模块的.install文件
    我们在第2章中已经看到,当我们编写的模块需要创建一个或者多个数据库表来存储信息时,创建和维护表结构的指令都放在了模块的.install文件中。
老葛的Drupal培训班 Think in Drupal

Drupal版本:

创建数据库表

老葛的Drupal培训班 Think in Drupal

安装钩子函数一般将数据库表的安装委托给drupal_install_schema();而drupal_install_schema()负责从模块的模式钩子中获取模式定义,并修改数据库,如图5-3所示。接着,安装钩子函数再做一些其它的必需的安装工作。下面是来自modules/book/book.install文件的例子,这里将数据库表的安装委托给了drupal_install_schema()。由于书籍模块需要处理书籍节点类型,所以在安装完数据库表后它还创建了这一节点类型。
 
/**
* Implementation of hook_install().
*/
function book_install() {
    // Create tables.
    drupal_install_schema('book');
 
    // Add the node type.
    _book_install_type_create();
}
 
    模式一般这样定义:
 
$schema['tablename'] = array(
    // Table description.
    'description' => t('Description of what the table is used for.'),
        'fields' => array(
            // Field definition.
            'field1' => array(
                'type' => 'int',
                'unsigned' => TRUE,
                'not null' => TRUE,
                'default' => 0,
                'description' => t('Description of what this field is used for.'),
            ),
        ),
        // Index declarations.
        'primary key' => array('field1'),
    );
 
 
5-3.使用模式定义创建数据库表
 

Drupal版本:

创建数据库表(1)

老葛的Drupal培训班 Think in Drupal

让我们看一下Drupal的书籍模块中的模式定义,位于modules/book/book.install文件中:
/**
* Implementation of hook_schema().
*/
function book_schema() {
    $schema['book'] = array(
        'description' => t('Stores book outline information. Uniquely connects each node in the outline to a link in {menu_links}'),
        'fields' => array(
            'mlid' => array(
                'type' => 'int',
                'unsigned' => TRUE,
                'not null' => TRUE,
                'default' => 0,
                'description' => t("The book page's {menu_links}.mlid."),
            ),
            'nid' => array(
                'type' => 'int',
                'unsigned' => TRUE,
                'not null' => TRUE,
                'default' => 0,
                'description' => t("The book page's {node}.nid."),
            ),
            'bid' => array(
                'type' => 'int',
                'unsigned' => TRUE,
                'not null' => TRUE,
                'default' => 0,
                'description' => t("The book ID is the {book}.nid of the top-level                  page."),
            ),
        ),
 
        'primary key' => array('mlid'),
        'unique keys' => array(
            'nid' => array('nid'),
        ),
        'indexes' => array(
            'bid' => array('bid'),
        ),
    );
 
    return $schema;
}
 
    这个模式定义描述了book表,它包含3个int类型的字段。它还有一个主键,一个唯一索引(这意味着该字段中的所有条目都是唯一的)和一个普通索引。注意,在字段描述中,引用另一个表中的字段时,需要为其使用花括号。这样模式模块(参看下一节)可以为表的描述构建方便的超链接。
 

Drupal版本:

使用模式模块

老葛的Drupal培训班 Think in Drupal

现在你可能会想,“花这么大的功夫,创建一个这么复杂的数组,来向Drupal描述我的表结构,是不是有点得不偿失啊?”不要着急。你用用模式模块就知道了,你可以从http://drupal.org/project/schema下载到该模块,接着将其启用。导航到“管理➤站点构建 ➤模式”,点击Inspect(检查)标签,你就能够看到所有数据库表的模式定义了。如果你为你的数据库表准备好了SQL脚本,那么使用模式模块就可以帮你自动生成模式定义,接着将模式定义复制粘贴到你的.install文件中就可以了。
 
提示 你一般很少需要从头编写一个模式定义。一般,你可以使用已有的表,使用模式模块的Inspect(检查)标签,让它帮你构建模式定义。
 
    模式模块还允许你查看任意模块的模式。如图5-4所示,在模式模块中显示了书籍模块的模式。注意,表和字段描述中,花括号中的表的名字被转化为了有用的链接。
 
5-4.模式模块显示了书籍模块的模式。
 

Drupal版本:

从模式向数据库的字段类型映射

在模式定义中声明的字段类型,将会映射成数据库中的本地字段类型。例如,一个size为tiny的整数字段将映射为MySQL中的TINYINT字段,或者PostgreSQL中的smallint字段。实际的映射可查看数据库驱动文件中的db_type_map()函数,比如includes/database.pgsql.php(参看表5-2, 本章后面讲到)。

 
文本型
    文本型字段是用来包含文本的。
 
Varchar
    Varchar,也就是变长字符字段;对于长度小于256字符的文本,通常使用这一字段类型。最大的字符长度,可以使用length键定义。MySQL中 varchar 字段的长度为0–255字符(MySQL 5.0.2 及更早版本)和0–65,535字符(MySQL 5.0.3及以后版本);而PostgreSQL中varchar字段的长度则可以更大一些。
 
$field['fieldname'] = array(
    'type' => 'varchar', // Required.
    'length' => 255, // Required.
    'not null' => TRUE, // Defaults to FALSE.
    'default' => 'chocolate', // See below.
    'description' => t('Always state the purpose of your field.'),
);
 
    如果default键未被设置,并且not null键被设置为了FALSE,那么默认值将被设置为NULL。
 
Char
    Char字段是定长字符字段,该字段的字符长度,可以使用length键定义。MySQL中char字段的长度为0–255字符。
$field['fieldname'] = array(
    'type' => 'char', // Required.
    'length' => 64, // Required.
    'not null' => TRUE, // Defaults to FALSE.
    'default' => 'strawberry', // See below.
    'description' => t('Always state the purpose of your field.'),
);
 
    如果default键未被设置,并且not null键被设置为了FALSE,那么默认值将被设置为NULL。
 
Text
Text字段用于大块的文本。例如,node_revisions表(存储节点正文的)中的body字段就是这种类型。Text字段可以不使用默认值。
 
$field['fieldname'] = array(
    'type' => 'text', // Required.
    'size' => 'small', // tiny | small | normal | medium | big
    'not null' => TRUE, // Defaults to FALSE.
    'description' => t('Always state the purpose of your field.'),
);
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

数字型

老葛的Drupal培训班 Think in Drupal

数字型数据类型是用来存储数字的,它包括integer(整数), serial(序列数), float(浮点数), 和numeric(数字)类型。
Integer
    这个字段是用来存储整数的,比如节点ID。如果unsigned键为TRUE的话,那么将不允许使用负整数。
 
$field['fieldname'] = array(
    'type' => 'int', // Required.
    'unsigned' => TRUE, // Defaults to FALSE.
    'size' => 'small', // tiny | small | medium | normal | big
    'not null' => TRUE, // Defaults to FALSE.
    'description' => t('Always state the purpose of your field.'),
);
 
Serial
    一个序列字段是用来保存自增数字的。例如,当添加一个节点时,node表中的nid字段将会自增。通过插入一行记录和调用db_last_insert_id()来实现自增。如果在插入记录和取回最后ID之间,另一线程也插入了一条记录,此时会不会出错呢?由于它是基于单连接追踪的,所以还会返回正确的ID。一个序列字段必须被索引;通常会把它作为主键进行索引。
$field['fieldname'] = array(
    'type' => 'serial', // Required.
    'unsigned' => TRUE, // Defaults to FALSE. Serial numbers are usually positive.
    'size' => 'small', // tiny | small | medium | normal | big
    'not null' => TRUE, // Defaults to FALSE. Typically TRUE for serial fields.
    'description' => t('Always state the purpose of your field.'),
);
 
Float
    浮点数字是用来存储浮点数据类型的。对于浮点数字来说,tiny, small, medium, 和normal型浮点一般是没有区别的;另外,big型浮点用来声明双精度字段。
 
$field['fieldname'] = array(
    'type' => 'float', // Required.
    'unsigned' => TRUE, // Defaults to FALSE.
    'size' => 'normal', // tiny | small | medium | normal | big
    'not null' => TRUE, // Defaults to FALSE.
    'description' => t('Always state the purpose of your field.'),
);
 
Numeric
    数字数据类型允许你声明数字的精度和小数位数。精度指的是数字的有效数字位数。小数位数指的是小数点右边的数字位数。例如,123.45的精度为5,小数位数为2。这里不使用size键。到目前为止,Drupal核心中还没有用到该字段。
 
$field['fieldname'] = array(
    'type' => 'numeric', // Required.
    'unsigned' => TRUE, // Defaults to FALSE.
    'precision' => 5, // Significant digits.
    'scale' => 2, // Digits to the right of the decimal.
    'not null' => TRUE, // Defaults to FALSE.
    'description' => t('Always state the purpose of your field.'),
);

Drupal版本:

日期和时间: Datetime

老葛的Drupal培训班 Think in Drupal

Drupal核心没有使用这一数据类型,它使用的是存放在整数字段中的Unix时间戳。Datetime格式是一个包含了日期和时间的混合格式。
 
$field['fieldname'] = array(
    'type' => 'datetime', // Required.
    'not null' => TRUE, // Defaults to FALSE.
    'description' => t('Always state the purpose of your field.'),
);
 
二进位:Blob
    二进位大型对象数据类型用于存储二进制数据(例如,Drupal的cache表用来存储缓存数据)。二进位数据包括音乐,图片,或者视频。有两个尺寸可用,normal 和big。
 
$field['fieldname'] = array(
    'type' => 'blob', // Required.
    'size' => 'normal' // normal | big
    'not null' => TRUE, // Defaults to FALSE.
    'description' => t('Always state the purpose of your field.'),
);
 

Drupal版本:

使用mysql_type声明特定字段类型

老葛的Drupal培训班 Think in Drupal

如果你知道你的数据库引擎的准确字段类型,那么你可以在你的模式定义中使用mysql_type (或者 pgsql_type)键.这将覆写该数据库引擎的type和size键。例如,MySQL有一个名为TINYBLOB的字段类型,专门用于小一点的二进位大对象。如果对于MySQL,我们为其使用TINYBLOB类型,而对于其它的数据库引擎,我们则为其使用普通的BLOB类型,那么在Drupal中该如何声明呢?答案如下所示:
 
$field['fieldname'] = array(
    'mysql_type' > 'TINYBLOB', // MySQL will use this.
    'type' => 'blob', // Other databases will use this.
    'size' => 'normal', // Other databases will use this.
    'not null' => TRUE,
    'description' => t('Wee little blobs.')
);
 
MySQL和PostgreSQL的本地类型,如表5-2所示。
 
5-2.如何将模式定义中的Type和Size键映射到本地的数据库类型
 
   模式定义            本地数据库字段类型
类型       尺寸       MySQL         PostgreSQL
varchar     normal      VARCHAR         varchar
char        normal      CHAR            character
text        tiny        TINYTEXT        text
text        small       TINYTEXT        text
text        medium      MEDIUMTEXT      text
text        big         LONGTEXT        text
text        normal      TEXT            text
serial      tiny        TINYINT         serial
serial      small       SMALLINT        serial
serial      medium      MEDIUMINT       serial
serial      big         BIGINT          bigserial
serial      normal      INT             serial
int         tiny        TINYINT         smallint
int         small       SMALLINT        smallint
int         medium      MEDIUMINT       int
int         big         BIGINT          bigint
int         normal      INT             int
float       tiny        FLOAT           real
float       small       FLOAT           real
float       medium      FLOAT           real
float       big         DOUBLE          double precision
float       normal      FLOAT           real
numeric     normal      DECIMAL         numeric
blob        big         LONGBLOB        bytea
blob        normal      BLOB            bytea
datetime    normal      DATETIME        timestamp
 

Drupal版本:

维护数据库表

老葛的Drupal培训班 Think in Drupal

当你为一个模块创建新版本时,你可能需要修改数据库模式。可能,你添加了一列,或者为某一列添加了索引。由于该表已经包含了数据,所以你不能简单地删除并重建该表。下面给出了如何保证平稳的修改数据库表:
 
1. 更新你的.install文件中hook_schema()的实现,这样模块的新用户安装时,用的就是新模式了。你的.install文件中的模式定义,应该总是最新的,以反映你模块的表和字段的当前结构。
2. 通过写一个更新函数,让已有用户对现有模块进行更新。更新函数按照顺序进行命名,起始数字一般是基于Drupal版本的。例如,Drupal6的第一个更新函数可以为modulename_update_6000(),那么第二个更新函数就为modulename_update_6001()。下面是来自modules/comment/comment.install中的例子,这里向评论表中的父ID(pid)列添加了一个索引:
 
/**
 * Add index to parent ID field.
 */
function comment_update_6003() {
    $ret = array(); // Query results will be collected here.
    // $ret will be modified by reference.
    db_add_index($ret, 'comments', 'pid', array('pid'));
    return $ret;
}
 
    在更新了模块以后,用户运行http://example.com/update.php时,就会调用这个函数。
 
 
警告 因为,你每次添加一个表,字段,或者索引时,都会修改hook_schema()实现中的模式定义,所以你的更新函数千万不要使用这里的模式定义。你可以把hook_schema()实现中的模式定义看成是当前的,而把更新函数中的模式看成是过去的。参看http://drupal.org/node/150220
 
用来处理模式的函数的完整列表,可参看http://api.drupal.org/api/group/schemaapi/6
 
提示 Drupal会追踪一个模块当前所用的模式版本。这一信息存储在system表中。在运行完本节所示的更新以后,评论模块对应记录中的schema_version的值就变成了6003。为了让Drupal忘记该项,可以使用devel模块中的“Reinstall Modules”(重装模块)选项,或者从system表中删除该模块的记录。
 

Drupal版本:

在Uninstall中删除数据库表

老葛的Drupal培训班 Think in Drupal

当一个模块被禁用时,该模块存储在数据库中的数据还被保留着,如果哪天管理员改变了主意,还可以重新安装该模块。在“管理➤站点构建 ➤模块”页面,有一个卸载标签,是用来从数据库中删除数据的。对于你模块的数据库表的删除,也放在这里进行,只需要在模块的.install文件中实现uninstall(卸载)钩子就可以了。同时,你可能还想删除你在模块中定义的各种变量。下面是第2章里annotation模块中的例子:
 
/**
 * Implementation of hook_uninstall().
 */
function annotate_uninstall() {
    // Use schema API to delete database table.
    drupal_uninstall_schema('annotate');
    // Clean up our entry in the variables table.
    variable_del('annotate_nodetypes');
}
 

Drupal版本:

使用hook_schema_alter()修改已有模式

老葛的Drupal培训班 Think in Drupal

一般来说,模块会创建和使用它们自己的数据库表。但是,如果你的模块想修改一个已有的表时,那该怎么办呢?假定你的模块需要向node表中添加一列。最简单的方式是直接访问你的数据库,在里面添加一列。但是这样以来,反映实际数据库表的模式定义就会出现不兼容的问题。这里有一个更好的方式,那就是使用hook_schema_alter()。
 
警告 hook_schema_alter()是Drupal中的新钩子,对于如何使用这个钩子,什么才是最佳的用法,还存在争论。更多详细,参看http://api.drupal.org/api/group/hooks/6
 
    假定你有一个模块,想按照某种方式来标记节点,一般来说,你可以创建一个数据库表,并使用节点ID将其与node表关联起来,但是你没有这样做,出于性能的考虑,你决定完全使用node表。这样一来,你的模块需要做两件事情:在你模块的安装过程中修改node表,并且修改模式定义以如实地反映数据库中的表结构。前者可以使用hook_install(),后者可以使用hook_schema_alter()。假定你的模块为markednode.module,那么在你的markednode.install文件中应该包含以下函数:
 
/**
 * Implementation of hook_install().
 */
function markednode_install() {
    $field = array(
        'type' => 'int',
        'unsigned' => TRUE,
        'not null' => TRUE,
        'default' => 0,
        'initial' => 0, // Sets initial value for preexisting nodes.
        'description' => t('Whether the node has been marked by the
            markednode module.'),
    );
 
    // Create a regular index called 'marked' on the field named 'marked'.
    $keys['indexes'] = array(
        'marked' => array('marked')
    );
 
    $ret = array(); // Results of the SQL calls will be stored here.
    db_add_field($ret, 'node', 'marked', $field, $keys);
}
 
/**
 * Implementation of hook_schema_alter(). We alter $schema by reference.
 *
 * @param $schema
 * The system-wide schema collected by drupal_get_schema().
 */
function markednode_schema_alter(&$schema) {
    // Add field to existing schema.
    $schema['node']['fields']['marked'] = array(
        'type' => 'int',
        'unsigned' => TRUE,
        'not null' => TRUE,
        'default' => 0,
        'description' => t('Whether the node has been marked by the
            markednode module.'),
    );
}

Drupal版本:

使用drupal_write_record()进行插入和更新

老葛的Drupal培训班 Think in Drupal

程序员常遇到的一个问题,就是处理数据库中新纪录的插入和已有记录的更新。代码一般都会先检查当前的操作是一个插入操作还是一个更新操作,接着再执行合适的操作。
 
    因为Drupal所用的每个表都使用模式来描述,所Drupal知道一个表中都包含哪些字段以及每个字段的默认值。通过向drupal_write_record()传递一个包含了字段和数值的数组,那么你就可以让Drupal为你生成和执行SQL了,这样你就不需要自己手写了。
 
    假定你有一个表,用来追踪你收集的小兔子。那么你模块中的用来描述表结构的模式钩子应该是这样的:
 
/**
 * Implementation of hook_schema().
 */
function bunny_schema() {
    $schema['bunnies'] = array(
        'description' => t('Stores information about giant rabbits.'),
        'fields' => array(
            'bid' => array(
                'type' => 'serial',
                'unsigned' => TRUE,
                'not null' => TRUE,
                'description' => t("Primary key: A unique ID for each bunny."),
            ),
            'name' => array(
                'type' => 'varchar',
                'length' => 64,
                'not null' => TRUE,
                'description' => t("Each bunny gets a name."),
            ),
            'tons' => array(
                'type' => 'int',
                'unsigned' => TRUE,
                'not null' => TRUE,
                'description' => t('The weight of the bunny to the nearest ton.'),
            ),
        ),
        'primary key' => array('bid'),
        'indexes' => array(
            'tons' => array('tons'),
        ),
    );
 
    return $schema;
}
 
    插入一条新纪录非常方便,更新记录也是如此:
 
$table = 'bunnies';
$record = new stdClass();
$record->name = t('Bortha');
$record->tons = 2;
drupal_write_record($table, $record);
 
// The new bunny ID, $record->bid, was set by drupal_write_record()
// since $record is passed by reference.
watchdog('bunny', 'Added bunny with id %id.', array('%id' => $record->bid));
 
// Change our mind about the name.
$record->name = t('Bertha');
 
// Now update the record in the database.
// For updates we pass in the name of the table's primary key.
drupal_write_record($table, $record, 'bid');
watchdog('bunny', 'Updated bunny with id %id.', array('%id' => $record->bid));
 
    这里也支持数组,如果$record是一个数组的话,那么drupal_write_record()会在内部将其转化为一个对象。
 

Drupal版本:

使用hook_db_rewrite_sql()将查询暴露给其它模块

老葛的Drupal培训班 Think in Drupal

这个钩子可以用来修改Drupal中任何地方的查询,这样你就不用直接修改相关模块了。如果你将一个查询传递给db_query(),而且你相信其他人可能想修改它,那么你就需要把它包装到函数db_rewrite_sql()里面,这样其他的开发者就可以访问它了。当执行一个这样的查询时,它首先检查所有实现了hook_db_rewrite_sql()的模块,并给它们一个修改查询的机会。例如,节点模块修改了节点列表查询,从而将受节点访问规则保护的节点排除在外。
 
 
警告 如果你执行一个节点列表查询(例如,你直接对node表查询,来获取所有节点的一些子集),但是你没有使用db_rewrite_sql()来包装你的查询,那么节点访问规则将被忽略,这是由于节点模块无法修改你的查询,因此无法排除受保护的节点。
 
如果查询语句不是你的,但是你又想在你的模块中修改这一查询,那么你需要在你的模块中实现hook_db_rewrite_sql()。
 
    表5-3 使用SQL重写的两种方式的总结
 
5-3.使用db_rewrite_sql()函数VS使用hook_db_rewrite_sql()钩子
名称                           什么时候使用
db_rewrite_sql()                     当编写节点列表查询或者其它查询时,你想让别人能够对它进行修改它时
hook_db_rewrite_sql()       当你想修改其它模块中的查询时
 

Drupal版本:

使用hook_db_rewrite_sql()

老葛的Drupal培训班 Think in Drupal

下面是函数签名:
function hook_db_rewrite_sql($query, $primary_table = 'n', $primary_field = 'nid',
$args = array())
 
参数如下所示:
• $query:可被覆写的SQL查询。
• $primary_table: 在该查询中,包含主键字段的表的名字或者别名。例如,对于node表它的值为n,而对于comment表它的值为c (例如, 对于 SELECT nid FROM {node} n, 该值应为 n)。常用的值如表5-4所示。
• $primary_field:在该查询中的主字段的名称。它的值可为nid, tid,vid, cid。(例如,如果你的查询要得到一列节点ID,那么主字段就为nid)。
• $args:一个包含了参数的数组,用来传递给每个模块中hook_db_rewrite_sql()的实现。
 
5-4. $primary_table别名的常用值
            别名
blocks          b
comments        c
forum           f
node            n
menu            m
term_data       t
vocabulary      v
 

Drupal版本:

修改其它模块的查询

让我们看一个hook_db_rewrite_sql()的具体实现。下面的例子利用了node表中moderate列来覆写节点查询。在我们修改了查询以后,那些不具有“管理内容”权限的用户,就会看不到处于待审核状态的节点(也就是,moderate列为1)。

 
/**
 * Implementation of hook_db_rewrite_sql().
 */
function moderate_db_rewrite_sql($query, $primary_table, $primary_field, $args) {
switch ($primary_field) {
case 'nid':
// Run only if the user does not already have full access.
if (!user_access('administer content')) {
$array = array();
if ($primary_table == 'n') {
// Node table is already present;
// just add a WHERE to hide moderated nodes.
$array['where'] = "(n.moderate = 0)";
}
// Test if node table is present but alias is not 'n'.
elseif (preg_match('@{node} ([A-Za-z_]+)@', $query, $match)) {
$node_table_alias = $match[1];
 
// Add a JOIN so that the moderate column will be available.
$array['join'] = "LEFT JOIN {node} n ON $node_table_alias.nid = n.nid";
 
// Add a WHERE to hide moderated nodes.
$array['where'] = "($node_table_alias.moderate = 0)";
}
return $array;
}
}
}
 
注意,我们检查所有查询,对于主键为nid的并且主表为node的查询,我们向里面插入一些额外信息。让我们看一下实际效果。
 
下面是最初的查询,未经moderate_db_rewrite_sql()处理的:
 
SELECT * FROM {node} n WHERE n.type = 'blog' and n.status = 1
 
下面是moderate_db_rewrite_sql()处理过后的查询:
 
SELECT * FROM {node} n WHERE n.type = 'blog' and n.status = 1 AND n.moderate = 0
 
moderate_db_rewrite_sql()被调用后,它向输入的查询中追加了AND n.moderate = 0。这个钩子通常还用于限制对节点、词汇表、术语、或者评论的访问。
db_rewrite_sql()局限于它能够理解的SQL语法。当你需要对表进行关联时,使用JOIN语法,而不是在FROM语句中对表进行关联。
 
下面的不正确:
 
SELECT * FROM {node} AS n, {comment} AS c WHERE n.nid = c.nid
 
这个正确:
 
SELECT * FROM {node} n INNER JOIN {comment} c ON n.nid = c.nid
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

在Drupal中使用多个数据库连接

老葛的Drupal培训班 Think in Drupal

数据库抽象层给我们带来了多项好处,比如函数名称更好记了,查询中内置了安全特性,等等。有时候,我们需要连接到第3方或者遗留的数据库上,如果Drupal的数据库API能满足这一需要并同时提供安全特性的话,那该多美啊。不错,我们可以实现这一点!例如,在你的模块中,你可以连接到一个非Drupal的数据库,并从中取出数据。
 
在settings.php文件中,$db_url既可以是一个字符串(通常是这样的),也可以是包含多个数据库连接的字符串数组。下面是默认的语法,声明了一个单独的连接:
$db_url = 'mysql://username:password@localhost/databasename';
 
当使用一个数组时,它的键就是在激活数据库连接时所引用的简洁名称,而它的值就是连接的字符串本身。下面是一个例子,在这里我们声明了两个连接字符串, default(默认的)和legacy(遗留的):
 
$db_url['default'] = 'mysql://user:password@localhost/drupal6';
$db_url['legacy'] = 'mysql://user:password@localhost/legacydatabase';
 
注意 Drupal本身使用的数据库一定要以default为键。
 
    当你需要连接到Drupal中其它的数据库上时,你首先使用它的键名激活该连接,当你使用完连接时,将它切换回到默认的连接上。
 
// Get some information from a non-Drupal database.
db_set_active('legacy');
$result = db_query("SELECT * FROM ldap_user WHERE uid = %d", $user->uid);
 
// Switch back to the default connection when finished.
db_set_active('default');
 
注意 切记一定要切换回到默认的连接上,这样Drupal可以干净的完成整个请求生命周期并重新回到自己的表中。
 
    由于数据库抽象层设计的是为每个数据库使用相同的函数名,所以不能够同时使用多个数据库后台(比如,同时使用MySQL 和 PostgreSQL)。然而,在同一个站点还是可以同时使用MySQL 和 PostgreSQL连接的,如何实现的更多详细,请参看http://drupal.org/node/19522
 

Drupal版本:

使用临时表

老葛的Drupal培训班 Think in Drupal

如果你需要做很多的处理,那么在请求过程中你可能需要创建一个临时表。你可以通过调用函数db_query_temporary()来完成它,如下所示:
$result = db_query_temporary($sql, $arguments, $temporary_table_name);
 
       接下来你就可以使用临时表的名字来对临时表进行查询。对于临时表的名字,推荐的一种命名方式是:“temp”+你的模块名+具体名字。
$final_result = db_query('SELECT foo FROM temp_mymodule_nids');
 
    注意,对于临时表,不需要使用花括号来对表进行前缀化,这是因为临时表的是暂时存在的,它不能经过表的前缀化处理。与之相对应的,永久的表的名字应该使用花括号,以支持表的前缀化。
 
注意Drupal核心中没有使用临时表,而Drupal所用的数据库用户也有可能没有权限来创建临时表。因此,模块的作者不应该假定所有的用户都具有该权限。
 

Drupal版本:

编写你自己的数据库驱动器

假定你想为一个新生的未来的名为DNAbase的数据库编写一个数据库抽象层,该数据库使用分子计算来提升性能。我们不需要从头开始,而是复制一份已有的抽象层,接着修改它。我们将使用PostgreSQL的实现,这是因为MySQL的驱动器被拆分成了,一个includes/database.mysql-common.inc文件,和两个单独的mysql、mysqli驱动器文件。

首先,我们复制一份includes/database.pgsql.inc并将其重命名为includes/database.dnabase.inc。接着我们修改每个包装函数的内部逻辑,使用DNAbase的功能来代替PostgreSQL的功能。当我们完成了所有的这些修改以后,那么在我们的文件中声明了以下函数:
 
_db_query($query, $debug = 0)
db_add_field(&$ret, $table, $field, $spec, $new_keys = array())
db_add_index(&$ret, $table, $name, $fields)
db_add_primary_key(&$ret, $table, $fields)
db_add_unique_key(&$ret, $table, $name, $fields)
db_affected_rows()
db_change_field(&$ret, $table, $field, $field_new, $spec, $new_keys = array())
db_check_setup()
db_column_exists($table, $column)
db_connect($url)
db_create_table_sql($name, $table)
db_decode_blob($data)
db_distinct_field($table, $field, $query)
db_drop_field(&$ret, $table, $field)
db_drop_index(&$ret, $table, $name)
db_drop_primary_key(&$ret, $table)
db_drop_table(&$ret, $table)
db_drop_unique_key(&$ret, $table, $name)
db_encode_blob($data)
db_error()
db_escape_string($text)
db_fetch_array($result)
db_fetch_object($result)
db_field_set_default(&$ret, $table, $field, $default)
db_field_set_no_default(&$ret, $table, $field)
db_last_insert_id($table, $field)
db_lock_table($table)
db_query_range($query)
db_query_temporary($query)
db_query($query)
db_rename_table(&$ret, $table, $new_name)
db_result($result)
db_status_report()
db_table_exists($table)
db_type_map()
db_unlock_tables()
db_version()
 
通过更新settings.php中的$db_url,我们在Drupal中连接到DNAbase数据库来测试这一系统。它看起来是这样的:
$db_url = 'dnabase://john:secret@localhost/mydnadatabase';
 
其中john是用户名,secret是密码,而mydnadatabase是我们将要连接的数据库名。你可能还想创建一个测试模块,来直接调用这些函数以确保它们正常工作。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

总结

老葛的Drupal培训班 Think in Drupal

读完本章后,你应该能够
•  理解Drupal的数据库抽象层
•  进行基本的查询
•  从数据库中获取单个或者多个结果
• 获取一个限定范围内的结果
• 使用分页器
• 理解Drupal的模式API
•  编写其它开发者可以修改的查询
• 干净的修改其它模块中的查询
•  连接多个数据库,包括遗留的数据库
•  编写一个抽象层驱动器
 

Drupal版本:

第6章 Drupal用户

老葛的Drupal培训班 Think in Drupal

用户是使用Drupal的重要原因。Drupal可以帮助用户创建一个在线社区,在上面大家可以一起协作,交流。在本章中,我们将揭开Drupal用户内幕,看一下如何验证用户,用户的登陆,还有用户的内部表示。首先,我们从检查对象$user是什么以及它是如何构造的开始。然后我们进一步讲述用户注册,用户登录,用户验证的流程。最后我们讲述了如何将一个现有的验证系统例如LDAP(轻量级目录访问协议)和Pubcookie与Drupal集成。
 

Drupal版本:

对象$user

老葛的Drupal培训班 Think in Drupal

用户为了登录,必须启用cookies。一个关闭了cookie的用户仍然可以以匿名的身份与Drupal进行交互。
     在引导指令流程的会话阶段,Drupal创建了一个全局$user对象,用来作为当前用户的标识。如果用户没有登录(这样就没有一个会话cookie),那么它将被当作匿名用户对待。创建匿名用户的代码如下所示(位于bootstrap.inc):
 
function drupal_anonymous_user($session = '') {
    $user = new stdClass();
    $user->uid = 0;
    $user->hostname = ip_address();
    $user->roles = array();
    $user->roles[DRUPAL_ANONYMOUS_RID] = 'anonymous user';
    $user->session = $session;
    $user->cache = 0;
    return $user;
}
 
    另一方面,如果用户当前登录了,那么可以通过使用用户的ID来关联表users和sessions,以创建对象$user。两个表中的所有字段都被放到了对象$user中。
 
注意:用户的ID是在用户注册时或者管理员创建用户时所分配的一个整数。这个ID是users表中的主键。
 
    通过向index.php中添加代码global $user;print_r($user);可以很容易的查看$user对象。下面是一个登录用户对象$user的通常结构:
 
stdClass Object (
    [uid]       => 2
    [name]    => Joe Example
    [pass]      => 7701e9e11ac326e98a3191cd386a114b
    [mail]      => joe@example.com
    [mode]      => 0
    [sort]      => 0
    [threshold] => 0
    [theme]     => bluemarine
    [signature] => Drupal rocks!
    [created]  => 1201383973
    [access]    => 1201384439
    [login]     => 1201383989
    [status]    => 1
    [timezone] => -21600
    [language] =>
    [picture]  => sites/default/files/pictures/picture-1.jpg
    [init]      => joe@example.com
    [data]      =>
    [roles]     => Array ( [2] => authenticated user )
    [sid]       => fq5vvn5ajvj4sihli314ltsqe4
    [hostname] => 127.0.0.1
    [timestamp] => 1201383994
    [cache]     => 0
    [session]  => user_overview_filter|a:0:{}
)
 
在上面显示的$user对象中,斜体字段意味着数据来自于sessions表。表6-1解释了$user对象的组成部分:
 
6-1 $user的组成部分
组成                     描述
来自于表users
uid               用户的ID.它是表users的主键,并在Drupal中是唯一的。
name              用户的用户名,当用户登录时输入
pass              用户的MD5哈希密码,当用户登录时进行对比。由于没有保存用户的原始真实密码,所以密码只能被重置,不能被恢复。
mail              用户当前的email地址
 
mode,sort和       特定于用户的评论浏览喜好
threshold
 
theme             如果启用了多个主题,这个代表用户选择的主题。如果用户主题未被安装,Drupal将其转到站点的默认主题。
signature         用户进入他/她的账号页面时所使用的签名。当用户添加一个评论时使用,并且只有当评论模块(comment module)启用时才可见。
created           用户账号创建时的Unix时间戳
access            用户最近一次访问的Unix时间戳
login             用户最近一次成功登录的Unix时间戳
status            1表示良好,0表示被拒绝访问的用户
timezone          用户时区与GMT之间的差异,以秒为单位
language          用户的默认语言。只有在站点上启用了多语言,并且用户通过编辑账号喜好选择了一个语言时,才不为空。
picture           与用户账号相联系的图像文件的路径
init              用户注册时提供的初始email地址
data              由模块存储的任何数据都可放置在这里(参看下一节,“向$user对象存储数据”)
来自于表user_roles
roles             分配给当前用户的角色
来自于表session
sid               通过PHP分配给当前用户会话的会话ID
hostname          用户浏览当前页面时所使用的IP地址
timestamp         一个Unix时间戳,表示用户的浏览器最后一次接收一个完整页面的时间
cache             一个用于per-user caching(参看 includes/cache.inc)的时间戳
session           在用户会话期间,模块可以向这里存储任意的数据。
 

Drupal版本:

向对象$user中存储数据

老葛的Drupal培训班 Think in Drupal

users包含了一个名为data的字段,用于存储保存序列化数组中的额外信息。如果你向对象$user中添加自己的数据,可以通过使用user_save()将数据存储在这一字段上。
 
// Add user's disposition.
global $user;
$extra_data = array('disposition' => t('Grumpy'));
user_save($user, $extra_data);
 
    现在,对象$user拥有了一个永久属性:
global $user;
print $user->disposition;
 
Grumpy
 
    尽管这种方式很方便,但是在用户登录和初始化对象$user时,由于以这种方式存储的数据需要反序列化,这样会增加更多的开销。因此,不加考虑就向$user中放入大量的数据将会引起一个性能瓶颈。一个可选的并且是更好的方法是,在$user加载时向它添加属性方法,将会在“在加载时向对象$user添加数据”一节进行讨论。
 

Drupal版本:

测试用户是否登录了

老葛的Drupal培训班 Think in Drupal

在请求期间,测试用户是否登录的标准方式,是检查$user->uid是否为0。Drupal有个名为user_is_logged_in()的函数可用来检查登录用户(还有一个相应的user_is_anonymous()函数用来检查匿名用户):
if (user_is_logged_in()) {
    $output = t('User is logged in.');
else {
    $output = t('User is an anonymous user.');
}

Drupal版本:

hook_user()入门

老葛的Drupal培训班 Think in Drupal

在你的模块中实现钩子hook_user(),它可以使你能够对用户账号进行不同的操作,以及修改$user对象。让我们看一下这个函数的签名:
 
function hook_user($op, &$edit, &$account, $category = NULL)
 
参数$op用来描述对用户账号所进行的当前操作,它可以有多个不同的值:
 
• after_update:在$user被保存到数据库中以后调用。
 
• categories:返回一个关于分类的数组,当用户编辑帐号时,这些分类被当作Drupal菜单本地任务(一般显示为可点击的标签)。这些实际上就是Drupal的菜单项。实现示例可参看profile.module中的profile_categories()。
 
• delete: 刚刚从数据库中删除了一个用户。这给了模块一个机会,用来从数据库中删除与该用户相关的信息。
 
• form:在被显示的用户编辑表单中,插入一个额外的表单字段元素。
 
• insert:新的用户帐号的记录已被保存到数据库中;将要保存$user->data和分配角色。之后,将会加载完整的$user对象。
 
• load:已成功加载用户帐号。模块可以向$user对象添加额外的信息(通过参数$account的引用传递,将信息传递给用户钩子)。
 
• login:用户已经成功登录。
 
• logout:用户刚刚退出登录,他/她的会话已被销毁。
 
• register:用户帐号注册表单将被显示。模块可以向表单中添加额外的表单元素。
 
• submit: 用户编辑表单已被提交。在帐号信息发送给user_save()以前,可对其进行修改。
 
• update: 已存在的用户帐号(修改后)将要被保存到数据库中。
 
• validate: 用户帐号已被修改。模块应该验证它的自定义数据,并生成必要的错误消息。
 
• view: 正在显示用户的帐号信息。模块应该返回它要显示的自定义信息,作为$user->content的结构化元素。查看操作最后调用theme_user_profile()来生成用户个人资料页面(更多详细可参看下面一节)。
 
     参数$edit是一个数组,当用户帐号正被创建或者更新时,所提交的表单数值构成了这一数组。注意它是通过引用传递的,所以你对它所做的任何修改都将修改实际的表单数值。
 
    $account对象(实际上就是一个$user对象)也是通过引用传递的,所以你对它所做的任何修改都将修改实际的$user信息。
 
    参数$category是正被编辑的当前用户帐号的分类。可以把分类看作用户相关的单独的信息组合。例如,如果你登录drupal.org,访问你的“我的账号”页面,并点击编辑标签,你将看到单独的分类,比如账号设置,个人信息,邮件订阅等等。
 
警告 不要混淆了hook_user()中的参数$account和全局变量$user对象。参数$account是当前正被操作的帐号的用户对象。而全局变量$user对象是当前登录的用户。通常,但不是绝对,两者是一样的。
 

Drupal版本:

理解hook_user('view')

老葛的Drupal培训班 Think in Drupal

模块可以使用hook_user('view')来向用户个人资料页面添加信息(例如,你在http://example.com/?q=user/1看到的;参看图6-1)
6-1 用户个人资料页面,这里日志模块和用户模块通过实现hook_user('view')来向该页面添加额外信息
 
    让我们看一下日志模块是如何向这一页面添加它的信息的:
 
/**
* Implementation of hook_user().
*/
function blog_user($op, &$edit, &$user) {
    if ($op == 'view' && user_access('create blog entries', $user)) {
        $user->content['summary']['blog'] = array(
            '#type' => 'user_profile_item',
            '#title' => t('Blog'),
            '#value' => l(t('View recent blog entries'), "blog/$user->uid",
                array('title' => t("Read @username's latest blog entries.",
                array('@username' => $user->name)))),
            '#attributes' => array('class' => 'blog'),
        );
    }
}
    查看操作向$user->content中加入了一些信息.用户资料信息组织成类别,而每个类别表示一个页面,里面包含了关于用户的信息.在图6-1中,只有一个名为History的类别.外部的数组的键应该对应于类别名,在前面的例子中,键的名字为summary,它对应于History类别(诚然,如果键的名字与类别名相同的话,那么会更好一些).内部数组应该有一个唯一的文本键(在这里为blog),它有4个元素#type, #title, #value, 和 #attributes。类型user_profile_item的主题层对应于modules/user/user-profile-item.tpl.php。通过对比代码片断与图6-1,你可以看到这些元素是如何显示的。图6-2给出了$user->content数组的内容,它生成了如图6-1的页面。
6-2. $user->content的结构
 
    你的模块还可以通过实现hook_profile_alter(),在个人资料项被主题化以前,来操作个人资料项。下面这个的例子,简单的从你的用户个人资料页面删除了日志项目。我们这里假定这个函数是模块hide.module中的:
 
/**
* Implementation of hook_profile_alter().
*/
function hide_profile_alter(&$account) {
    unset($account->content['summary']['blog']);
}
 

Drupal版本:

用户注册流程

默认情况下,Drupal站点的用户注册,只需要一个用户名和一个有效的e-mail地址就可以了。模块可以通过实现用户钩子,来向用户注册表单中添加它们自己的字段。让我们编写一个名为legalagree.module的模块,它提供了一个便捷的方式,以使你的站点适应今天这个法制社会。

 
    首先在sites/all/modules/custom创建一个名为legalagree的文件夹,并向legalagree目录中添加一下文件(参看列表6-1和6-2)。接着通过“管理➤站点构建➤模块”来启用模块。
 
列表 6-1. legalagree.info
; $Id$
name = Legal Agreement
description = Displays a dubious legal agreement during user registration.
package = Pro Drupal Development
core = 6.x
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

用户注册流程(1)

列表 6-2. legalagree.module

<?php
// $Id$
 
/**
 * @file
 * Support for dubious legal agreement during user registration.
 */
 
/**
 * Implementation of hook_user().
 */
function legalagree_user($op, &$edit, &$user, $category = NULL) {
     switch($op) {
     // User is registering.
          case 'register':
              // Add a fieldset containing radio buttons to the
              // user registration form.
              $fields['legal_agreement'] = array(
                   '#type' => 'fieldset',
                   '#title' => t('Legal Agreement')
              );
              $fields['legal_agreement']['decision'] = array(
                   '#type' => 'radios',
                   '#description' => t('By registering at %site-name, you agree that at any time, we (or our surly, brutish henchmen) may enter your place of residence and smash your belongings with a ball-peen hammer.',array('%site-name' => variable_get('site_name', 'drupal'))),
                   '#default_value' => 0,
                   '#options' => array(t('I disagree'), t('I agree'))
          );
          return $fields;
    
     // Field values for registration are being checked.
     case 'validate':
          // Make sure the user selected radio button 1 ('I agree').
          // The validate op is reused when a user updates information on
          // the 'My account' page, so we use isset() to test whether we are
          // on the registration page where the decision field is present.
          if (isset($edit['decision']) && $edit['decision'] != '1') {
              form_set_error('decision', t('You must agree to the Legal Agreement before registration can be completed.'));
          }
          break;
    
     // New user has just been inserted into the database.
     case 'insert':
          // Record information for future lawsuit.
          watchdog('user', t('User %user agreed to legal terms', array('%user' => $user->name)));
          break;
     }
}
 
    在注册表单创建期间,在表单验证期间,还有在用户记录被插入到数据库中以后,都要调用user钩子。我们这个简单的模块将生成类似于图6-2所示的注册表单。
 
6-2 一个修改了的用户注册表单
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

使用profile.module来收集用户信息

老葛的Drupal培训班 Think in Drupal

如果你想扩展用户注册表单以收集用户信息,在你打算编写自己的模块以前,你可以先试用一下profile.module。它允许你创建任意的用来收集数据的表单,在用户注册表单上定义信息是否是必须的(或者收集的),指定信息是公开的还是私有的。另外,它允许管理员定义页面,这样就可以根据用户的个人资料选项,使用一个由“站点URL” +profile/” + “个人资料字段的名字” +“值“构建的URL,来查看用户了。
 
    例如,如果你定义了一个名为profile_color的文本字段,你可以使用http://example.com/?q=profile/profile_color/black,来查看所有选择了黑色作为他们喜欢颜色的用户。或者假定你正在创建一个会议网站,并负责为参加者计划宴会。你可以定义一个名为profile_vegetarian的复选框,以作为个人资料字段,并可在http://example.com/?q=profile/profile_vegetarian(注意,对于复选框字段,值已被隐含,因此这里忽略了它)查看所有的素食用户;也就是说,这里没有向URL中追加一个值,而在前面的profile_color后面,则追加了black。
    在Drupal官方网站http://drupal.org上可以找到一个实际中的例子,参加马萨诸塞州波士顿2008年Drupal会议的用户列表,可以使用地址 http://drupal.org/profile/conference-boston-2008来查看(这里,字段名前面没有加前缀“profile_ “)。
 
提示 只有在个人资料字段设置中填充了字段Page的标题时,个人资料总结页面的自动创建才正常工作,但它不适用于textarea,URL,或者日期字段。
 

Drupal版本:

登录流程

当用户填完登录表单(一般位于http://example.com/?q=user 或者在一个区块中)并点击登录按钮时,登录流程开始。

 
    登录表单的验证程序检查用户名是否被封了,无论是根据访问规则拒绝访问,还是由于用户输入了一个错误的用户名或密码。如果任何一种情况发生了,都会及时的通知用户,告诉他为什么无法登录。
 
注意 Drupal中可以使用本地和外部两种方式进行认证。外部认证系统的例子包括OpenID,LDAP, Pubcookie,以及其它。一种外部认证类型是分布式认证,在这里来自于一个Drupal站点的用户可以登录到另一个Drupal站点(参看site_network模块,http://drupal.org/project/site_network)。
 
    Drupal将首先尝试从本地登录,在users表中查找是否存在一个与用户名和密码哈希值相匹配的记录。本地登录成功,则会调用两个用户钩子(load 和 login),在你的模块中可以实现这些钩子,如图6-4所示。
 
6-4 本地用户登录的执行路径
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

在加载时,向$user对象添加数据

老葛的Drupal培训班 Think in Drupal

通过调用user_load(),从数据库中成功的加载一个$user对象时,将调用user钩子的加载(load)操作。当一个用户登录(或退出)时,当从一个节点取回作者信息时,或者一些其它情况时,都会调用这一加载(load)操作。
 
注意 由于触发user钩子非常耗费资源,所以在为一个请求实例化当前$user对象时(参看前面的“$user对象“部分),并没有调用user_load()。如果你正在编写你自己的模块,如果你调用的函数,需要完整的加载$user对象, 那么在该函数的前面,你首先需要调用user_load()函数,除非你能保证已经完整加载了$user对象。
 
    让我们创建一个名为“loginhistory”的模块,用来保存用户登录的历史记录。我们在用户的“我的帐号”页面显示用户已经登录的次数。在sites/all/modules/custom/下面创建一个名为loginhistory的文件夹,并添加列表6-3到6-5中的文件。首先是loginhistory.module。
 
列表 6-3. loginhistory.info
; $Id$
name = Login History
description = Keeps track of user logins.
package = Pro Drupal Development
core = 6.x
 

Drupal版本:

在加载时,向$user对象添加数据(1)

老葛的Drupal培训班 Think in Drupal

为了存储登录信息,我们需要使用一个.install文件来创建数据库表,所以我们创建了sites/all/modules/custom/loginhistory.install文件。
 
列表 6-4. loginhistory.install
<?php
// $Id$
 
/**
 * Implementation of hook_install().
 */
function loginhistory_install() {
    // Create tables.
    drupal_install_schema('loginhistory');
}
 
/**
 * Implementation of hook_uninstall().
 */
function loginhistory_uninstall() {
    // Remove tables.
    drupal_uninstall_schema('loginhistory');
}
 
/**
 * Implementation of hook_schema().
 */
function loginhistory_schema() {
    $schema['login_history'] = array(
        'description' => t('Stores information about user logins.'),
        'fields' => array(
            'uid' => array(
                'type' => 'int',
                'unsigned' => TRUE,
                'not null' => TRUE,
                'description' => t('The {user}.uid of the user logging in.'),
            ),
            'login' => array(
                'type' => 'int',
                'unsigned' => TRUE,
                'not null' => TRUE,
                'description' => t('Unix timestamp denoting time of login.'),
            ),
        ),
        'index' => array('uid'),
    );
   
    return $schema;
}
 
列表 6-5. loginhistory.module
<?php
// $Id$
 
/**
 * @file
 * Keeps track of user logins.
 */
 
/**
 * Implementation of hook_user().
 */
function loginhistory_user($op, &$edit, &$account, $category = NULL) {
    switch($op) {
        // Successful login.
        case 'login':
            // Record timestamp in database.
            db_query("INSERT INTO {login_history} (uid, login) VALUES (%d, %d)",$account->uid, $account->login);
            break;
 
        // $user object has been created and is given to us as $account parameter.
        case 'load':
            // Add the number of times user has logged in.
            $account->loginhistory_count = db_result(db_query("SELECT COUNT(login) AS count FROM {login_history} WHERE uid = %d", $account->uid));
            break;
 
        // 'My account' page is being created.
        case 'view':
            // Add a field displaying number of logins.
            $account->content['summary']['login_history'] = array(
                '#type' => 'user_profile_item',
                '#title' => t('Number of Logins'),
                '#value' => $account->loginhistory_count,
                '#attributes' => array('class' => 'login-history'),
                '#weight' => 10,
            );
            break;
    }
}
 
    在安装了这个模块以后,对于每次成功的用户登录,都将调用user钩子的login操作,在这个钩子里面,模块将向数据库表login_history插入一条记录。在加载$user对象时,将会调用用户加载钩子,此时模块将把用户的当前登录次数添加$user->loginhistory_count中。当用户查看“我的帐号”页面时,登录次数将显示出来,如图6-5所示。
6-5 追踪用户的登录历史
 
注意 当你在你的模块中为对象$user或$node添加属性时,在属性名前面最好加上前缀,以避免命名空间的冲突。这就是为什么这里使用$account->loginhistory_count来代替$account->count的原因。
 
 
    尽管在“我的帐号”页面,我们显示了我们添加到$user上的额外信息,记住由于$user对象是全局变量,所以其它模块也能访问它。我们留给读者一个非常有用的联系,为了安全起见,来修改前面的模块,在左(或右)边栏的区块中提供一个格式美观的历史登录列表(“喂!我今天上午3:00没有登录”)。
 

Drupal版本:

提供用户信息类别

老葛的Drupal培训班 Think in Drupal

如果你在http://drupal.org拥有一个帐号,首先登录并点击“我的帐号”链接,接着选择编辑标签,你就可以看到提供关于用户信息的类别的效果了。除了编辑你的帐号信息,比如你的密码以外,在其它类别中你还可以提供你的其它个人信息。在编写此书时,http://drupal.org支持编辑CVS信息、Drupal相关信息、个人信息、工作信息、以及接收新闻通讯的优选。
 
通过使用profile.module或者user钩子的categories操作,你可以添加像这些类别一样的信息类别;参看profile.module中的实现。
 

Drupal版本:

外部登录

 

有时候,你可能不想使用Drupal的本地users表。例如,可能你在另一个数据库中或者LDAP中,已经有了一个users表。在Drupal中,可以很方便的将外部认证集成到登录流程中来。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

简单的外部认证(1)

老葛的Drupal培训班 Think in Drupal

让我们实现一个非常简单的外部认证模块,来说明外部认证的工作原理。假定你的公司只雇用名为Dave的员工,而基于名字和姓氏来分配用户名。对于任何以dave开头的用户名都将通过该模块的认证,所以用户davebrown, davesmith, 和davejones都将能够成功的登录。我们在这里,将会使用form_alter()来修改用户登录验证处理器,这样就可以调用我们自己的验证处理器了。下面是sites/all/modules/custom/authdave/authdave.info文件:
 
; $Id$
name = Authenticate Daves
description = External authentication for all Daves.
package = Pro Drupal Development
core = 6.x

Drupal版本:

简单的外部认证(2)

老葛的Drupal培训班 Think in Drupal

而下面则是实际的authdave.module:
 
<?php
// $Id$
 
/**
 * Implementation of hook_form_alter().
 * We replace the local login validation handler with our own.
 */
function authdave_form_alter(&$form, $form_state, $form_id) {
    // In this simple example we authenticate on username only,
    // so password is not a required field. But we leave it in
    // in case another module needs it.
    if ($form_id == 'user_login' || $form_id == 'user_login_block') {
        $form['pass']['#required'] = FALSE;
       
        // If the user login form is being submitted, add our validation handler.
        if (isset($form_state['post']['name'])) {
            // Find the local validation function's entry so we can replace it.
            $array_key = array_search('user_login_authenticate_validate',
                $form['#validate']);
       
            if ($array_key === FALSE) {
                // Could not find it. Some other module must have run form_alter().
                // We will simply add our validation just before the final                                  //validator.
                $final_validator = array_pop($form['#validate']);
                $form['#validate'][] = 'authdave_login_validate';
                $form['#validate'][] = $final_validator;
            }
            else {
                // Found the local validation function. Replace with ours.
                $form['#validate'][$array_key] = 'authdave_login_validate';
            }
        }
    }
}
 
/**
 * Form validation handler.
 */
function authdave_login_validate($form, &$form_state) {
    global $user;
    if (!empty($user->uid)) {
        // Another module has already handled authentication.
        return;
    }
    // Call our custom authentication function.
    if (!authdave_authenticate($form_state['values'])) {
        // Authentication failed; username did not begin with 'dave'.
        form_set_error('name', t('Unrecognized username.'));
    }
}
 
/**
 * Custom authentication function. This could be much more complicated,
 * checking an external database, LDAP, etc.
 */
function authdave_authenticate($form_values) {
    global $authdave_authenticated;
    $username = $form_values['name'];
    if (substr(drupal_strtolower($username), 0, 4) == 'dave') {
        // Log user in, or register new user if not already present.
        user_external_login_register($username, 'authdave');
       
        // Write session, update timestamp, run user 'login' hook.
        user_authenticate_finalize($form_state['values']);
        // Use a global variable to save the fact that we did authentication.
        // (See use of this global in hook_user() implementation of next
        // code listing.)
        $authdave_authenticated = TRUE;
        return TRUE;
    }
    else {
        // Not a Dave.
        return FALSE;
    }
}
 
    图6-4给出了Drupal的本地登录流程。它包含了3个表单验证器:
 
• user_login_name_validate():如果用户名被封,或者访问规则(Administer >> User management >> Access rules)拒绝了该用户名或主机的访问,那么就设置一个表单错误信息。
 
• user_login_authenticate_validate():使用此用户名,密码,以及状态设置为1(也就是,没有被封的),来对users表进行查询,如果查询失败,则设置一个表单错误信息。
 

user_login_final_validate():如果没有成功的加载用户,那么设置一个错误信息“对不起,无效的用户名或密码。你是不是忘记密码了?”,并向系统写入一个日志记录“用户尝试登录失败”。

Drupal版本:

简单的外部认证(3)

老葛的Drupal培训班 Think in Drupal

聪明的读者将会注意到,如果同时启用了Drupal的本地认证和我们的外部认证,那么在插入操作下面,没有方式通过代码来区分用户究竟是通过本地认证的还是通过外部认证的;所以我们在这里聪明的使用了一个全局变量,来指示是我们的模块进行了认证。我们还对authmap表进行了查询,如下所示:
 
db_query("SELECT uid FROM {authmap} WHERE uid = %d AND module = '%s'", $account->uid, 'authdave');
 
    所有通过外部认证添加的用户,同时会在users表和authmap表中各有一行记录。然而,在这种情况下,由于在同一请求期间运行了认证和user钩子,所以在这里使用一个全局变量就可以很好的替代一个数据库查询了。
 
6-7. 外部用户登录/注册流程的详细

Drupal版本:

总结

老葛的Drupal培训班 Think in Drupal

读完本章后,你应该能够
• 理解用户在Drupal内部是如何表示的
• 理解如何使用不同的方式来存储与用户相关的信息
• 使用用户注册过程中的钩子,来获取一个正在注册的用户的更多信息。
• 使用用户登录过程中的钩子,在用户登录时调用你自己的代码
• 理解外部用户认证的工作原理
• 实现你自己的外部认证模块
 
    更多关于外部认证的信息,可参看openid.module(Drupal核心可选模块), 或者第3方模块pubcookie.module。
 
 
译者注:
profile,在这章沿用了简体中文包中“个人资料”的译法,在更多的地方,我使用了“轮廓”的译法。
 

Drupal版本:

第7章 Drupal节点

老葛的Drupal培训班 Think in Drupal

在本章中,我将介绍节点和节点类型。我将向大家展示使用两种不同方式创建一个节点类型。首先,我将向你介绍程序解决方案,也就是通过Drupal钩子函数编写模块来创建节点类型。这种方式,在定义节点可以做什么不可以做什么的时候,具有更高的自由度和灵活性。接着,我将向大家介绍如何通过Drupal后台管理接口来创建一个节点类型,并简单的讨论了内容创建工具集模块(CCK),Drupal社区正在逐步的将CCK的方式添加到Drupal核心中去。最后我们将研究一下Drupal的节点访问控制机制。
 
提示 开发者通常使用术语节点节点类型。而在Drupal的用户界面,分别将其称为posts(发布)和内容类型,这主要是为了让站点管理员能够更好的理解这些概念。
 

Drupal版本:

那么什么才是节点呢?

老葛的Drupal培训班 Think in Drupal

对于刚刚接触Drupal开发的新手来说,最先遇到的问题之一就是,什么是节点?一个节点就是一个内容片段。Drupal为每一片内容指定一个名为“节点 ID”(在代码中简写为$nid)的数字ID。一个每个节点还拥有一个标题,从而允许管理员通过标题来查看节点列表。
 
注意 如果你熟悉面向对象的话,那么你可以把每个节点类型看作一个类,把每个节点看做一个对象实例。然而,Drupal的代码不是100%面向对象的,为什么这样呢?这有一个很好的解释。(参看http://api.drupal.org/api/HEAD/file/developer/topics/oop.html)。在Drupal的将来版本中,如果需求合理的话,将会越来越倾向于使用面向对象技术,因为将来不再支持PHP4(它对面向对象的支持很弱)。
 
    有许多不同的节点或节点类型。常见的节点类型有“blog entry”(博客),“poll”(投票),和“book page”(书籍页面)。一般情况下(在本书中),术语“内容类型”和“节点类型”是同义的,尽管节点类型是一个更抽象的概念并且你可以把它看作基节点的派生,如图7-1所展示的。
 
 
    把所有的内容类型当作节点的好处是,这样就可以为它们使用相同的底层数据结构了。对于开发者来说,这意味着你可以对所有的内容以同样的代码方式进行多种操作。对于节点可以非常容易的进行一批操作,并且你还可以为自定义的节点类型添加许多额外的功能。由于所有的内容都是节点,其底层的数据结构和行为是一样的,所以Drupal内置的支持了对内容的搜索、创建、编辑和管理等操作。显然,该一致性对于终端用户也同样有用。由于创建、编辑和删除节点的表单拥有一个类似的外观,这样就既保持了一致性,并且用户界面更易于使用。
7-1 源于基本节点的节点类型和可能添加的字段
 

Drupal版本:

那么什么才是节点呢?(1)

老葛的Drupal培训班 Think in Drupal

通常通过为节点类型添加它们自己的属性,来扩展基本节点。节点类型poll存储了投票相关条目,如投票的有效期,投票当前是否可用,以及用户是否允许投票。节点类型forum为每个节点加载了分类术语,这样它就知道了它位于管理员定义的哪个论坛下面。节点类型blog,则与前面二者不同,它没有添加任何的其它的数据;替代的,通过为每个用户创建日志和为每个日志创建RSS种子,从而为数据添加了不同的视图。所有的节点都包括了下列属性,它们存储在表node和node_revisions中:
 
• nid:节点的唯一标识ID。
 
• vid:节点的唯一修订本ID,由于Drupal需要为每个节点存储内容修订本,所以该字段是必须的。在所有的节点和节点修订本中,vid是唯一的。
 
• type:每个节点都有一个节点类型;例如,blog, story, article, image等等。
 
• language:节点的语言。如果此列为空的话,那么就意味着该节点是语言中立的。
 
• title:节点的标题,一个简短的255位字符的字符串。如果通过代码将表node_type中的字段has_title设置为0的话,那么节点就没有标题了。
 
• uid:作者的用户ID。默认情况,每个节点都有一个唯一的作者。
 
• status: 0表示未发布;就是说,不具有 “管理节点” 权限的用户看不到它的内容。1意味着已发布,并且具有“管理节点”权限的用户可以看见它的内容。Drupal的节点级别的访问控制机制(可参看本章中的后面两节,“使用hook_access()来限制对一节点类型的访问”和“限制对节点的访问”)可以禁止已发布节点的显示。如果启用了搜索模块,那么可以使用搜索模块来对内容建立索引。
 
• created:节点创建时的Unix时间戳。
 
• changed:节点最后被修改的Unix时间戳。如果你是使用了节点修订本系统,那么它的值与表node_revisions中字段timestamp的值相同。
 
• comment:一个整数字段,用来描述节点的评论状态,它有3个可能值:
• 0:对当前节点禁用了评论。这是评论模块禁用时已有节点的默认值。在节点编辑表单的“评论设置”部分里的用户界面中,它对应于“已禁用”选项。
• 1:不能再向当前节点添加评论了。在节点编辑表单的“评论设置”部分里的用户界面中,它对应于“只读”选项。
• 2:可以查看评论,并且用户可以创建新的评论。评论模块负责控制着谁可以创建评论以及评论显示的外观。在节点编辑表单的“评论设置”部分里的用户界面中,它对应于“读/写”选项。
 
• promote:另一个整数字段,用来决定是否将节点显示在首页上,有两个值可用:
• 1:推到首页。节点将被推到你站点的默认首页上。该节点仍然会显示在它的普通页面上,例如http://example.com/?q=node/3。这里需要注意的是,由于你可以在“管理>>站点设置>>站点信息”中将首页改成你想要的那个页面,所以这里可能有点用词不当。更准确一点的说,页面http://example.com/?q=node将包含所有的promote字段为1的节点,而该页面在默认情况下为站点的首页。
• 0: 不将节点显示在http://example.com/?q=node中。
 
• moderate:一个整数字段,其中0表示禁用了审核,1表示启用了审核。下面是该字段的警告说明:在核心的Drupal安装中没有为该字段留下接口。换句话说就是,你可以反复的改变该字段的值,而默认情况下它不起任何作用。所以开发者可以根据它们的需要,将该字段用在各种功能中去。第3方模块,比如http://drupal.org/project/modr8http://drupal.org/project/revision_moderation,使用了该字段。
 
• sticky:当Drupal在一个页面中显示一列节点时,默认情况是将标记为“置顶”的节点列在前面,接着按照创建日期列出剩下的“不置顶”节点。换句话说就是“置顶”的节点位于节点列表的顶部。1表示“置顶”,0表示“不置顶”。你可以在同一列表中包含多个“置顶”节点。
 
• tnid:当一个节点作为另一个节点的翻译版本时,被翻译的源节点的nid将被存储在这里。例如,如果节点3的语言为英语,节点5是节点3的瑞典语翻译,那么节点5的tnid字段就为3。
 
• translate:有两个可选的值,1意味着翻译需要被更新;0意味着翻译是最新的。
 
如果你使用了Drupal的修订本系统,Drupal将创建一个内容的修订本,同时又追踪了谁在最后修改了节点。
 

Drupal版本:

不是所有的东西都是节点

用户、区块和评论不是节点。在这些特定的数据结构中,为了适应它们各自的特定目的,它们每一个都拥有自己的钩子系统。节点一般有“标题”和“正文”两部分,而在表示用户的数据结构中则不需要这些。用户需要的是,e-mail地址、用户名称、一种安全的存储密码的方式。当要存储的内容片段更小一些时,比如存的是导航菜单、搜索框、最新评论列表等等,我们此时可以使用轻量级的存储解决方案---区块。评论也不是节点,它们也属于轻量级的内容。一个页面可能会有100或者更多的评论,试想,如果所有的这些评论在被加载时都使用节点钩子系统的话,那么会给系统带来多大的负担呢.

    在过去,经常争论,用户或评论到底应不应该归结为节点,而一些第3方模块实际上实现了这一点。如果现在还对这个问题进行争论的话,那么就好比在编程风格上高呼“Emacs更好一些”一样。(译者注:我不知道Emacs什么意思^_^)。
 
老葛的Drupal培训班 Think in Drupal

Drupal版本:

创建一个节点模块

老葛的Drupal培训班 Think in Drupal

传统上,当你想在Drupal中创建一个新的内容类型时,你应该编写一个节点模块,由它来负责提供你的内容类型所需的新的且有趣的东西。我们之所说这是传统方式,这是因为Drupal框架最近常用的方式是,让你通过后台管理界面来创建内容类型,使用第3方模块来扩展这些内容类型的功能,而不是从头开始编写一个节点模块。在本章中,我们将讨论这两种方式。
    让我们编写一个节点模块,从而让用户可以为站点添加笑话。每一个笑话都包括一个标题,笑话本身,接着是一个笑话妙语(punchline)。你应该可以非常容易的使用内置的节点属性title来存储笑话的标题,用节点属性body来存储笑话内容,但是你还需要创建一个新的数据库表来存储笑话妙语。我们将通过使用.install文件来实现它。
 
    首先,让我们在目录sites/all/modules/custom下面创建一个名为joke的文件夹。

Drupal版本:

创建.install文件

老葛的Drupal培训班 Think in Drupal

你将需要在你的数据库表中存储一些信息。首先,你需要节点的ID,这样你就可以引用node_revisions表中的对应节点了,node_revisions表存储了节点的标题和主体。其次,你需要存储节点的修订本ID,这样你的模块就可以使用Drupal内置的修订本控制了。当然,你还需要存储笑话妙语。由于你已经知道了数据库的模式,让我们继续,创建joke.install文件并将其放到目录sites/all/modules/custom/joke下面。关于创建安装文件的更多信息,可参看第2章。
 
<?php
// $Id$
 
/**
 * Implementation of hook_install().
 */
function joke_install() {
    drupal_install_schema('joke');
}
 
/**
 * Implementation of hook_uninstall().
 */
function joke_uninstall() {
    drupal_uninstall_schema('joke');
}
 
/**
 * Implementation of hook_schema().
 */
function joke_schema() {
    $schema['joke'] = array(
        'description' => t("Stores punch lines for nodes of type 'joke'."),
        'fields' => array(
            'nid' => array(
                'type' => 'int',
                'unsigned' => TRUE,
                'not null' => TRUE,
                'default' => 0,
                'description' => t("The joke's {node}.nid."),
            ),
            'vid' => array(
                'type' => 'int',
                'unsigned' => TRUE,
                'not null' => TRUE,
                'default' => 0,
                'description' => t("The joke's {node_revisions}.vid."),
            ),
            'punchline' => array(
                'type' => 'text',
                'not null' => TRUE,
                'description' => t('Text of the punchline.'),
            ),
        ),
        'primary key' => array('nid', 'vid'),
        'unique keys' => array(
            'vid' => array('vid')
        ),
        'indexes' => array(
            'nid' => array('nid')
        ),
    );
 
    return $schema;
}
 

Drupal版本:

创建.info文件

老葛的Drupal培训班 Think in Drupal

让我们再创建一个joke.info文件并将其添加到joke文件夹下。
 
; $Id$
name = Joke
description = A content type for jokes.
package = Pro Drupal Development
core = 6.x
 

Drupal版本:

创建.module文件

老葛的Drupal培训班 Think in Drupal

最后,你需要创建模块文件本身。创建一个名为joke.module的文件并将其放到sites/all/modules/custom/joke下面。在你完成了这个模块以后,你可以在模块列表页面(管理➤站点构建 ➤模块)启用这一模块。你首先添加如下代码:
<?php
// $Id$
 
/**
 * @file
 * Provides a "joke" node type.
 */
 

Drupal版本:

提供我们节点类型的相关信息

老葛的Drupal培训班 Think in Drupal

现在你该向joke.module中添加钩子函数了。你想要实现的第一个钩子函数是hook_node_info().当Drupal发现有那些节点类型可用时,调用这个钩子函数。你将为你的自定义节点提供一些元数据。
/**
 * Implementation of hook_node_info().
 */
function joke_node_info() {
// We return an array since a module can define multiple node types.
// We're only defining one node type, type 'joke'.
return array(
'joke' => array(
'name' => t('Joke'), // Required.
'module' => 'joke', // Required.
'description' => t('Tell us your favorite joke!'), // Required.
'has_title' => TRUE,
'title_label' => t('Title'),
'has_body' => TRUE,
'body_label' => t('Joke'),
'min_word_count' => 2,
'locked' => TRUE
)
);
}
 
    由于在单个模块中可以定义多个节点类型,所以返回值应该是一个数组。下面是钩子hook_node_info()中可以提供的元数据的总结:
 
• name (必须的):显示在站点上的节点名称。例如,如果它的值为’Joke’,那么Drupal将使用该值,作为本节点提交表单的标题。
 
• module (必须的):Drupal要查找的回调函数的前缀名。我们这里使用了’joke’,所以Drupal将寻找以’joke’为前缀的回调函数,比如 joke_validate(), joke_insert(), joke_delete()等等。
 
• description:一般用于添加一个简短描述,以用来说明此内容类型可以干什么。该文本将会显示在“创建内容”页面的列表中(http://example.com/?q=node/add)。
 
• has_title:布尔值,用以说明此内容类型是否使用标题字段。默认为TRUE。
 
• title_label:在节点编辑表单中标题字段对应的文本标签。只有当has_title为TRUE时它才可见。默认为Title。
 
• has_body: 布尔值,用以说明此内容类型是否使用主体字段。默认为TRUE。
 
• body_label: 表单中body字段对应的文本标签。只有当has_body为TRUE时它才可见。默认为Body。
 
• min_word_count: body字段想要通过验证所需的最小单词个数。默认为0.(在我们的模块中,我们将其设置为2,以阻止一字笑话。)
 
• locked:布尔值,用以指示此内容类型的内部名称是否被锁定了;如果锁定了,那么站点管理员将不能修改它了;否则,管理员可以在“管理➤内容管理➤内容类型”中该内容类型的相应选项。默认为TRUE,这意味着名字被锁定,因此不可编辑。
 
注意: 在前面列表中提到的内部名称字段,是用来构造“创建内容”页面链接URL的。例如,我们使用“joke”作为我们节点类型的内部名称(它是我们返回的数组的键),如果要创建一个新的笑话的话,那么用户要访问页面http://example.com/?q=node/add/joke。通常你不需要通过将locked设置为FALSE,来对此作出修改。内部名称存储在表nodenode_revisions的“type”列中。
 

Drupal版本:

修改菜单回调

老葛的Drupal培训班 Think in Drupal

现在不需要实现钩子hook_menu()了(译者注:在Drupal5中,是必须的),在“创建内容”页面已经有了相应的链接。Drupal可以自动的发现你新创建的内容类型,并将它的条目添加到http://example.com/?q=node/add页面,如图7-2所示。而URL http://example.com/?q=node/add/joke就是指向节点提交表单的直接链接。这里的名字和描述都来源于你在joke_node_info()中所给的定义。
 
7-2.该内容类型出现在了http://example.com/node/add页面。
 
    如果你不想添加这个直接的链接,那么你可以使用hook_menu_alter()来移除它。例如,下面的代码,就可以为不具有“管理节点”权限的用户移除该页面。
 
/**
 * Implementation of hook_menu_alter().
 */
function joke_menu_alter(&$callbacks) {
    // If the user does not have 'administer nodes' permission,
    // disable the joke menu item by setting its access callback to FALSE.
    if (!user_access('administer nodes')) {
        $callbacks['node/add/joke']['access callback'] = FALSE;
        // Must unset access arguments or Drupal will use user_access()
        // as a default access callback.
        unset($callbacks['node/add/joke']['access arguments']);
    }
}
 

Drupal版本:

使用hook_perm()定义特定于节点类型的权限

老葛的Drupal培训班 Think in Drupal

一般情况下,由模块创建的节点类型的权限包括:创建该类型的一个节点,编辑你自己创建的节点,编辑该类型的任意节点。可以在hook_perm()中将它们定义为create joke, edit own joke, 和edit any joke,等等。你仍然需要在模块中定义这些权限。现在,让我们使用hook_perm()来创建这些权限:
 
/**
 * Implementation of hook_perm().
 */
function joke_perm() {
    return array('create joke', 'edit own joke', 'edit any joke', 'delete own joke','delete any joke');
}
    现在你可以导航到“管理➤用户管理 ➤访问控制”,你就可以看到你在上面定义的权限了,并且可以将它们分配给用户角色了。
 

Drupal版本:

使用hook_access()来限制对一个节点类型的访问

你在hook_perm()中定义了权限,但是它们是如何起作用的呢?节点模块可以使用hook_access()来限制对它们定义的节点类型的访问。超级用户(用户ID 1)将绕过所有的访问权限检查,所以对于超级用户则不会调这个钩子函数。如果没有为你的节点类型定义这个钩子函数,那么所有的访问权限检查都会失败,这样就只有超级用户和具有“管理节点”权限的用户,才能够创建、编辑、或删除该类型的内容。

 
/**
 * Implementation of hook_access().
 */
function joke_access($op, $node, $account) {
    $is_author = $account->uid == $node->uid;
    switch ($op) {
        case 'create':
            // Allow if user's role has 'create joke' permission.
            return user_access('create joke', $account);
 
        case 'update':
            // Allow if user's role has 'edit own joke' permission and user is
            // the author; or if the user's role has 'edit any joke' permission.
            return user_access('edit own joke', $account) && $is_author ||
                user_access('edit any joke', $account);
 
        case 'delete':
            // Allow if user's role has 'delete own joke' permission and user is
            // the author; or if the user's role has 'delete any joke' permission.
            return user_access('delete own joke', $account) && $is_author ||
                user_access('delete any joke', $account);
    }
}
 
    前面的函数允许具有“create joke”权限的用户创建一个笑话节点。如果用户还具有“edit own joke”权限并且他们是节点作者,或者如果他们具有'edit any joke'权限,那么他们还可以更新一个笑话。那些具有'delete own joke'权限的用户,他们可以删除自己创建的笑话;而具有'delete any joke'权限的用户,则可以删除joke类型的所有节点。
 

    在钩子函数hook_access()中, $op另一个可用的值是“view”(查看),它允许你控制谁可以查看该节点。然而我们需要提醒一下:当查看的页面仅有一个节点时,才调用钩子hook_access()。当节点处于摘要视图状态时,比如位于一个多节点列表页面,在这种情况下, hook_access()就无法阻止用户对该节点的访问。你可以创建一些其它的钩子函数,并直接操纵$node->teaser的值来控制对它的访问,但是这有点黑客的味道了。一个比较好的解决方案是,使用我们在后面即将讨论的函数hook_node_grants()hook_db_rewrite_sql()。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

为我们的节点类型定制节点表单

老葛的Drupal培训班 Think in Drupal

到目前为止,你已经为你的新节点类型定义了元数,还有定义了访问控制权限。接着,你需要构建节点表单,这样用户就可以输入笑话了。你可以通过实现hook_form()来完成这一工作:
 
/**
 * Implementation of hook_form().
 */
function joke_form($node) {
    // Get metadata for this node type
    // (we use it for labeling title and body fields).
    // We defined this in joke_node_info().
    $type = node_get_types('type', $node);
    $form['title'] = array(
        '#type' => 'textfield',
        '#title' => check_plain($type->title_label),
        '#required' => TRUE,
        '#default_value' => $node->title,
        '#weight' => -5,
        '#maxlength' => 255,
    );
    $form['body_filter']['body'] = array(
        '#type' => 'textarea',
        '#title' => check_plain($type->body_label),
        '#default_value' => $node->body,
        '#rows' => 7,
        '#required' => TRUE,
    );
    $form['body_filter']['filter'] = filter_form($node->format);
    $form['punchline'] = array(
        '#type' => 'textfield',
        '#title' => t('Punchline'),
        '#required' => TRUE,
        '#default_value' => isset($node->punchline) ? $node->punchline : '',
        '#weight' => 5
    );
    return $form;
}
 
注意:如果你不熟悉表单API的话,请参看第10章。
 
    作为站点管理员,如果你已经启用了你的模块,现在你就可以导航到“创建内容➤笑话”来查看新创建的表单了。在前面函数中,第一行代码返回了该节点类型的元数据信息。node_get_types()将检查$node->type以判定要返回哪种节点类型的元数据(在我们的例子中,$node->type的值将为“joke”)。这里再强调一遍,在钩子hook_node_info()中设置节点元数据,你已经在前面的joke_node_info()中设置了它。
    函数的其余部分包含了三个表单字段,用来收集标题、主题、笑话妙语。这里有一个重点,就是如何实现标题和主体#title键的动态化的。它们的值来源于hook_node_info(),如果在hook_node_info()中“locked”属性设置为FALSE的话,站点管理员也可以在http://example.com/?q=admin/content/types/joke修改这些值。
 
7-3.笑话的提交表单
 

Drupal版本:

添加过滤器格式支持

由于主体字段是一个textarea,并且对于节点主体字段可以使用过滤器格式,所以上面的表单中包含Drupal的标准内容过滤器,代码如下(过滤转换文本;使用过滤器的更多信息,可参看第11章):

$form['body_filter']['filter'] = filter_form($node->format);
 
    $node->format属性,指的是本节点body字段所用的过滤器格式的ID.这个属性存储在node_revisions表中。 如果你想让笑话妙语字段也可以使用过滤器格式,那么你就需要找个地方来存储该字段所用过滤器的信息.一个比较好的解决方案是, 在你的数据库表joke中再添加一个名为punchline_format的整数列, 来为每个笑话妙语存储过滤器格式。
 
    接着,将你的最后一个表单字段的定义修改成如下所示的形式:
 
$form['punchline']['field'] = array(
    '#type' => 'textarea',
    '#title' => t('Punchline'),
    '#required' => TRUE,
    '#default_value' => $node->punchline,
    '#weight' => 5
);
// Add filter support.
$form['punchline']['filter'] = filter_form($node->punchline_format);
 
    当你使用的是一个节点表单而不是一个普通表单时,node.module将处理节点表单中它所知道的默认字段的验证和存储工作(比如title和body字段---我们把后者改名为了Joke,但是节点模块仍然会把它作为节点主体字段进行处理);节点模块为你(开发者)提供了多个钩子,用来验证和存储你的自定义字段。
接下来我们将讨论这些钩子函数。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

使用hook_validate()来验证字段

老葛的Drupal培训班 Think in Drupal

当用户提交一个你节点类型的节点时,将会调用你模块中的钩子hook_validate()。因此,当用户提交该表单,来创建或者编辑一个笑话时,钩子hook_validate()将会查找joke_validate()函数,这样你就可以验证你的自定义字段中的输入了。在提交后,你也可以使用form_set_value()对数据做些修改。还可以使用form_set_error()来设置错误消息,如下所示:
/**
 * Implementation of hook_validate().
 */
function joke_validate($node) {
    // Enforce a minimum word length of 3 on punch lines.
    if (isset($node->punchline) && str_word_count($node->punchline) < 3) {
        $type = node_get_types('type', $node);
        form_set_error('punchline', t('The punch line of your @type is too short. You need at least three words.', array('@type' => $type->name)));
    }
}
 
    注意,你已经在hook_node_info()中为body字段定义了最小单词书目,而Drupal将自动对此进行验证。然而,punchline字段是你添加到该节点类型表单中的一个额外字段,所以你需要负责它的验证(加载、保存)。
 

Drupal版本:

使用hook_insert()来存储我们的数据

当一个新节点被保存时,会调用钩子insert()。在这个钩子中你可以将自定义数据存储到相关的表中。只有对于在节点类型元数据中定义的模块,才为其调用这一钩子。该信息定义在hook_node_info()的“module”键中(参看“提供我们节点类型的相关信息”一节)。例如,如果“module”键的值为joke,那么就会调用joke_insert()。如果你启用了书籍模块,并且新加了一个书籍类型的节点,此时就不会调用joke_insert();这里将调用的是book_insert(),这是因为book.module使用“module”键为book来定义了它的节点类型。

 
注意 如果你想在插入一个不同类型的节点时,对其做些操作的话,你需要把它当作一个普通的节点提交,使用钩子hook_nodeapi()插入一些操作。参看“使用hook_nodeapi()操纵其它类型的节点”一节。
 
    下面是为joke.module编写的hook_insert()函数:
/**
 * Implementation of hook_insert().
 */
function joke_insert($node) {
db_query("INSERT INTO {joke} (nid, vid, punchline) VALUES (%d, %d, '%s')",
$node->nid, $node->vid, $node->punchline);
}
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

使用hook_update()保持数据同步

老葛的Drupal培训班 Think in Drupal

当编辑完一个节点,并且节点核心数据已被写入到数据库中时,将会调用钩子update()。在这一钩子中可以编写对相关表的更新操作。和钩子hook_insert()一样,只有在节点为当前节点类型时才调用这个钩子。例如,如果节点类型在hook_node_info()中的“module”键为“joke”的话,那么将调用joke_update()。
 
/**
 * Implementation of hook_update().
 */
function joke_update($node) {
if ($node->revision) {
joke_insert($node);
}
else {
db_query("UPDATE {joke} SET punchline = '%s' WHERE vid = %d",
$node->punchline, $node->vid);
}
}
 
/**
 * Implementation of hook_update().
 */
function joke_update($node) {
    if ($node->revision) {
        // New revision; treat it as a new record.
        joke_insert($node);
    }
    else {
        db_query("UPDATE {joke} SET punchline = '%s' WHERE vid = %d",
        $node->punchline, $node->vid);
    }
}
 
在这里,你首先检查看是否设置了节点修订本标记,如果设置了,你为笑话妙语创建一个新的拷贝来替代旧的版本。
 

Drupal版本:

使用hook_delete()清理数据

老葛的Drupal培训班 Think in Drupal

在从数据库中删除一个节点以后,Drupal将会立即调用钩子hook_delete(),所有实现了这个钩子的模块都会被调用。该钩子一般用来从数据库中删除相关的信息。只有在删除当前节点类型的节点时,才调用这个钩子。如果节点类型在hook_node_info()中的“module”键为“joke”的话,将会调用joke_delete()。
 
/**
 * Implementation of hook_delete().
 */
function joke_delete(&$node) {
// Delete the related information we were saving for this node.
db_query('DELETE FROM {joke} WHERE nid = %d', $node->nid);
}
 
注意 当要删除的是一个修订本而不是整个节点时,Drupal将调用钩子hook_nodeapi(),其中将$op设为“delete revision”并将整个节点对象传递进来。接着你的模块可以以$node->vid为键,来删除该修订本的数据。
 

Drupal版本:

使用hook_load()来修改节点对象

老葛的Drupal培训班 Think in Drupal

在你的joke模块中,另一个需要实现的钩子就是hook_load(),它可以在构建节点对象时向对象中添加你自定义的节点属性。我们需要把笑话妙语字段注入到节点加载流程中,这样就可以在其它模块中以及主题层中使用它了。此时使用hook_load()。
在构建完核心节点对象以后,并且加载的节点属于当前节点类型时,才会调用该钩子。如果节点类型在hook_node_info()中的“module”键为“joke”的话,那么就调用joke_load()。
/**
 * Implementation of hook_load().
 */
function joke_load($node) {
return db_fetch_object(db_query('SELECT punchline FROM {joke} WHERE vid = %d', $node->vid));
}
 

Drupal版本:

使用hook_view()显示笑话妙语

老葛的Drupal培训班 Think in Drupal

现在你有了一个完整的系统,可以用来输入和编辑笑话。然而,尽管可以在节点提交表单中输入笑话妙语,但在查看笑话时,你却没有提供显示笑话妙语字段的方式,你的用户将会为此感到很困惑。让我们使用hook_view()来显示笑话妙语:
 
/**
 * Implementation of hook_view().
 */
function joke_view($node, $teaser = FALSE, $page = FALSE) {
    // If $teaser is FALSE, the entire node is being displayed.
    if (!$teaser) {
        // Use Drupal's default node view.
        $node = node_prepare($node, $teaser);
 
        // Add a random number of Ha's to simulate a laugh track.
        $node->guffaw = str_repeat(t('Ha!'), mt_rand(0, 10));
 
        // Now add the punch line.
        $node->content['punchline'] = array(
            '#value' => theme('joke_punchline', $node),
            '#weight' => 2
        );
    }
 
    // If $teaser is TRUE, node is being displayed as a teaser,
    // such as on a node listing page. We omit the punch line in this case.
    if ($teaser) {
        // Use Drupal's default node view.
        $node = node_prepare($node, $teaser);
    }
 
    return $node;
}
 
    在这段代码中,如果节点没有被显示为摘要形式(也就是说,$teaser为FALSE),那么笑话中就会包含笑话妙语。你已将笑话妙语的显示分解为一个单独的主题函数,这样就可以方便的进行覆写了。如果一个系统管理员想使用你的模块,但又想自定义外观的话,那么这将会非常方便。通过实现hook_theme(),并提供一个joke_punchline主题函数的默认实现,这样你就告诉了Drupal,你要使用这个主题函数了。
 
/**
 * Implementation of hook_theme().
 * We declare joke_punchline so Drupal will look for a function
 * named theme_joke_punchline().
 */
function joke_theme() {
    return array(
        'joke_punchline' => array(
            'arguments' => array('node'),
        ),
    );
}
 
function theme_joke_punchline($node) {
    $output = '<div class="joke-punchline">'.
        check_markup($node->punchline). '</div><br />';
    $output .= '<div class="joke-guffaw">'.
        $node->guffaw .'</div>';
    return $output;
}
 

Drupal版本:

使用hook_view()显示笑话妙语(1)

老葛的Drupal培训班 Think in Drupal

你需要清除主题注册表的缓存,这样Drupal就能找到你的主题钩子了。清除缓存有多种方式,一种是使用devel.module,还有一种是简单的访问“管理➤站点构建 ➤模块”页面。现在你的笑话输入和查看系统,应该可以完整工作了。继续前进,输入一些笑话来测试一下。现在你应该可以看到你的笑话了,它看起来外观有点朴素,如图7-4和7-5所示:
 
7-4 笑话节点的简单主题
 
 
7-5.节点以摘要形式显示时,没有添加笑话妙语
 
    尽管这也可以工作,但还存在一个更好的方式,让用户可以在查看完整节点页面时能够立即看到笑话妙语。我们想要的是,使用一个可伸缩的字段集,当用户点击时再展示笑话妙语。在Drupa中,可伸缩字段集的功能已经存在了,所以你只需要使用现有的就可以了,而不需要创建你自己的Javascript文件了。把这个交互放到你站点主题的模板文件中,比放到主题函数中更好一些,因为它依赖于标识字体和CSS类。你的设计者很乐意看到你这样做,因为如果要修改笑话节点的外观的话,只需要简单的编辑模板文件就可以了。
    你需要创建一个名为node-joke.tpl.php的模板文件,并将其放到你当前使用的主题的目录下面,下面是该文件中的内容。如果你使用的主题为bluemarine,那么node-joke.tpl.php将被放到themes/bluemarine下面。由于我们将会使用一个模板文件,那么就不再需要实现钩子hook_theme()和函数theme_joke_punchline()了,所以我们就可以把它们注释掉了。记住,要像前面所讲的一样,再次清除主题注册表缓存,这样Drupal就不再查找函数theme_joke_punchline()了。由于模板文件将负责笑话妙语的输出,所以在joke_view()中,我们还可以将笑话妙语指定到$node->content中的那段代码注释掉(否则,笑话妙语会被显示两次)。
 
注意:访问“管理➤站点构建 ➤模块”页面以后(将会自动重构主题注册表),主题系统将会自动发现node-joke.tpl.php,Drupal将使用该模板文件来修改笑话的外观,而不是默认的节点模板文件node.tpl.php。更多关于主题系统方面的知识,请参看第8章。
 
<div class="node<?php if ($sticky) { print " sticky"; } ?>
    <?php if (!$status) { print " node-unpublished"; } ?>">
        <?php if ($picture) {
            print $picture;
        }?>
        <?php if ($page == 0) { ?><h2 class="title"><a href="<?php
            print $node_url?>"><?php print $title?></a></h2><?php }; ?>
        <span class="submitted"><?php print $submitted?></span>
        <span class="taxonomy"><?php print $terms?></span>
        <div class="content">
            <?php print $content?>
            <fieldset class="collapsible collapsed">
                <legend>Punchline</legend>
                <div class="form-item">
                    <label><?php if (isset($node->punchline)) print check_markup($node->punchline)?></label>
                    <label><?php if (isset($node->guffaw)) print $node->guffaw?></label>
                </div>
                </legend>
            </fieldset>
        </div>
    <?php if ($links) { ?><div class="links">&raquo; <?php print $links?></div>
        <?php }; ?>
</div>
 
    Drupal将会自动地包含进来启用可伸缩功能的JavaScript文件。misc/collapsible.js中的JavaScript将为字段集查找可伸缩的CSS选择器,并且在找到以后知道如何处理它,如图7-6所示。这样,在node-joke.tpl.php中它将找到下面代码并激活它自己:
 
<fieldset class="collapsible collapsed">
 
    这就可以得到我们想要的笑话交互体验了:
 
7-6 使用Drupal内置的可伸缩CSS支持,来隐藏笑话妙语
 

Drupal版本:

使用hook_nodeapi()操纵其它类型的节点

老葛的Drupal培训班 Think in Drupal

前面的钩子只有在基于模块的hook_node_info()实现的“module”键时才调用。当Drupal看到一个blog节点类型时,那么将调用blog_load()。如果你想为每个节点都添加一些信息,不管它是什么类型,那该怎么办呢?我们到目前为止看到的钩子函数都做不到这一点;对于这一工作,我们需要一个更强大的钩子:hook_nodeapi()。
    这个钩子为模块提供了一个机会,来响应任意节点的生命周期期间的不同操作。node.module一般在调用完所有的特定节点类型的钩子函数以后,再调用钩子nodeapi()。下面是这个函数的签名:
 
hook_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL)
 
    因为节点对象$node是通过引用传递的,所以对它的任何修改都将改变真正的节点。参数$op用来描述对节点所进行的当前操作,它可以有多种不同的值:
 
• prepare: 即将显示节点表单。它适用于节点的添加和编辑表单。
 
• validate:用户刚刚完成对节点的编辑并试图预览或者提交它。在这里,你的代码应该进行检查,以确保数据是你期望;如果有地方出错了,那么就调用form_set_error(),它将为用户返回一个错误消息。你可以使用该钩子来检查甚至修改数据,当然,在验证钩子中修改数据通常被认为是坏的实践。
 
• presave:节点通过了验证,即将被保存到数据库中。
 
• insert:新节点刚被插入到了数据库中。
 
• update:节点刚被更新到了数据库中。
 
• delete:节点已被删除。
 
• delete revision:一个节点的修订本已被删除。如果模块中保存了与该修订本有关的数据,那么它需要对此作出反应。使用$node->nid可得到节点ID,使用$node->vid可得到该修订本的ID。
 
• load: 从数据库中加载了基本的节点对象,再加上由节点类型设置的额外的节点属性(为了响应已经运行的hook_load();参看本章前面的“使用hook_load()来修改节点对象”一节)。此时你可以添加新的属性或者操作已有的节点属性。
 
• alter:节点的内容已经通过了drupal_render(),并被保存到了$node->body(如果节点以完整的形式显示)或者node->teaser(如果节点以摘要的形式显示)中,并且节点即将被传递给主题层。模块可以修改这个完整构建的节点。但是对$node->content中字段的修改,应该放到查看操作中,而不是这个操作中。
 
• view: 节点即将被显示给用户。这个动作将在钩子hook_view()之后调用,所以模块可以假定节点已被过滤并且现在包含了HTML。其它项目也可被添加到$node->content(示例可参看,前面我们是如何添加笑话妙语的)。
 
• search result:节点即将作为一个搜索结果项目显示出来。
 
• update index:节点正在被搜索模块索引化。有些额外信息,使用nodeapi的“view”操作不能将其显示出来,如果你想对其进行索引的话,你可以在这里将它返回(参看第12章)。
 
• prepare translation:翻译模块正在准备对节点进行翻译。模块可以添加自定义的翻译了的字段。
 
• rss item:节点正在被作为RSS种子的一部分而包含进来。
 
    函数hook_nodeapi()的最后两个参数,它们的值将根据当前操作的不同而改变。当一个节点被显示并且$op为alter或者view时,$a3 将为$teaser,而 $a4将为$page(参看node.module中的node_view())。参数的总结可参看表7-1 。
 
7-1. $op为alter或者 view时,hook_nodeapi()中参数$a3 和 $a4的含义
参数       含义
$teaser     是否要仅仅展示teaser,比如在http://example.com/?q=node上。
$page       如果一个节点自己作为一个页面显示时,$pageTRUE(比如,                                          http://example.com/?q=node/2))
 
    当节点正被验证时, 参数$a3为node_validate()中的参数$form(也就是,表单定义数组)。
    显示一个节点页面时,比如http://example.com/?q=node/3,钩子的调用次序,如图7-7所示:
7-7. 显示一个节点页面的执行路径
 

Drupal版本:

如何存储节点

老葛的Drupal培训班 Think in Drupal

节点在数据库中被分成了3个部分。表node包含了描述节点的大部分元数据。表node_revisions包含了节点的主体和摘要,以及一些修订本特有的信息。正如你在joke.module例子中看到的,其它节点类型可以自由的在加载节点时向节点添加数据,同时可以在它们自己的表中存储它们想要的数据。
    图7-8展示了一个包含了大部分常用属性的节点对象。注意,你创建的用以存储笑话妙语的表,被用来填充节点。根据启用模块的不同,在你的Drupal安装里面,节点对象包含的属性可能会有或多或少的变化。
 
 
7-4 节点对象

Drupal版本:

使用CCK创建节点类型

老葛的Drupal培训班 Think in Drupal

在前面joke.module中,我们给大家展示了使用模块的方式来创建一个节点类型,尽管这种方式具有较高的自由度并且具有较高的性能,但是它却有点枯燥无味。如果不做任何编程工作,就可以组装一个新的节点类型的话,难道这样的方式不会更好么?这就是CCK提供的方式。
 
     注意:关于CCK的更多信息,访问CCK项目页面 http://drupal.org/project/cck
 
 
    现在,你可以导航到“管理内容管理内容类型”,通过后台管理界面页面添加一个新的内容类型(比如一个笑话(joke)内容类型)。如果你已经启用了joke.module,你要为节点类型起一个不同的名字以避免命名冲突。CCK中的其它部分尚未被添加到Drupal核心中,比如为新节点类型添加除标题和主体以外的其它字段的能力。在joke.module的例子中,你需要三个字段:标题,笑话本身,和笑话妙语。你使用Drupal的hook_node_info()将主体(body)字段改名为笑话(Joke);通过实现一些钩子函数,并创建你自己的数据库表来存储笑话妙语,从而提供了笑话妙语(punchline)字段。在CCK中,你可以简单的创建一个名为punchline的文本输入字段,并将其添加到你的内容类型中。CCK替你负责数据的存储、取回、和删除。
 
注意:Drupal第3方模块库中包含了大量的CCK字段模块,用来添加图片、日期、电子邮件、地址等字段。CCK相关第3方模块位于:http://drupal.org/project/Modules/category/88
 
       由于在编写本书时,CCK的许多地方还在开发和完善中,所以我们在这里不讨论更多细节了。然而,可以清晰的看到,在将来,使用编写模块的方式会越来越少,而使用CCK的方式(通过管理界面来组装一个新的节点类型)则会越来越流行。
 

Drupal版本:

限制对节点的访问

老葛的Drupal培训班 Think in Drupal

有多种方式可用于限制对节点的访问。你已经看到了,如何使用hook_access()来限制对一个节点类型的访问,以及使用hook_perm()定义权限。但是Drupal提供了用于控制访问的更丰富的工具集:使用表node_access以及两个访问钩子函数hook_node_grants()和hook_node_access_records()。
 
    当Drupal初次安装时,将会向node_access表中写入一条记录,它将有效的关闭节点访问机制。只有当使用了节点访问机制的模块被启用时,才会用到Drupal的这一部分。位于modules/node/node.module中的函数node_access_rebuild()可用来追踪启用了哪些节点访问模块,如果这些模块都被禁用了,那么这个函数还可以恢复默认记录,如表7-2所示。
 
7-2. node_access表的默认记录
nid gid realm grant_view grant_update grant_delete
0   0   all      1           0            0
 
    一般情况下,如果一个节点访问模块正被使用(也就是说,它修改了标node_access),如果它没有向表node_access插入一行记录,用来定义如何处理访问,那么Drupal将拒绝对节点的访问。
 

Drupal版本:

定义节点授权(Grants)

老葛的Drupal培训班 Think in Drupal

有三个基本的权限,对应于节点之上的三种操作:查看、更新、删除。当这些操作中的一个将要发生时,如果一个模块实现该节点类型,将首先使用这个模块里面的函数node_access()。如果该模块没有定义是否允许访问的话(也就是说,它返回了NULL,而不是TRUE或FALSE),Drupal将向所有应用于节点访问控制的模块询问,这个操作是否应该被允许进行。通过使用hook_node_grants(),为每个领域(realm)每个用户得到一个授权(grant)ID列表,来完成这一工作。

Drupal版本:

什么是领域(Realm)?

 

领域就是一个任意的字符串,它用于允许多个节点访问控制模块共享数据库表node_access。例如,acl.module是一个使用访问控制列表(ACLs)来管理节点访问的第三方模块,它的领域就是acl。taxonomy_access.module是另一个第3方模块,它基于分类术语来限制对节点的访问,它使用term_access作为领域。所以,领域就是在表node_access中标识你的模块空间的东西;它有点像命名空间。当需要你的模块返回许可ID时,你将从你模块定义的领域中返回它。
老葛的Drupal培训班 Think in Drupal

Drupal版本:

什么是授权(Grant)ID

老葛的Drupal培训班 Think in Drupal

一个授权ID是一个标识符,用于为一个给定领域,提供节点访问控制权限方面的信息。例如,一个节点访问控制模块----比如forum_access.module,它根据用户角色,来管理对论坛类型节点的访问----可以使用角色ID作为授权ID。一个使用US ZIP代码来管理对节点访问的模块,可以使用ZI P代码作为授权ID。在每种情况下,都是用与用户有关的东西作为授权ID,比如该用户拥有这个角色么?这个用户的ZIP是12345么?用户是在这个访问控制列表中么?或者,这个用户定阅的时间超过1年了么?
    节点访问模块为包含授权ID的领域提供了授权ID,尽管每个授权ID都特定于它的节点访问模块,但是它们的底层原理是一样的,那就是在node_access表中,如果存在一条包含了授权ID的记录的话,那么就启用访问,访问的类型则可以通过grant_view, grant_update, 或者grant_delete列中,值为1的那个进行判定.
    在一个节点正在保存时,会将授权ID插入到表node_access中。将这个节点对象传递给实现了钩子hook_node_access_records()的每个模块。模块将检查节点,或者简单的返回(如果它不用为该节点处理访问控制),或者返回一个数组,里面包含了用于插入到表node_access中的授权。使用node_access_acquire_grants()可以批量的插入授权。下面是一个来自于forum_access.module的例子。
 
/**
 * Implementation of hook_node_access_records().
 *
 * Returns a list of grant records for the passed in node object.
 */
function forum_access_node_access_records($node) {
...
if ($node->type == 'forum') {
$result = db_query('SELECT * FROM {forum_access} WHERE tid = %d', $node->tid);
while ($grant = db_fetch_object($result)) {
$grants[] = array(
'realm' => 'forum_access',
'gid' => $grant->rid,
'grant_view' => $grant->grant_view,
'grant_update' => $grant->grant_update,
'grant_delete' => $grant->grant_delete
);
}
return $grants;
}
}

Drupal版本:

节点访问流程

老葛的Drupal培训班 Think in Drupal

当要对节点进行某一操作时,Drupal将进入如图7-9所示的流程。
 
7-9 判定是否允许对给定节点的访问
 

Drupal版本:

总结

读完本章后,你应该可以

老葛的Drupal培训班 Think in Drupal

 

• 理解什么是节点,什么是节点类型
• 编写模块来创建节点类型
• 理解如何在节点创建、保存、加载等等操作时插入钩子函数
• 理解如何判定对节点的访问控制
 

Drupal版本:

第8章 drupal主题系统

老葛的Drupal培训班 Think in Drupal

如果你想修改Drupal生成的HTML或者其它标识字体,那么你需要深入的了解主题系统的各个组成部分。主题系统是个优雅的架构,它使你无需核心代码,就可以得到想要的外观;但是它也有一个很长的学习曲线,特别是你想要完全定制一个站点主题,以与其它drupal站点区别开来,那么你还是需要费点功夫的。我将向你讲述主题系统的工作原理,以及向你展示隐藏在Drupal核心之中的一些最佳实践。首先要记住的是:不要通过编辑模块文件内部的HTML来改变你站点的外观。如果这样做了,你仅仅创建了一个对你个人适用的内容管理系统,这样你就会失去开源软件系统最大的优势之一------社区的支持。覆写,而不是修改!
 
主题系统的组成
       主题系统由多个抽象层次所组成:模板语言,主题引擎和主题。

Drupal版本:

drupal模板语言和主题引擎

老葛的Drupal培训班 Think in Drupal

主题系统可以使用多个模板语言。Smarty, PHPTAL, 和PHPTemplate都可以与Drupal集成,用来向模板文件中添加动态数据。为了使用这些语言,需要一个叫做主题引擎的包装器,用来在模板语言和Drupal之间进行交互。你可以在http://drupal.org/project/Theme+engines中找到对应模板语言的主题引擎。安装主题引擎其实很简单,只需要通过将相应主题引擎的目录放置到你站点的主题引擎目录下面就可以了。如果仅用于单个站点,使用目录sites/sitename/themes/engines;如果用于多个Drupal站点,则使用目录sites/all/themes/engines,如图8-1所示。
    Drupal社区创建了一个自己的引擎,专门对Drupal作了优化。它就是PHPTemplate,它使用PHP作为模板语言,这样它就不需要中间层的解析环节了,而其它模板语言常常需要这一环节。这是Drupal最常用的模板引擎,它是Drupal自带的。它位于themes/engines/phptemplate,如图8-2所示:
 
8-1 为Drupal添加定制主题引擎的目录结构
 
8-1 Drupal核心主题引擎的目录结构。这个位置专门用于放置核心主题引擎。
 
注意 完全可以不使用模板语言,而简单的使用纯php的模板文件。如果你是热衷于追求速度,或者可能仅仅是想折磨一下你的设计人员,那么你可以不使用主题引擎而仅仅整个主题包装在PHP函数中,比如使用函数themename_page()和themename_node()来代替模板文件。一个基于PHP主题的示例,可参看themes/chameleon/chameleon.theme。
 
    当你安装好一个主题引擎后,你不会看到你的站点有了任何改变。这是因为,主题引擎仅仅是一个接口库,在它被使用以前,你仍然需要安装一个依赖于该主题引擎的Drupal主题。
    要使用哪一个模板语言呢?如果你正在转换一个遗留站点,那么可能使用以前的模板语言会更方便一些;也许你的设计团队更倾向于使用所见即所得的编辑器,这样PHPTAL应该是个更好的选择,因为它可以阻止这些编辑器对模板的破坏。你可以发现,大多数的文档和支持都是关于PHPTemplate的,如果你是从头开始建立一个站点的话,那么从长期的维护和社区支持这两个方面来看,PHPTemplate应该是最好的选择。
 

Drupal版本:

drupal主题

 

Drupal的行话来说,主题就是一组负责你站点外观的文件。你可以从http://drupal.org/project/Themes下载第3方主题,或者你可以自己动手创建一个主题,后者正是你在本章将要学习的。作为一个web设计者,主题由你所熟悉的大部分内容所组成:样式表,图片,JavaScript文件,等等。你将发现,在Drupal主题和纯HTML站点之间的区别就是模板文件。这些文件一般都包含大段的静态HTML,和一些小段的用来插入动态内容的代码。它们负责你站点的一个特定部分的外观。模板文件的语法依赖于它所使用的主题引擎。例如,列表8-1,8-2,8-3列出了3个模板文件的代码片段,它们输出的内容是一样但是包含的模板文件内容却完全不同。
 
列表 8-1. Smarty
<div id="top-nav">
    {if count($secondary_links)}
        <ul id="secondary">
        {foreach from=$secondary_links item=link}
            <li>{$link}</li>
        {/foreach}
        </ul>
    {/if}
 
    {if count($primary_links)}
        <ul id="primary">
        {foreach from=$primary_links item=link}
            <li>{$link}</li>
        {/foreach}
        </ul>
    {/if}
</div>
 
列表 8-2. PHPTAL
<div id="top-nav">
    <ul tal:condition="php:is_array(secondary_links)" id="secondary">
        <li tal:repeat="link secondary_links" tal:content="link">secondary link</li>
    </ul>
 
    <ul tal:condition="php:is_array(primary_links)" id="primary">
        <li tal:repeat="link primary_links" tal:content="link">primary link</li>
    </ul>
</div>
 
列表 8-3. PHPTemplate
<div id="top-nav">
    <?php if (count($secondary_links)) : ?>
        <ul id="secondary">
        <?php foreach ($secondary_links as $link): ?>
            <li><?php print $link?></li>
        <?php endforeach; ?>
        </ul>
    <?php endif; ?>
 
    <?php if (count($primary_links)) : ?>
        <ul id="primary">
        <?php foreach ($primary_links as $link): ?>
            <li><?php print $link?></li>
        <?php endforeach; ?>
        </ul>
    <?php endif; ?>
</div>
 
    每一个模板文件,由于它所使用的模板语言的不同,所以看起来也各不相同。模板文件的扩展名指明了它所使用的模板语言,也就是它所依赖的主题引擎(参看表8-1)
 
8-1 模板文件的扩展名指出了它所依赖的模板语言。
模板文件           主题引擎扩展
.theme                   PHP
.tpl.php                PHPTemplate*
.tal                      PHPTAL
.tpl                      Smarty
* PHPTemplate是Drupal的默认主题引擎
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

安装drupal主题

老葛的Drupal培训班 Think in Drupal

为了让一个新的主题显示在Drupal管理界面中,你需要把它放到sites/all/themes下面。这样不仅你的Drupal站点可以使用这个主题,一个多站点系统中的所有站点都可以使用该主题。如果你的是个多站点系统,而你又想把这个主题仅仅用在特定站点上,那么你可以把它放到sites/sitename/themes下面。你可以在你的站点安装多个主题,主题的安装过程和模块的基本相同。将主题文件放到相应的位置后,导航到管理界面“管理➤站点构建➤主题”。你可以安装多个主题,也可以一次启用多个主题。这意味着什么?通过启用多个主题,用户可以在他们的个人资料页面上,从已启用的主题中选择一个作为他们自己的主题。在用户访问站点时,就会使用所选的主题了。
  当下载或者创建一个新的主题时,将新建主题和核心主题以及第3方主题区分开来是个很好的习惯。我们推荐在你的themes文件夹下面创建两个文件夹。将自定义主题放到文件夹custom下,而将从drupal.org下载下来的第3方的主题放到drupal-contrib下。不过这个实践不是特别重要,不像模块目录下面那样特别注重这点,因为一个站点的主题一般只有几个,但是模块的数量却有很多。

Drupal版本:

构建一个PHPTemplate主题

老葛的Dupal培训班 Think in Drupal

创建一个主题,可以有多种方式,这取决于你的起始材料。假定你的设计者已经为你的站点提供了HTML和CSS文件。那么将设计者的设计转化为一个Drupal主题,到底难不难呢?它实际上不是很难,而且你能够轻易的完成工作的80%。不过还有20%---最后的难点了---它是Drupal主题制作高手与新手的分水岭。首先让我们从简单的部分开始。这里有个概括:
 
1. 为站点创建或修改HTML文件。
2. 为站点创建或修改CSS文件。
3. 创建一个.info文件,来向Drupal描述你的新主题。
4. 按照Drupal的标准为文件命名。
5. 在你的模板中,插入可用的变量。
6. 为单独的节点类型,区块,等等创建模板文件。
 
注意 如果你从头开始设计你的主题,那么在开放源代码WEB设计站点http://www.oswd.org里面有很多非常好的设计可供借鉴(注意这些是HTML和CSS设计,而不是Drupal主题)。
 

Drupal版本:

使用已有的HTML和CSS文件

 

我们假设你已经有了HTML页面和CSS样式,如列表8-4和8-5中所给出的,现在让你将它们转化为一个Drupal主题。显然在一个实际的项目中,你所用到的文件应该比这些更详细;我们在这里介绍的是方法,所以示例简单了一些。
 
列表 8-4. page.html
<html>
<head>
    <title>Page Title</title>
    <link rel="stylesheet" href="global.css" type="text/css" />
</head>
<body>
    <div id="container">
        <div id="header">
            <h1>Header</h1>
        </div>
        <div id="sidebar-left">
            <p>
                Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam
                nonummy nibh euismod tincidunt ut.
            </p>
        </div>
        <div id="main">
            <h2>Subheading</h2>
            <p>
                Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam
                nonummy nibh euismod tincidunt ut.
            </p>
        </div>
 
        <div id="footer">
            Footer
        </div>
    </div>
</body>
</html>
 
列表 8-5. global.css
#container {
    width: 90%;
    margin: 10px auto;
    background-color: #fff;
    color: #333;
    border: 1px solid gray;
    line-height: 130%;
}
#header {
    padding: .5em;
    background-color: #ddd;
    border-bottom: 1px solid gray;
}
#header h1 {
    padding: 0;
    margin: 0;
}
#sidebar-left {
    float: left;
    width: 160px;
    margin: 0;
    padding: 1em;
}
#main {
    margin-left: 200px;
    border-left: 1px solid gray;
    padding: 1em;
    max-width: 36em;
}
#footer {
    clear: both;
    margin: 0;
    padding: .5em;
    color: #333;
    background-color: #ddd;
    border-top: 1px solid gray;
}
#sidebar-left p {
    margin: 0 0 1em 0;
}
#main h2 {
    margin: 0 0 .5em 0;
}
 
    该设计如图8-3所示
 
8-3 在转化为Drupal主题以前的设计
 
    让我们将这个新主题叫作greyscale,在文件夹sites/all/themes/custom下面创建一个子文件夹greyscale。如果sites/all/themes/custom文件夹不存在的话,那么你需要新建一个。将page.html和global.css复制到greyscale文件夹下面。接下来,将page.html重命名为page.tpl.php,这样它将作为一个新的页面模板,为Drupal的每个页面服务了。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

为你的drupal主题创建一个.info文件

老葛的Drupal培训班 Think in Drupal

每个主题都需要包含一个文件,用来向Drupal描述它的能力。这个文件就是主题的.info文件。由于我们把我们的主题叫作greyscale,所以我们的.info文件就被命名为greyscale.info。创建文件sites/all/themes/custom/greyscale/greyscale.info,并输入列表8-6所示的10行代码。
 
列表 8-6.主题的.info文件
; $Id$
name = Greyscale
core = 6.x
engine = phptemplate
regions[left] = Left sidebar
; We do not have a right sidebar.
; regions[right] = Right sidebar
regions[content] = Content
regions[header] = Header
regions[footer] = Footer
 
    如果我们想要更复杂一些的话,那么我们可以在我们的.info文件中为Drupal提供更多的信息。让我们看看这个文件都可以包含哪些信息,如列表8-7所示。
 
列表 8-7.带有更多信息的.info文件
; $Id$
; Name and core are required; all else is optional.
name = Greyscale
description = Demurely grey tableless theme.
screenshot = screenshot.png
core = 6.x
engine = phptemplate
 
regions[left] = Left sidebar
; We do not have a right sidebar
; regions[right] = Right sidebar
regions[content] = Content
regions[header] = Header
regions[footer] = Footer
 
; Features not commented out here appear as checkboxes
; on the theme configuration page for this theme.
features[] = logo
features[] = name
features[] = slogan
features[] = mission
features[] = node_user_picture
features[] = comment_user_picture
features[] = search
features[] = favicon
features[] = primary_links
features[] = secondary_links
 
; Stylesheets can be declared here or, for more
; control, be added by drupal_add_css() in template.php.
; Add a stylesheet for media="all":
stylesheets[all][] = mystylesheet.css
; Add a stylesheet for media="print":
stylesheets[print][] = printable.css
; Add a stylesheet for media="handheld":
stylesheets[handheld][] = smallscreen.css
; Add a stylesheet for media="screen, projection, tv":
stylesheets[screen, projection, tv][] = screen.css
; Override an existing Drupal stylesheet with our own
; (in this case the forum module's stylesheet):
stylesheets[all][] = forum.css
 
; JavaScript files can be declared here or, for more
; control, be added by drupal_add_js() in template.php.
; scripts.js is added automatically (just like style.css
; is added automatically to stylesheets[]).
scripts[] = custom.js
 
; PHP version is rarely used; you might need it if your
; templates have code that uses very new PHP features.
php = 5.2.0
 
; Themes may be based on other themes; for example, they
; may simply override some of the parent theme's CSS.
; See the Minnelli theme at themes/garland/minnelli for
; an example of a theme that does this in Drupal core.
base theme = garland
 
    由于Greyscale主题现在有了一个.info文件(列表8-6所给的简单的那个)和一个page.tpl.php文件,所以你就可以在管理界面中启用它了。导航到“管理➤站点构建➤主题”,将它设置为默认主题。

Drupal版本:

为你的drupal主题创建一个.info文件(1)

老葛的Drupal培训班 Think in Drupal

恭喜恭喜!现在你应该可以实际的看到你的设计了。外部的样式表还没有加载进来(我们将在后面讨论它),访问你的站点中的任何页面,都会一而再再而三的显示同一个页面,尽管如此,这也是一个了不起的开始!由于访问你的站点中的任何页面,都会显示page.tpl.php中的静态HTML内容,所以现在你无法进入管理界面了。我们将你关到了Drupal站点的门外面!哎哟。一不小心被关到了门外面,对于初学者来说,这是常碰到的事情,下面我们将向你讲述如何解决这个问题。一种方案是对刚才启用的主题进行重命名。在这种情况下,你可以简单的将greyscale重命名为greyscale_,这样你就可以重新返回站点到里面了。那是一个快速解决办法,但是由于你知道问题的真正所在(也就是,我们还没有包含动态内容),这里给出另一种方案:你可以向page.tpl.php中添加适当的变量,从而显示Drupal的动态内容而不是前面的静态内容。
    每一个PHPTemplate模板文件----比如page.tpl.php,node.tpl.phpblock.tpl.php等等----都有一组动态内容的变量传递给它们使用。打开page.tpl.php将相应的静态内容替换为相应的Drupal变量。不要担心,我很快就会对这些变量进行讲解。
 
<html>
<head>
    <title><?php print $head_title ?></title>
    <link rel="stylesheet" href="global.css" type="text/css" />
</head>
<body>
    <div id="container">
        <div id="header">
            <h1><?php print $site_name ?></h1>
            <?php print $header ?>
        </div>
 
        <?php if ($left): ?>
            <div id="sidebar-left">
               <?php print $left ?>
            </div>
        <?php endif; ?>
 
        <div id="main">
            <?php print $breadcrumb ?>
            <h2><?php print $title ?></h2>
            <?php print $content ?>
        </div>
 
        <div id="footer">
            <?php print $footer_message ?>
            <?php print $footer ?>
        </div>
    </div>
<?php print $closure ?>
</body>
</html>
 
    重新加载页面,你将发现,变量被来Drupal的内容替换了。你将注意到没有加载global.css样式表,这是因为指向该文件的路径不对。你可以手工的调整它的路径,或者你可以采用Drupal的方式来完成它,这样更加灵活并且具有其它好处。
  首先,将global.css重命名为style.css。根据规定,Drupal将自动的查找每个主题下面的style.css文件。一旦找到了这个文件,那么Drupal会将其添加到变量$styles里面,从而被传递给page.tpl.php.让我们使用下面的信息来更新page.tpl.php。
<html>
<head>
<title><?php print $head_title ?></title>
<?php print $styles ?>
</head>
...
    保存你的修改并重新加载页面。瞧!如果你查看页面的源代码的话,你将注意到,其它启用的模块所带有的样式表也被加载了进来,这些都是通过变量$styles实现的:
<html>
<head>
    <title>Example | Drupal 6</title>
    <link type="text/css" rel="stylesheet" media="all"
        href="modules/node/node.css?f" />
    <link type="text/css" rel="stylesheet" media="all"
        href="modules/system/defaults.css?f" />
    <link type="text/css" rel="stylesheet" media="all"
        href="modules/system/system.css?f" />
    <link type="text/css" rel="stylesheet" media="all"
        href="modules/system/system-menus.css?f" />
    <link type="text/css" rel="stylesheet" media="all"
        href="modules/user/user.css?f" />
    <link type="text/css" rel="stylesheet" media="all"
        href="sites/all/themes/greyscale/style.css?f" />
</head>
...
 
    通过将你的CSS文件命名为style.css,这样Drupal就可以使用它的CSS预处理引擎来对它进行处理,以消除CSS文件中所有的空白和换行,另外,它还将它们合并到了一起(Drupal没有使用多个样式表),作为一个文件提供给浏览器。关于这一特性的更多细节,参看第22章。
 
注意 Drupal在样式表URL的后面添加了伪查询字符串(在前面例子中的“?f”),这样就可以控制缓存了。当需要的时候,它可以修改字符串,比如运行update.php以后,或者在管理界面“管理➤站点构建 ➤性能”中清空了缓存以后。
 
    在你将global.css重命名为style.css以后,刷新浏览器,你将看到一个与图8-3中主题类似的主题,它包含了页首,页脚,和左边栏。尝试一下,导航到“管理➤站点构建 ➤区块”,将“在线用户”区块指定到左边栏。
  除了前面提到的这些变量以外,还有更多的变量可以添加到page.tpl.php和其它模板文件中。让我们深入的学习一下!如果你没有动手实现前面所给的例子,那么你可以浏览一下themes目录中所带有的核心主题,看看在这些主题中,变量是如何使用的。
 

Drupal版本:

理解drupal模板文件

老葛的Drupal培训班 Think in Drupal

一些主题包含各种模板文件,而有些仅包含page.tpl.php。那么你如何知道,你可以创建哪些Drupal能够识别的模板文件呢?创建模板文件时,所遵循的命名约定有哪些?在接下来的部分中,我将向你讲解使用模板文件的各种技能。
 
大图
    page.tpl.php是所有其它模板文件的祖宗,它负责站点的整体布局。其它模板文件被插入到了page.tpl.php中,如图8-4所说明的。
8-4其它的模板被插入到了page.tpl.php文件中
 
    在页面的构建期间,图8-4中block.tpl.php和node.tpl.php的插入是由主题系统自动完成的。还记不记得,你在前面的例子中创建的page.tpl.php文件?好的,变量$content包含了调用node.tpl.ph的输出,而$left包含了调用block.tpl.php的输出。让我们看看它是怎么工作的。
    让我们向Greyscale主题中添加一个节点模板文件。我们在这里没有从头创建一个,而是拷贝Drupal的默认节点模板文件;也就是,如果一个主题中找不到node.tpl.php文件的话,所使用的节点模板文件。将modules/node/node.tpl.php拷贝到sites/all/themes/custom/greyscale/node.tpl.php。然后访问“管理➤站点构建 ➤模块”页面,这样就会重新构建主题注册表。在重新构建的过程中,Drupal将找到sites/all/themes/custom/greyscale/node.tpl.php文件,并且从现在起,它将使用这个文件作为节点模板。导航到“创建内容➤Page”,来创建一个节点(只输入标题和主体字段就可以了)。现在你可以对你的node.tpl.php文件做一点小的修改(比如在它的最后面加上“你好吗!”)。现在你节点的显示,就使用了你修改的模板文件。
    对于block.tpl.php,方法是一样的(你可以在modules/system/block.tpl.php找到默认的区块模板文件),对于Drupal中的其它模板,也同样适用。
 

Drupal版本:

drupal theme()函数介绍

Drupal想要为一个可主题化的项目(比如节点,区块,面包屑,评论,或者用户签名)生成一些HTML输出时,它将查找用来为该项目生成HTML的主题函数或者模板文件。Drupal的所有部分,基本上都是可主题化的,这意味着,对于为该项目实际生成的HTML,你可以进行覆写。我们一会儿看一些例子。

 
提示 在Drupal中,可主题化的项目的列表,可参看http://api.drupal.org/api/group/themeable/6
 
老葛的Drupal培训班

Drupal版本:

drupal theme()工作原理概览

老葛的Drupal培训班 Think in Drupal

当一个简单的节点页面显示时,比如http://example.com/?q=node/3,都发生了什么呢,这里给出了大致的总结:
 
1. Drupal的菜单系统收到了请求,并将控制权转交给节点模块。
 
2. 在构建了节点数据结构以后,调用theme('node', $node, $teaser, $page)。这将查找合适的主题函数或者模板文件,定义模板文件中所用的各种变量,应用该模板,为节点生成最终的HTML。(如果有多个节点正在被显示,比如一个日志,那么对于每个节点都会调用一遍这个流程。)
 
3. 如果启用了评论模块,节点的评论也将被转化为HTML,并追加在节点的HTML后面。
 
4. 这样就返回了一团HTML(在index.php中,它就是变量$return),使用theme('page', $return),这样就再次传递给了theme()函数。
 
5. 在处理页面模板以前,Drupal作了一些预处理,比如,找出有哪些区域可用,以及在每个区域中显示哪些区块。通过调用theme('blocks', $region),将每个区块转化为HTML,theme('blocks', $region)是用来定义区块变量并应用区块模板的。在这里,你应该可以开始看到一个模式了。
 
6. 最后,Drupal定义了许多供页面模板使用的变量,并将其应用到页面模板中去。
 
    现在,从前面的列表中,你应该能够认识到theme()函数在Drupal中的重要地位。它负责运行预处理函数,来设置模板中所用的变量;它将主题函数的调用,分发给合适的函数或者查找合适的模板文件。而输出的结果就是HTML。这一流程的图示可参看图8-5。我们将在后面更深入的学习一下,这个函数是如何工作的。现在,应该不难理解,当Drupal想将一个节点转化为HTML时,就会调用theme('node')。根据所启用的主题,theme_node()将用来生成HTML,或者使用一个名为node.tpl.php的模板文件来生成HTML。
    可以在多个层次上,对这个流程进行覆写。例如,主题可以覆写内置的主题函数,所以,当调用theme('node')时,那么将会调用greyscale_node(),而不是默认的theme_node()。模板文件也有命名约定,我们将在后对它进行讲解,所以,模板文件node-story.tpl.php将专门负责Story类型的节点。
8-5. theme()函数调用时的执行流程
 

Drupal版本:

覆写可主题化的项目

老葛的Drupal培训班 Think in Drupal

Drupal的主题系统背后的核心哲理和钩子系统的类似。通过遵循命名规范,就可以标识出哪些函数是主题相关的函数,它们负责格式化并返回你站点的内容,或者使用模板文件负责输出HTML内容。
 

Drupal版本:

覆写drupal主题函数

老葛的Drupal培训班 Think in Drupal

正如你看到的,可主题化的项目是通过它们的函数名来标识的,每个函数名前都带有前缀“theme_”,或者还可以通过模板文件来标识。这一命名规范使得Drupal能够为所有的可主题化函数创建一个函数覆写机制。这样,设计者就可以指示Drupal执行一个具有更高优先级的自定义函数,从而替代开发者在模块中给出的默认的主题函数,或者替代Drupal的默认模板文件。例如,让我们检查一下,在构建站点的面包屑时该流程是怎么工作的。
    打开includes/theme.inc文件,并检查里面的函数。这里的许多函数都以theme_开头,这就告诉人们它们是可以被覆写的。特别的,我们看看theme_breadcrumb():
 
/**
 * Return a themed breadcrumb trail.
 *
 * @param $breadcrumb
 * An array containing the breadcrumb links.
 * @return a string containing the breadcrumb output.
 */
function theme_breadcrumb($breadcrumb) {
     if (!empty($breadcrumb)) {
         return '<div class="breadcrumb">'. implode(' » ', $breadcrumb) .'</div>';
     }
}
 
这个函数控制着Drupal中面包屑导航条的HTML输出。当前,它在面包屑的每一项之间添加了一个向右的双箭头分隔符(»)。假定你想将div标签改为span标签,并使用星号(*)来代替双箭头(»)。那么你该怎么办呢?一种方式是在theme.inc中修改这个函数,保存,并调用。这样也能达到目的。(别!别!千万别这样做!)。我们有更好的方式。
你有没有见过Drupal核心中是怎么调用这些主题函数的?你永远都不会看到直接调用theme_breadcrumb()的情况。替代的,它通常包装在帮助函数theme()中。你期望这样调用这个函数:
 
theme_breadcrumb($breadcrumb)
 
    但实际不是这样。替代的,你将看到开发者这样调用:
 
theme('breadcrumb', $breadcrumb);
 
    这个通用的theme()函数负责初始化主题层,并将函数调用分发到合适的位置,这使得我们能够以更优雅的方式来解决我们的问题。图8-5展示了通过调用theme(),指示Drupal按照下面的次序查来找相应的面包屑函数。
假定你使用的主题为Greyscale,,它是基于PHPTemplate的主题,那么Drupal将会查找下面的函数(我们暂且忽略一下breadcrumb.tpl.php):
 
greyscale_breadcrumb()
phptemplate_breadcrumb()
sites/all/themes/custom/greyscale/breadcrumb.tpl.php
theme_breadcrumb()
 
    我们看到函数phptemplate_breadcrumb()可以覆写内置的面包屑函数,那么我们要把这个函数放到哪里呢?
    很简单,那就是你主题的template.php文件,在这里你可以覆写Drupal的默认主题函数,拦截和创建传递给模板文件的自定义变量.
 
注意 在做这些练习的时候,不要使用Garland作为当前主题,因为Garland已经有了一个template.php文件.替代的,在这里可以使用Greyscale或者Bluemarine.
 
    为了修改Drupal的面包屑,创建文件sites/all/themes/custom/greyscale/template.ph,并将theme.inc中的theme_breadcrumb()函数复制并粘贴到该文件里面。记住要包含<?php标签。还有对函数要进行重命名,将theme_breadcrumb改为phptemplate_breadcrumb。接着,导航到“管理➤站点构建 ➤模块”以重新构建主题注册表,这样Drupal就能够找到你的新函数了。
 
<?php
/**
 * Return a themed breadcrumb trail.
 *
 * @param $breadcrumb
 * An array containing the breadcrumb links.
 * @return a string containing the breadcrumb output.
 */
function phptemplate_breadcrumb($breadcrumb) {
     if (!empty($breadcrumb)) {
         return '<span class="breadcrumb">'. implode(' * ', $breadcrumb) .'</span>';
     }
}
 

当下一次Drupal需要生成面包屑时,它就会首先找到你的函数,并使用它来代替默认的theme_breadcrumb()函数,这样面包屑中就会包含你的星号,而不再包含默认的双箭头了。很漂亮,不是么?通过theme()函数来管理所有的主题函数调用,如果当前主题覆写了任何一个theme_ 函数,那么Drupal将使用它们来代替默认的主题函数。开发者,请注意:在你的模块中任何需要输出HTML或者XML的部分都应该使用以“theme_”开头的主题函数,这样设计者就可以对它们进行覆写了。

Drupal版本:

覆写drupal模板文件

老葛的Drupal培训班 Think in Drupal

假定你和一个设计者一同工作,你告诉他/她“从代码中找到主题函数并对其进行覆写”,这是不是有点难为人了?幸运的是,有另一种方式,使得设计者能够更容易的修改外观。你可以将匹配的可主题化项目替换为它们自己的模板文件,我将通过大家熟悉的面包屑例子来说明这一点。
    在我们开始以前,首先确保没有主题函数对heme_breadcrumb()进行了覆写。所以,如果你在前面的一节中,在你主题的template.php文件里面创建了phptemplate_breadcrumb()函数的话,那么把它注释掉。接着创建文件sites/all/themes/custom/greyscale/breadcrumb.tpl.php。这是面包屑的新模板文件。因为我们想将<div>标签替换为<span>标签,继续前进,向该文件中添加以下内容:
 
<?php if (!empty($breadcrumb)): ?>
    <span class="breadcrumb"><?php print implode(' ! ', $breadcrumb) ?></span>
<?php endif; ?>
 
现在设计者就很容易编辑文件了。现在你需要告诉Drupal,在显示面包屑时调用这个模板文件。为了实现这一点,你需要导航到“管理➤站点构建 ➤模块”,来重新构建主题注册表。在重新构建主题注册表的时候,Drupal将找到你的breadcrumb.tpl.php文件,并将面包屑的可主题化项目映射到该模板文件上。
 

Drupal版本:

添加和操纵drupal模板变量

 

问题又来了:如果你可以创建你自己的模板文件并控制传递给它们的参数,那么你如何操纵或者添加传递给页面和节点模板文件的变量呢?
 
注意 只有实现为模板文件的可主题化项目,才有变量的聚合和传递一说。如果可主题化项目采用主题函数的实现方式,那么就不需要向其传递变量了。
 
每次加载一个模板文件都需要调用一系列的预处理函数。这些函数负责聚集变量,以将其传递给合适的模板文件。 让我们继续使用面包屑作为我们的例子。首先,让我们修改sites/all/themes/custom/greyscale/breadcrumb.tpl.php文件,为面包屑分隔符使用一个名为$breadcrumb_delimiter的变量:
 
<?php if (!empty($breadcrumb)): ?>
    <span class="breadcrumb">
        <?php print implode(' '. $breadcrumb_delimiter .' ', $breadcrumb) ?>
    </span>
<?php endif; ?>
 
    那么我们如何设置变量$breadcrumb_delimiter的值呢?一种可选的方式是,在模块中设置。我们可以创建文件sites/all/modules/custom/crumbpicker.info:
 
; $Id$
name = Breadcrumb Picker
description = Provide a character for the breadcrumb trail delimiter.
package = Pro Drupal Development
core = 6.x
 
    这个模块非常小,下面是文件sites/all/modules/custom/crumbpicker.module中的内容:
 
<?php
// $Id$
 
/**
 * @file
 * Provide a character for the breadcrumb trail delimiter.
 */
 
/**
 * Implementation of $modulename_preprocess_$hook().
 */
function crumbpicker_preprocess_breadcrumb(&$variables) {
    $variables['breadcrumb_delimiter'] = '/';
}
 
    导航到“管理➤站点构建 ➤模块”,启用这个模块,这样你的面包屑就变成了这个样子:首页/管理/站点构建。
    前面的例子说明了,如何使用模块来设置模板文件中的变量。如果每设置一个变量,都需要编写一个模块的话,那不是太麻烦了吗?有没有更简单的方式呢?当然有了,那就是使用template.php文件。让我们编写一个函数来设置面包屑分隔符。向你的主题的template.php文件中添加以下代码:
 
/**
 * Implementation of $themeenginename_preprocess_$hook().
 * Variables we set here will be available to the breadcrumb template file.
 */
function phptemplate_preprocess_breadcrumb(&$variables) {
    $variables['breadcrumb_delimiter'] = '#';
}
 
    这种方式与创建模块相比,更加简单明了,而模块的方式通常适用于在已有的模块中向模板提供变量;如果仅仅为了提供变量,就编写一个模块的话,那么就大材小用了。现在,我们使用模块提供了一个变量,还使用template.php文件中的函数提供了一个变量,那么实际中会使用哪个变量呢?
    实际上,预处理函数是有层级结构的,它们按照一定的顺序先后执行,后面的预处理函数可以覆写前面的预处理函数中定义的变量。在前面的例子中,面包屑的分隔符将会是#,这是因为phptemplate_preprocess_breadcrumb()放在crumbpicker_preprocess_breadcrumb()后面执行,前者就对后者中的$breadcrumb_delimiter变量进行了覆写。预处理函数的执行顺序,如图8-6所示。
    对于使用Greyscale主题的面包屑的主题化,实际的执行顺序(从前向后)如下:
 
template_preprocess()
template_preprocess_breadcrumb()
crumbpicker_preprocess()
crumbpicker_preprocess_breadcrumb()
phptemplate_preprocess()
phptemplate_preprocess_breadcrumb()
greyscale_preprocess()
greyscale_preprocess_breadcrumb()
 
    因此,greyscale_preprocess_breadcrumb()可以覆写已经设置的任意变量;它是在变量传递给模板文件以前,最后才调用的。如果这些函数中,只有几个实现了,那么调用所有的函数,会不会浪费时间呢?不错,在构建主题注册表的时候,Drupal将判定有哪些函数实现了,并且只调用这些实现了的函数。
 
8-6.预处理函数的执行顺序
 
注意预处理函数中,你还可以修改的一个变量是$vars['template_file'],它是Drupal将要调用的模板文件的名字。如果你需要基于一个更复杂的条件来加载另一个模板文件的话,那么你可以在这里修改这个变量。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

适用于所有drupal模板的变量

Drupal在为特定模板设置变量以前,它会预先设置一些通用变量:

 
• $zebra:它的值要么为odd,要么为even,它随着theme('node')的每次调用而不断切换,用来方便的控制节点列表的样式。
 
• $id:模板的编码,模板每被调用一次,它自增1。例如,theme('node')函数每调用一次,$id都会自增1。所以在一个包含了许多节点摘要的列表页面中,应用于节点模板(它负责节点摘要的主题化)的$id变量会随着节点摘要的增加而增加。
 
• $directory:这是指向主题的路径,比如themes/bluemarine(如果一个主题没有一个模板文件,那么该路径就指向提供模板文件的模块,比如modules/node)。
 
    如果数据库可用,并且站点不处于维护模式下,也就是大多数的时候,下面的变量将被预先设置:
 
• $is_admin: user_access('access administration pages')的返回结果
 
• $is_front: 当前页面为首页时,返回TRUE;否则,返回FALSE
 
• $logged_in: 当前用户已经登录时,返回TRUE;否则,返回FALSE
 
• $user: 全局$user对象(不要在主题中,未经安全处理就直接使用这个对象的属性;参看第20章)
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

page.tpl.php

如果你需要创建一个自定义的页面模板,那么你可以复制已有主题中的page.tpl.php文件,或者复制modules/system/page.tpl.php,复制完以后,再按照你的需要对它进行修改。实际上,一个最小的主题,所需要的文件仅仅是.info文件和style.css文件;如果你的主题中没有包含page.tpl.php文件,那么Drupal将使用modules/system/page.tpl.php。对于基本的主题,这可能就是你所想要的了。

    下面是在页面模板中可用的变量:
 
• $base_path: Drupal安装的基本路径。如果安装在根目录下,这是最简单的,它将默认为/。
 
• $body_classes: 一个使用空格分隔的CSS类名的字符串,它将应用于body元素中。使用这些CSS类,可以创建时髦的主题。例如,对于一个节点类型的页面,http://example.com/?q=node/3,$body_classes的值将会是“not-front logged-in page-node node-type-page one-sidebar sidebar-left”。
 
• $breadcrumb:返回页面上用于导航的面包屑的HTML。
 
• $closure:返回hook_footer()的输出,它通常显示在页面的底部。恰好在body的结束标签前面。hook_footer()允许模块在页面的尾部插入HTML或者JavaScript。注意,在hook_footer()中不能使用drupal_add_js()。
 
警告 $closure是一个非常重要的变量,它应该包含在所有的page.tpl.php文件中,这是因为,许多模块都依赖于这个变量。如果没有包含这个变量的话,那么这些模块可能就无法正常工作,因为它们将无法插入它们的HTML或者JavaScript。
 
• $content:返回将要显示的HTML内容。例如,它可以包含一个节点,一组节点,管理界面的内容,等等。
 
• $css:.返回一个数组结构,里面包含了将要添加到页面中的所有css文件。如果你需要$css数组的HTML版本,那么可以使用$styles。
 
• $directory:主题所在的相对路径。例如themes/bluemarine 或者 sites/all/themes/custom/greyscale.。你通常把这个变量和$base_path一起使用,来构建你的站点主题的绝对路径:
 
<?php print $base_path . $directory ?>
将转变为
<?php print '/' . 'sites/all/themes/custom/greyscale' ?>
 
• $feed_icons:为该页面返回RSS种子链接。可以通过drupal_add_feed()来添加RSS种子链接。
 
• $footer: 返回页脚区域的HTML,里面包含了该区域中所有区块的HTML。不要把它与hook_footer()混淆了,后者是一个Drupal钩子函数,用来向变量$closure中添加HTML或者JavaScript,而变量$closure显示在body结束标签的前面。
 
• $footer_message:返回页脚消息文本,可在管理界面“管理➤站点配置➤站点信息”中对它进行设置。
 
• $front_page: 不带参数的url()函数的输出;例如,/drupal/。在链接到一个站点的首页时,使用$front_page来代替$base_path,这是因为在多语言站点上,$front_page变量可以包含语言的域和前缀。
 
• $head:返回放置在<head></head>部分的HTML。模块可以通过调用drupal_set_html_head()来向$head添加额外的标识文本(markup)。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

page.tpl.php(1)

 

• $head_title:在页面标题中所显示的文本,放在HTML <title></title>标签中。可以使用drupal_get_title()来获取它。
 
• $header: 返回页首区域的HTML,里面包含了该区域中所有区块的HTML。
 
• $help: 帮助文本,大多数用于管理页面。模块可以通过实现hook_help()来填充这个变量。
 
• $is_front: 如果当前显示的为首页的话,返回TRUE。
 
• $language:一个对象,包含了当前站点语言的多个属性。例如,$language->language可能是en,而$language->name可能是English
 
• $layout:这一变量允许你定义不同布局类型的样式,而变量$layout的值则取决于已启用的边栏的数量。包括以下可能值:none, left, right, 和both。
 
• $left:返回左边栏的HTML,包含了该区域中所有区块的HTML。
 
• $logged_in: 当前用户已经登录时,返回TRUE;否则,返回FALSE
 
• $logo: 指向logo图片的路径,可以在已启用主题的配置页面中进行定义。它在Drupal的默认页面模板中,这样使用:
<img src="<?php print $logo; ?>" alt="<?php print t('Home'); ?>" />
 
• $messages: 返回消息的HTML,消息通常包括:表单的验证错误消息,表单提交成功的通知消息,以及其它各种消息。它通常显示在页面的顶部。
 
• $mission: 返回站点使命的文本,可在管理界面“管理➤站点配置➤站点信息”中输入.只有当$is_front为TRUE时,这个变量才可以使用。
 
• $node:整个节点对象,当查看一个单独的节点页面时可用。
 
• $primary_links: 一个包含了一级链接的数组。可在“管理➤站点构建➤菜单”中定义它们。通常$primary_links通过函数theme('links')来定制输出的样式,如下所示:
 
<?php
    print theme('links', $primary_links, array('class' => 'links primary-links'))
?>
 
• $right:返回右边栏的HTML,包含了该区域中所有区块的HTML。
 
• $scripts: 返回添加到页面的<script>标签中的HTML。jQuery也是通过它加载进来的(关于jQuery的更多信息,可参看第17章)
 
• $search_box: 返回搜索表单的HTML。如果管理员在已启用主题中的配置页面禁止了搜索的显示,或者禁用了搜索模块,那么$site_slogan为空。
 
• $secondary_links: 一个包含了二级链接的数组。可在“管理➤站点构建➤菜单”中定义它们。通常$secondary_links通过函数theme('links')来定制输出的样式,如下所示:
 
<?php
    print theme('links', $secondary_links, array('class' =>
        'links primary-links'))
?>
 
• $show_blocks: 这是theme('page', $content, $show_blocks, $show_messages)中的参数。它默认为TRUE;当它为FALSE时,用来填充左边栏和右边栏的$blocks变量将被设置为空,这样区块就无法显示了。
 
• $show_messages: 这是theme('page', $content, $show_blocks, $show_messages)中的参数。它默认为TRUE;当它为FALSE时,$messages变量(参看前面的$messages变量)将被设置为空,这样消息就无法显示了。
 
• $site_name: 站点的名称。在“管理➤站点配置➤站点信息”中设置。当管理员在已启用主题的配置页面中禁止显示时,$site_ name为空。
 
• $site_slogan: 站点的标语。在“管理➤站点配置➤站点信息”中设置。当管理员在已启用主题的配置页面中禁止显示时,$site_slogan为空。
 
• $styles:返回页面需要的CSS文件链接的HTML。可以通过drupal_add_css(),将CSS文件添加到变量$styles中去。
 
• $tabs: 返回标签(tab)的HTML,比如节点的View/Edit标签。在Drupal的核心主题中,标签通常位于页面的顶部。
 
• $template_files: 当前显示页面可用的模板文件名字的建议。这些名字没有包含扩展名,例如page-node, page-front。查找这些模板文件时,对于它们的默认顺序,可参看“多个页面模板”一节。
 
• $title:主内容标题,与$head_title不同。当查看一个单独的节点页面时,$title就是节点的标题。当常看Drupal的管理界面时,通常由菜单项来设置$title,菜单项对应于当前查看的页面。(菜单项的更多信息,可参看第4章).
 
警告 即便是你没有在page.tpl.php中使用区域变量($header, $footer, $left,$right,它们仍然会被构建。这是一个性能问题,因为Drupal将构建所有的区块,而只对于特定的页面视图,才将它们扔掉。如果自定义页面模板中不需要区块,除了从模板文件中排除该变量以外,还有一个更好的方式,那就是到区块的管理界面中去,禁止这些区块显示在你的自定义页面中去。关于在特定页面禁用区块的更多信息,可参看第9章。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

node.tpl.php

老葛的Drupal培训班 Think in Drupal

节点模板负责控制一个页面内部的单独的内容片段的显示。节点模板影响的不是整个页面,而仅仅是page.tpl.php中的变量$content。它们负责节点是以摘要视图的方式显示(当多个节点在同一个列表页面时),还是以主体视图的方式显示(当page.tpl.php中的变量$content仅包含一个节点,并且该节点单独成页时)。节点模板文件中的$page变量,当为主体视图时,它为真,当为摘要视图时,它为假。
    文件node.tpl.php是一个通用的模板,可用来处理所有节点的视图。如果你想要一个不同的模板,比如说日志节点模板与论坛节点模板不一致时,那该怎么办呢?你如何才能为特定的节点类型创建一个专有的模板,而不是全都使用一个通用的模板?
  节点模板很容易实现这一点。简单的将node.tpl.php复制一份并重命名为node-nodetype.tpl.php,这样PHPTemplate就会选择这个模板,而不是选择通用模板了。所以对于日志节点的主题化,只需要简单的使用node-blog.tpl.php就可以了。对于你通过管理界面“管理➤内容管理➤内容类型”创建的任何节点类型,都可以使用同样的方式创建一个单独的模板文件。在节点模板中,你可以使用下面的变量:
 
• $build_mode: 构建节点的上下文的相关信息。它的值是下面的常量之一:NODE_BUILD_NORMAL, NODE_BUILD_PREVIEW, NODE_BUILD_SEARCH_INDEX, NODE_BUILD_SEARCH_RESULT, or NODE_BUILD_RSS.
 
• $content: 节点的主体部分,如果是在一个列表页面中显示时,它为节点的摘要部分。
 
• $date: 格式化的节点创建日期。通过使用$created,比如, format_date($created, 'large'),你可以选择一个不同的日期格式。
 
• $links: 与节点相关的链接,比如“阅读全文” 或者“添加新评论”。模块通过实现hook_link()来添加额外的链接。这些链接已经经过了theme_links()的处理。
 
• $name: 该页面的作者的格式化的名称,链接到他/她的个人资料页面。
 
• $node:整个节点对象,还有它的所有属性。
 
• $node_url: URL路径中该节点的部分;例如,对于http://example.com/?q=node/3,它的值为/node/3。
 
• $page: 当节点独自显示为一个页面时,它为TRUE;否则,它为FALSE。
 
• $picture: 如果在管理界面“管理➤站点构建➤主题➤配置”中,选择了“文章中显示用户头像”,并在全局主题设置中的“显示文章的发布信息”选项中选中该节点类型,那么theme('user_picture', $node)的输出将被放到$picture中。
 
• $taxonomy.由节点的分类术语构成的一个数组,术语的格式适用于传递给theme_links()函数。事实上,theme_links()的输出可用在$terms变量中。

Drupal版本:

node.tpl.php(1)

老葛的Drupal培训班 Think in Drupal

• $teaser:布尔值,用来判定是否以摘要的方式显示。当它为假时,意味着节点采用主体方式显示,为真时,表示以摘要方式显示。
 
• $terms:与该节点相关的分类术语的HTML。每个术语都链接到它自己的分类术语页面。
 
• $title:节点的标题。当在多节点列表页面时,这里还会有个链接,指向该节点的主体视图页面。
 
• $submitted: 来自于theme('node_submitted', $node) 的“Submitted by”文本。管理员可以配置这一信息的显示,在一个基于单个节点类型的主题配置页面进行配置。
 
• $picture:用户头像的HTML,如果启用了头像并且设置了用户头像。
 
注意 因为节点属性和传递给节点模板的变量混合在了一起,所以节点属性也可以作为变量。对于节点属性列表,可参看第7章。直接使用节点属性可能会存在安全风险;如何将安全风险降到最低,可参看第20章。
 
    通常节点模板文件中的变量$content,并不会以你期望的方式来构建数据。当使用了一个扩展了节点属性的第3方模块时,比如CCK字段相关模块,这一点尤为明显。
 
 幸运的是,PHPTemplate将整个节点对象传递给了节点模板文件。如果你在你的模板文件中的顶部,使用下面的调试语句,并重新加载包含节点的页面,你将看到构成节点的所有属性。如果查看页面的源代码的话,读起来可能会更容易一些。
 
<pre>
    <?php print_r($node) ?>
</pre>
 
    现在你可以看到构成节点的所有部分了,直接访问它们的属性,像期望的那样为它们添加标识文本,而不需要再使用聚合变量$content了.
 
警告:在直接格式化一个节点对象时,你还必须为你站点的安全负责。参看第20章,学习如何将用户提交的数据包装在适当的函数中,以阻止XSS攻击
 

Drupal版本:

block.tpl.php

老葛的Drupal培训班 Think in Drupal

你可以在“管理站点构建区块”中查看所有的区块,而区块的外观则由block.tpl.php模板负责。如果你对区块不熟悉的话,可以参看第9章以获得更多信息。像页面模板和节点模板文件一样,区块系统按照一定的顺序来查找模板文件。先后顺序如下所示:
 
block-modulename-delta.tpl.php
block-modulename.tpl.php
block-region.tpl.php
block.tpl.php
 
    在上面的序列中,modulename是实现了该区块的模块的名称.例如,区块“在线用户”是由模块user.module实现的(假定区块的delta为1),下面是可用于该区块的模板文件的先后顺序:
 
block-user-1.tpl.php
block-user.tpl.php
block-left.tpl.php
block.tpl.php
 
    由站点管理员在后台创建的区块,它们都与区块模块绑定在了一起,所以它们在前面序列中的modulename都为block。如果你不知道那个模块实现了给定的区块,通过做一些PHP调试,你可以找到所有的原始信息。通过在你的block.tpl.php的顶部键入下面一段代码,你可以输出当前页面启用的每个区块的整个区块对象。
 
<pre>
    <?php print_r($block); ?>
</pre>
 
         如果你查看页面的源文件的话,阅读起来会更容易一些。下面是区块“在线用户”所显示的:
 
stdClass Object
(
    [bid] => 42
    [module] => user
    [delta] => 3
    [theme] => bluemarine
    [status] => 1
    [weight] => 0
    [region] => footer
    [custom] => 0
    [throttle] => 0
    [visibility] => 0
    [pages] =>
    [title] =>
    [cache] => -1
    [subject] => Who's online
    [content] => There are currently ...
)
 
         现在你得到了该区块的所有细节了,根据你需要覆盖的范围,你可以非常容易的构建一个或多个如下所示的区块模板文件:
 
block-user-3.tpl.php // Target just the Who's Online block.
block-user.tpl.php // Target all block output by user module.
block-footer.tpl.php // Target all blocks in the footer region.
block.tpl.php // Target all blocks on any page.
 
下面是在区块模板文件中你可以使用的默认变量:
 
• $block:整个区块对象。一般情况下,你会使用$block->subject和$block->content;具体的示例可参看核心主题中的block.tpl.php文件。
 
• $block_id:一个整数,当每次生成一个区块和调用一次区块模板文件时,自增1。
 
• $block_zebra: 当$block_id自增时,该变量不断的在“odd”和“even.”之间切换。
 

Drupal版本:

comment.tpl.php

老葛的Drupal培训班 Think in Drupal

模板文件comment.tpl.php负责评论的外观显示。下面是评论模板文件中可以使用的变量:
 
• $author: 带有超链接的作者名,如果他/她有一个个人资料页面的话,那么将链接到该页面。
 
• $comment: 评论对象,包含了所有的评论属性。
 
• $content: 评论的内容。
 
• $date: 格式化的发布日期。通过调用format_date(),比如, format_date($comment->timestamp, 'large'),你可以选择一个不同的日期格式。
 
• $links: 与评论相关的上下文链接的HTML,比如“编辑”, “回复”, 和 “删除”。
 
• $new: 对于一个当前登录用户,它将为未读评论返回一个“new”,为一个更新过的评论返回“updated”。通过覆写includes/theme.inc中的theme_mark(),你可修改从$new中返回的文本。对于匿名用户,Drupal将不会为其追踪评论是否读过或者修改过。
 
• $node:这个评论所对应的节点的整个节点对象。
 
• $picture: 用户头像的HTML。你必须在“管理➤用户管理➤用户设置”中启用头像图片支持,你还必须为每个启用的主题,在其配置页面上选中复选框“评论中作者头像”。最后,要么站点管理员上传一个默认图片,或者用户也需要上传一个图片,这样就有可显示的图片了。
 
• $signature: 经过过滤的用户签名HTML。如果你想使用这个变量的话,那么需要在“管理➤用户管理 ➤用户设置”中启用签名支持。
 
• $status: 反映评论的状态,有以下可能值:comment-preview, comment-unpublished,和comment-published。
 
• $submitted: 带有用户名和日期的“Submitted by”字符串,由theme('comment_submitted', $comment)输出。
 
• $title: 带有超链接的标题,链接指向该评论,并且包含URL片段。
 

Drupal版本:

box.tpl.php

 

模板文件 box.tpl.php是Drupal内部最容易引起歧义的模板文件了。它用在Drupal核心中,用来包裹评论的提交表单和查询结果。除了这些,就很少用到它了。它对区块不起作用,这可能是个容易混淆的地方(这是因为由管理员创建的区块,保存在数据库中名为boxes的表中).在盒子模板中,你可使用的默认变量如下所示:
 
• $content:盒子的内容
 
• $region:盒子所在的区域。例如包括header, left,和main。
 
• $title: 盒子的标题。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

其它的.tpl.php文件

老葛的Drupal培训班 Think in Drupal

到目前为止,我们所讲到的模板都是最常用的模板。但是还有许多其它模板可用。为了查看它们,你可以浏览modules目录,在那里查找以.tpl.php结尾的文件。例如,modules/forum下面包含了6个这样的文件。这些文件都包含了很好的文档,你可以直接将它们拷贝到你的自定义主题所在的目录中,根据需要对其进行修改。这比从头创建一个模板要高效很多!
 

Drupal版本:

多页面模板

老葛的Drupal培训班 Think in Drupal

当你想为站点上的不同页面创建不同的布局时,而一个单独的页面布局不能再满足所有需要了,你该怎么办呢?下面有一些好的实践,用来创建其它的页面模板。
  你可以基于站点的当前系统URL,来创建其它的页面模板文件。例如,如果你访问页面http://example.com/?q=user/1,那么PHPTemplate将以下面的顺序来查找页面模板,这里假定你使用的Greyscale主题:
 
sites/all/themes/custom/greyscale/page-user-1.tpl.php
modules/system/page-user-1.tpl.php
sites/all/themes/custom/greyscale/page-user.tpl.php
modules/system/page-user.tpl.php
sites/all/themes/custom/greyscale/page.tpl.php
modules/system/page.tpl.php
 
    PHPTemplate一旦找到一个要包含的模板文件,那么它将会停止查找。page-user.tpl.php适用于所有的用户页面,而page-user-1仅仅可以用在URL为user/1, user/1/edit,等等的页面。如果Drupal在主题系统中的任何位置都找不到页面模板,那么将会调用内置的模板modules/system/page.tpl.php。
 
注意 在这里,Drupal使用的是内部的URL,所以即便是使用了path或者pathauto模板(这两个模块允许你创建URL别名),对于页面模板,你仍然需要使用Drupal的内部URL,而不是使用别名。
   
    让我们使用节点编辑页面http://example.com/?q=node/1/edit作为示例。下面是PHPTemplate查找的页面模板文件的顺序:
 
sites/all/themes/custom/greyscale/page-node-edit.tpl.php
modules/system/page-node-edit.tpl.php
sites/all/themes/custom/greyscale/page-node-1.tpl.php
modules/system/page-node-1.tpl.php
sites/all/themes/custom/greyscale/page-node.tpl.php
modules/system/page-node.tpl.php
sites/all/themes/custom/greyscale/page.tpl.php
modules/system/page.tpl.php
 
    如果你是一个Drupal模块程序员的话,通过学习前面的例子,你应该可以很方便的为你的模块提供一个默认模板;具体的示例,你可以到modules目录下看看。
 
提示 如果想为你站点的首页创建一个自定义页面模板,那么只需要简单的创建一个名为page-front.tpl.php的模板文件就可以了
 

Drupal版本:

高级Drupal主题化

老葛的Drupal培训班 Think in Drupal

如果你想深入的了解Drupal主题化的工作原理的话,有两个重要的方面是需要掌握的。首先,我们先开始学习一下主题系统的背后的引擎:主题注册表。接着,我们对theme()函数逐步进行分析,这样你就可以掌握它的工作原理,并知道在什么地方进行对它覆写了。
 
主题注册表
    主题注册表是Drupal用来追踪所有的主题化函数和模板的地方。在Drupal中,每一个可主题化的项目都是通过函数或者模板来主题化的。当Drupal构建主题注册表时,它为每一项查找和匹配相关信息。这意味着这一过程不需要发生在运行时,从而让Drupal跑得更快。
 

Drupal版本:

注册表是如何构建的

老葛的Drupal培训班 Think in Drupal

当启用一个新的主题时,就会重新构建主题注册表,此时它按照下面的顺序来查找主题钩子:
 
1.首先,它查找有哪些模块实现了hook_theme(),从而找到这些模块提供的主题函数和模板文件。
 
2. 如果该主题是基于另一个主题的,那么首先会调用基主题引擎中的hook_theme()实现。例如,Minnelli是一个基于Garland的主题。它的基主题的主题引擎为PHPTemplate。所以会调用phptemplate_theme()来查找前缀为phptemplate_或garland_的主题函数,以及基主题目录下按照特定方式命名的模板文件。例如,模板文件themes/garland/node.tpl.php就添加到了这里。
 
3. 调用该主题的hook_theme()实现。所以,对于Minnelli,将会调用phptemplate_theme()来查找前缀为phptemplate_ 或minnelli_的主题函数,以及该主题目录下的模板文件。所以,如果Minnelli在themes/garland/minnelli/node.tpl.php提供了一个节点模板,那么它就会被发现。
 
    注意,在每一步中,新发现的主题函数和模板文件,能够覆写注册表中已有的主题函数和模板文件。这就是继承的机制,它允许你覆写任意的主题函数或者模板文件。
    让我们更进一步的检查一下,在模块中是如何实现hook_theme()的。这个主题钩子的任务是,返回一个包含可主题化项目的数组。当一个项目通过主题函数主题化时,函数的参数也会被包含进来。例如,面包屑是由函数theme_breadcrumb($breadcrumb)负责主题化的。在一个假定的foo.module模块的主题钩子中,通过下面的方式来说明面包屑是可以主题化的:
 
/**
 * Implementation of hook_theme().
 */
foo_theme() {
    return array(
        'breadcrumb' => array(
            'arguments' => array ('breadcrumb' => NULL),
        );
    );
}
 
    如果传递过来的参数为空,那么就使用NULL作为默认值。现在你已经描述了该项的名字和它的参数,并给出了参数的默认值。如果你的主题函数,或者模板预处理函数是包含在另外的一个文件中的,那么你需要使用file键把它包含进来:
 
/**
 * Implementation of hook_theme().
 */
function user_theme() {
    return array(
        'user_filter_form' => array(
            'arguments' => array('form' => NULL),
            'file' => 'user.admin.inc',
        ),
        ...
    );
}
 
    如果你需要声明,一个可主题化项目使用的是模板文件,而不是主题函数的话,你可以在主题钩子中定义模板文件的名字(不带扩展名.tpl.php):
 
/**
 * Implementation of hook_theme().
 */
function user_theme() {
    return array(
        'user_profile_item' => array(
            'arguments' => array('element' => NULL),
            'template' => 'user-profile-item',
            'file' => 'user.pages.inc',
        ),
        ...
    );
}
 
    在前面的user_profile_item例子中,模板文件是通过template键指定的,可以在modules/user/user-profile-item.tpl.php找到。模板的预处理函数位于modules/user/user.pages.inc中,名为template_preprocess_user_profile_item()。template_preprocess()中定义的变量,以及在键arguments中定义的变量$element,都会被传递给template_preprocess_user_profile_item()。变量$element的值,在显示期间被指定。

Drupal版本:

逐步分析theme()函数

在本节中,我们将深入幕后,我们将学习theme()实际是如何工作的。假定当前主题为Drupal的核心主题Bluemarine,并进行了下面的主题调用,让我们逐步分析一下调用的执行路径。

 
theme('node', $node, $teaser, $page)
 
    首先,Drupal通过第一个参数来得知当前是什么东西正被主题化。在这里,它是“node”,所以Drupal将在主题注册表中为“node”查找相应的条目。它找到的注册表条目,和图8-7中所给的类似。
8-7.主题为Bluemarine时,节点的注册表条目
 
    如果注册表路径里有一个文件条目,那么Drupal将为该文件运行include_once(),从而将一些需要的主题函数包含进来,但是在这里,没有这样的条目。
    Drupal将检查这个主题调用是由一个函数处理的,还是由一个模板文件处理面的。如果该调用是由函数负责处理的,Drupal将简单的调用这个函数并返回输出。但是在本次调用中,因为在注册表条目中没有定义函数,所以Drupal将准备一些变量,以将它们传递给一个模板文件。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

逐步分析theme()函数(1)

老葛的Drupal培训班 Think in Drupal

首先,传递给theme()函数的参数现在可以使用了。在这里,传递的参数有$node, $teaser, 和$page。所以,对于注册表参数条目中所列的每个参数,Drupal将为其分配一个对应的变量:
 
$variables['node']   = $node;
$variables['teaser'] = $teaser;
$variables['page']   = $page;
 
    接着,默认的呈现函数将设置为theme_render_template(),而默认的扩展名将设置为.tpl.php(PHPTemplate模板的标准文件扩展名)。呈现函数负责将变量传递给模板文件,一会儿你就会看到。
    模板中所用的其它变量则由模板预处理函数提供。首先,对于所有的主题化项目,不管该项目是一个节点,区块,面包屑,或者其它你有的东西,都会调用template_preprocess(),。而第2个预处理函数则是特定于正在呈现的项目的(在这里就是一个节点)。图8-7显示,为节点定义了两个预处理函数,这样调用它们:
 
template_preprocess($variables, 'node');
template_preprocess_node($variables, 'node');
 
    第一个函数就是template_preprocess()。你可以在http://api.drupal.org/api/function/template_preprocess/6中查看这个函数的代码,或者也可以直接在includes/theme.inc中查看。这个函数用来设置所有模板中都适用的变量(参看“实用于所有模板的变量”一节)。
   预处理函数是成对儿出现的。第2个预处理函数名称的最后部分,对应于当前正被主题化的项目。template_preprocess()运行完以后,现在就运行template_preprocess_node()了。它添加了以下变量:$taxonomy, $content, $date, $links, $name, $node_url, $terms, 和$title。这显示在template_preprocess_node()的代码中。注意,数组$variables中的每一个条目,都将变成一个单独的变量,以供模板文件使用。例如,对于$variables['date'],在模板文件中,将简单的作为$date来使用:
 
/**
 * Process variables for node.tpl.php
 *
 * Most themes utilize their own copy of node.tpl.php. The default is located
 * inside "modules/node/node.tpl.php". Look in there for the full list of
 * variables.
 *
 * The $variables array contains the following arguments:
 * $node, $teaser, $page
 */
function template_preprocess_node(&$variables) {
    $node = $variables['node'];
    if (module_exists('taxonomy')) {
        $variables['taxonomy'] = taxonomy_link('taxonomy terms', $node);
    }
    else {
        $variables['taxonomy'] = array();
    }
 
    if ($variables['teaser'] && $node->teaser) {
        $variables['content'] = $node->teaser;
    }
    elseif (isset($node->body)) {
        $variables['content'] = $node->body;
    }
    else {
        $variables['content'] = '';
    }
 
    $variables['date'] = format_date($node->created);
    $variables['links'] = !empty($node->links) ?
        theme('links', $node->links, array('class' => 'links inline')) : '';
    $variables['name'] = theme('username', $node);
    $variables['node_url'] = url('node/'. $node->nid);
    $variables['terms'] = theme('links', $variables['taxonomy'],
    array('class' => 'links inline'));
    $variables['title'] = check_plain($node->title);
 
    // Flatten the node object's member fields.
    $variables = array_merge((array)$node, $variables);
    ...
}
 
    关于这些变量都是干什么的,可以参看本章前面的部分。
    在分配了变量以后,一些疯狂的事情发生了。节点本身从一个对象转化为了一个数组,并与已有的变量合并在了一起。所以,所有的节点属性现在都可以应用在模板文件中了,只需要在属性名前面加个前缀$就可以了。例如,$node->nid现在就可作为$nid来使用。如果一个节点的属性和一个变量拥有相同的名字,那么变量优先。例如,$title包含了$node->title的普通文本版本。当合并发生时,将会在模板文件中使用这个普通文本版本。注意,原始的标题仍然可以通过$variables['node']->title来访问,不过在使用它以前,为了安全考虑,你需要对其进行过滤(参看第20章)。
    好了,Drupal现在运行完了预处理函数。现在需要做一个决定:将所有的这些变量传递给哪个模板文件呢?也就是为节点使用哪个模板文件呢?为了做出决定,Drupal进行以下检查:
 
1. 是不是在$variables['template_files']中定义了一些模板文件?这里定义的条目都是Drupal将要查找的模板文件的名字。在我们的例子中,节点的类型为story,所以node-story定义在这里;Drupal首先会匹配一个特定内容类型的模板文件,找不到的话再使用通用的节点模板。更多详细,参看http://drupal.org/node/190815
 
2.是否设置了$variables['template_file']?如果设置了,优先采用。
 
    函数drupal_discover_template()负责决定采用哪个模板文件。首先,它会找到主题注册表条目中定义的主题路径,然后在这些路径下查找模板文件。在我们的这种情况下,它首先会查找themes/bluemarine/node-story.tpl.php,接着查找modules/node/node-story.tpl.php。如果一个文件也不存在的话(在我们的例子中,一个也不存在:节点模块没有在它的目录中提供特定节点类型的模板文件,而Bluemarine也没有为story节点单独提供一个模板----只有一个通用的节点模板),那么第一轮的模板查找就失败了。接着,Drupal将路径,模板文件,和扩展名串联起来,并对其进行检查:themes/bluemarine/node.tpl.php。嗯,这个文件存在,接着Drupal将调用呈现函数(记住,那就是theme_render_template()),并将选择的模板文件和变量数组传递过来。
    呈现函数将变量传递给模板并执行它,然后将结果返回。在我们的例子中,结果就是执行了themes/bluemarine/node.tpl.php所返回的HTML。

Drupal版本:

自定义drupal区块区域

老葛的Drupal培训班 Think in Drupal

区域是在Drupal的主题中放置区块的地方。通过Drupal的管理界面“管理➤站点构建➤区块”,你可以将区块指定到区域中,并管理它们。
尽管你可以创建你想要的任意区域,但是在主题中,默认的区域有left, right, content, header, 和footer。一旦声明了区域以后,你就可以在页面模板文件中(例如,page.tpl.php)将其作为变量使用了。例如,对于header区域,可以使用<?php print $header ?>。通过在你主题的.info文件中进行定义,你可以创建其它的区域。
 
Drupal表单的主题化
修改Drupal表单的外观不像创建一个模板文件那样容易,因为Drupal中的表单依赖于它们自己的API。关于表单主题化的具体细节,可参看第10章。
 
使用主题开发者模块
    用于制作Drupal主题的一个非常宝贵的资源就是主题开发者模块。它是devel.module的一部分,你可以在http://drupal.org/project/devel下载到它。主题开发者模块,能够让你指定一个页面元素,来查看生成该元素所用到的模板文件和主题函数,以及对于该元素都有哪些变量(和它们的值)可用。图8-8给出了使用该模块的示例。
 
8-8.主题开发者模块
 

Drupal版本:

drupal主题系统 总结

老葛的Drupal培训班 Think in Drupal

读完本章以后,你应该可以
• 理解主题引擎和主题是什么
• 理解Drupal中PHPTemplate的工作原理
• 创建模板文件
• 覆写主题函数
• 操纵模板变量
• 为区块创建新的页面区域
 

Drupal版本:

第9章 Drupal区块

老葛的Drupal培训班 Think in Drupal

区块就是文本片段或者功能片段,它通常位于一个网站的主内容区域之外,比如左边栏,右边栏,页首,页尾,等等。如果你曾经登录过一个Drupal站点,或者访问过一个Drupal的管理界面,那么你就用过区块。区块的访问权限和放置位置通过后台管理界面来控制,这简化了开发者创建区块时的工作量。区块配置页面位于“管理➤站点构建➤区块”(http://example.com/?q=admin/build/block).。

Drupal版本:

什么是区块?

 

区块包含一个标题和一个描述,主要用于广告、代码片段和状态指示器,它不适用于主内容片段;因此,区块不是节点,它与节点有着不同的规则。节点可以具有多种功能:修订本控制,完善的访问权限,附带评论的能力,RSS种子和分类术语,等等;它们通常用于一个站点的主内容部分。
       区域是站点上用来放置区块的部分。区域的创建和显示是由主题(位于主题的.info文件中)负责的,而不是通过区块API来定义。没有指定区域的区块将不会显示出来。
       可以通过配置选项来控制谁可以访问区块,以及区块出现在站点的哪些页面。如果启用了节流阀模块,当访问量超过一定阀值时,一些不重要的区块,将被自动关闭。区块列表页面如图9-1所示。
       可以通过Drupal管理界面创建区块(称作自定义区块),也可以通过区块API用代码创建区块(称作模块区块)。当你创建区块时,该选用哪种方法呢?一次性的区块,比如一个与站点相关的一小段静态HTML,那么适用于自定义区块。如果区块本身具有动态性,与你编写的模块相关,或者里面大部分都是php代码,那么可以使用区块API通过模块来创建。由于代码存放在数据库中比写在模块中更难于维护,所以尽量不要在自定义区块中使用php代码,一个站点的编辑人员可能对此并不了解,他可能会偶然的不经意间将大量工作轻易的删掉。退一步讲,如果使用模块创建区块过于笨重,而又不得不使用php代码时,那么可以在区块中调用一个自定义函数,而将函数存放在别处。
 
图9-1当启用了节流阀模块时,区块列表页面显示了节流阀选项
 
注意 对于特定站点的区块和其它组件的一个常用实践:创建一个特定于站点的模块,将站点的自定义函数放在该模块里面。例如,为Jones Pies and Soda公司开发网站的程序员,他就可以了创建一个jonespiesandsoda模块。
 
       尽管区块API很简单,并且只有单个钩子函数hook_block(),但是在这一框架下,你可以实现许多复杂的事情。区块可以显示你所想要的任何东西(这是因为,由于它们是用php实现的,所以在功能上没有限制)。尽管如此,它们通常扮演一个对站点主内容进行支撑的角色。比如,你可以为每一个用户角色创建一个自定义导航区块,或者你可以在区块中列出等待批准的评论。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

drupal区块配置选项

老葛的Drupal培训班 Think in Drupal

开发人员通常不需要担心区块的可见性,因为可以通过区块管理界面“管理➤站点构建➤区块”来处理大多数的情况。使用如图9-2所示的界面,你可以控制以下选项:
 
       特定用户可见性设置:管理员可以允许个人用户在他们的帐号设置中定制给定区块的可见性。用户可以通过单击他们的“我的帐号”链接来修改区块的可见性。
 
       特定角色可见性设置:管理员可以选择让区块仅对具有特定角色的用户可见。
 
       特定页面可见性设置:管理员可以选择让区块仅对特定页面或者特定范围内的页面可见或者隐藏,或者在你的php代码返回值为真的情况下可见。
 
图9-2管理界面中区块配置选项截图

Drupal版本:

drupal区块位置

老葛的Drupal培训班 Think in Drupal

我在前面提到,在Drupal的区块管理页面中,站点管理员可以选择将区块放置在哪个区域中。在同一页面,他们还可以对同一区域内的不同区块进行排序,如图9-1所示。区域是通过主题层的.info文件定义的,而不是通过区块API,而且不同的主题可以有不同的区域。关于创建区域的更多信息,参看第8章。
 

Drupal版本:

定义drupal区块

老葛的Drupal培训班 Think in Drupal

可以在模块中使用钩子hool_block()来定义区块,而且一个模块可以在单个钩子中实现多个区块。一旦区块定义好了以后,那么它将会出现在区块管理页面。另外,站点管理员可以通过后台管理界面来手工的创建自定义区块。在本节中,我们主要讨论如何通过代码创建区块。让我们看一下区块的数据库模式,如图9-3所示。
 
9-3 区块的数据库模式
 
       每个区块的区块属性都存储在表blocks里面。通过区块配置界面为区块创建的其它数据,比如它们的内容和输入格式类型,都存放在表boxes里面。最后,表blocks_roles存放每个区块的基于角色的权限。下面的属性定义在表blocks的列中:
 
• bid:这是每个区块的唯一的标识ID。
 
module:这一字段存放的是定义区块的模块的名称。比如,用户登录区块是在用户模块中定义的,等等。对于管理员在“管理➤站点构建➤区块”中创建的自定义区块,它们将被认为是由区块模块创建的。
 
delta:由于一个模块可以在钩子hook_block()中定义多个区块,所以delta存放了每个区块的键,它们在钩子hook_block()范围内是唯一的,但对于整个站点的所有区块则不一定唯一。delta可以是整数,也可以是字符串。
 
theme:一个区块可以用于多个主题。因此,Drupal需要存放启用了该区块的主题的名称。启用了该区块的每个主题,在数据库中都有一行自己的记录。配置选项不能在主题之间共享。
 
status:它用来追踪区块是否被启用。1意味着启用,0意味着禁用。如果一个区块,没有为其指定一个区域,那么Drupal会将其状态设置为0.
 
• weight:区块的重量,用来决定区块在区域中的相对位置。
 
• region: 放置该区块的区域的名字,例如,页脚。
 
• custom: 这是这个区块的“特定用户可见性设置”的值(参看图9-2)。0意味着用户不能控制该区块的可见性;1意味着该区块默认是显示的,但是用户可以隐藏它;2意味着该区块默认是隐藏的,但是用户可以选择显示它。
 
throttle:当节流阀模块启用时,该字段用于追踪哪些区块可被节流。0意味着禁用节流,1意味着启用。节流阀模块能够自动的探测访问流量,当流量超过了阀值,它能临时的禁用一些耗费资源的区块(更多详细,可参看第22章)。
 
• visibility: 这个值用来表示如何判定区块的可见性。0意味着区块将显示在除所列页面以外的所有页面;1意味着区块只显示在所列页面;2意味着,Drupal将执行一段由管理员定义的PHP代码,以判定区块的可见性。
 
• pages: 该字段的内容依赖于visibility字段中的设置。如果visibility字段的值为0或1,那么该字段将包含一列Drupal路径。如果visibility字段的值为2,那么该字段将包含一段PHP代码,通过对其计算来判定是否需要显示区块。
 
• title:这是区块的自定义标题。如果这个字段为空,那么将使用区块的默认标题(由区块所在的模块提供)。如果这个字段包含的为<none>,那么该区块就没有标题。否则,该字段的文本将用作区块的标题。
 
• cache: 这个值用来判定Drupal是如何缓存该区块的。–1意味着区块不被缓存。1意味着为每个角色缓存区块,如果没有声明缓存设置,那么这是Drupal区块的默认设置。2意味着为每个用户缓存区块。4意味着为每个页面缓存区块。8意味着区块将被缓存,但是不管是什么角色,什么用户,什么页面,缓存的方式都是一样的。
 

Drupal版本:

理解drupal区块的主题化

老葛的Drupal培训班 Think in Drupal

在一个页面请求周期内,主题系统向区块系统请求返回每个区域内的一列区块。当为页面模板文件(通常为page.tpl.php)变量赋值时,完成这一步骤。对于每个区域(比如,页脚区域),为了聚集主题化的区块,Drupal执行以下类似的代码:
$variables['footer'] = theme('blocks', 'footer');
    你可能还记得第8章里面的theme(“block”),它实际上就是调用方法theme_blocks()。下面是theme_blocks()的代码:
/**
 * Return a set of blocks available for the current user.
 *
 * @param $region
 * Which set of blocks to retrieve.
 * @return
 * A string containing the themed blocks for this region.
 */
function theme_blocks($region) {
    $output = '';
 
    if ($list = block_list($region)) {
        foreach ($list as $key => $block) {
            $output .= theme('block', $block);
        }
    }
 
    // Add any content assigned to this region through drupal_set_content() calls.
    $output .= drupal_get_content($region);
 
    return $output;
}
 
    在前面的代码中,我们对每个给定区域中的区块进行迭代,并为每个区块执行一个主题函数调用,一般最终会调用block.tpl.php文件。有关主题化的原理,以及如何覆写单个区块的外观,参看第8章。最后,我们为调用代码返回该区域内所有主题化的区块。
 

Drupal版本:

使用drupal区块钩子

老葛的Drupal培训班 Think in Drupal

在用代码创建区块时,我们在区块钩子hook_block()中处理所有的逻辑。通过这个钩子,你可以创建单个区块或者一组区块。任何模块都可以通过实现钩子hook_block()来创建区块。让我们看一下函数签名:
function hook_block($op = 'list', $delta = 0, $edit = array())
 
参数列表
    区块钩子中使用的参数将在下面讨论。
 
$op
    这一参数用于定义区块所经过的阶段。通过传递参数$op来定义一个操作阶段,这一模型在Drupal框架中是很常用的---例如hook_nodeapi()和hook_user()中都用到了这一模型。$op的可能值如下:
 
list:返回一个数组,里面包含了该模块定义的所有区块。数组的键就是delta(在本模块定义的所有区块中,它是唯一的标识符)。每个数据元素的值,还是一个数组,里面提供了区块的重要数据。list的可能值和默认值,如下所示:
 
    info:这个值是必须的。一个可翻译的字符串(例如,使用t()封装),为站点管理员提供了一个合适的区块描述。
 
    cache: 这个区块如何被缓存?可能的值有LOCK_NO_CACHE (不缓存区块), BLOCK_CACHE_PER_ROLE (为每个角色缓存区块),    BLOCK_CACHE_PER_USER (为每个角色缓存区块----站点用户多了最好不要用这种方式!), BLOCK_CACHE_PER_PAGE (为每个页面缓存区块), 和BLOCK_CACHE_GLOBAL (缓存一次区块,所有的都一样)。
 
    status:区块默认应该被启用吗----True 还是 FALSE?默认为FALSE。
 
    region:可能为区块设置的默认区域。当然,管理员可以将区块移到一个不同的区域中。只有当状态值为TRUE时,区域的值才会有效;如果区块未被启用,那么区域将被设置为None。
 
    weight:它控制着区块在它的区域内的放置次序。重量越小,位置越靠前,水平方向是靠左,垂直方向是靠上。重量越大,越靠后。默认值为0.
 
    pages:定义区块所在的默认页面。默认是一个空字符串。pages的值包含了通过换行分隔的Drupal路径。*为通配符。例如,路径blog为日志首页,而blog/*则为个人日志页面。<front>代表首页。
 
    custom:TRUE意味着这是一个自定义区块,而FALSE则意味着它是通过模块实现的区块。
 
    title:默认区块标题。
 
    configure:返回一个数组,里面包含了用于特定区块设置的表单字段定义。它与区块配置页面的表单合并在了一起,从而使你能够扩展区块的配置方式。如果你实现了这个操作,那么你还需要实现save操作 (参看下一项)。
 
    save:在配置表单提交时被调用。当你的模块可以保存你在configure操作中收集的自定义区块配置信息时,使用该操作。你想保存的数据包含在参数$edit中。它不需要返回值。
 
    view:区块正被显示。返回一个包含了区块标题和内容的数组。
 
$delta
    这是要返回的区块ID。你可以为$delta使用一个整数或者一个字符串。注意当参数$op为list时,因为delta是在list操作中定义的,所以$delta此时将被忽略。
 
$edit
    当$op为save时,$edit包含了从区块配置表单提交过来的表单数据。
 

Drupal版本:

创建一个drupal区块(1)

老葛的Drupal培训班 Think in Drupal

在这个例子中,你将创建两个区块,它们使得内容审核更易于管理。首先,你将创建一个区块用来列出等待批准的评论,然后你将创建一个区块用来列出未发布的节点。两个区块都为其中的每个待审核内容提供了链接,用来指向内容编辑表单页面。
       让我们创建一个名为approval.module的模块,它将包含我们的区块代码。在路径sites/all/modules/custom下面创建一个名为approval的文件夹(如果modules和custom不存在的话,你需要创建它们)。
       接下来,向文件夹中添加approval.info文件:
 
; $Id$
name = Approval
description = Blocks for facilitating pending content workflow.
package = Pro Drupal Development
core = 6.x
 
接着,再添加approval.module文件:
<?php
// $Id$
 
/**
 * @file
 * Implements various blocks to improve pending content workflow.
 */
 
    当你创建好这些文件后,在“管理➤站点构建 ➤模块”下面启用该模块。你还会用到approval.module,所以不要关闭文本编辑器。
       让我们添加区块钩子并实现list操作,这样,我们的区块就会出现在区块管理页面上的区块列表中(参看图9-4)。

Drupal版本:

创建一个drupal区块(2)

 

/**
 * Implementation of hook_block().
 */
function approval_block($op = 'list', $delta = 0, $edit = array()) {
    switch ($op) {
        case 'list':
            $blocks[0]['info'] = t('Pending comments');
            $blocks[0]['cache'] = BLOCK_NO_CACHE;
            return $blocks;
    }
}
 
9-4 在区块列表页面,你可以看到你创建的区块“Pending comments”了,它位于Disabled标题下面。它现在可被指定到一个区域中。
 
    注意数组的info键不是区块启用时所显示给用户的区块标题。而是一个仅仅出现在区块列表页面中的描述。你将在接下来的view操作中实现真正的区块标题。首先,你需要创建其它的配置选项,为了实现这一点,需要实现configue操作,如下面的代码所示。你创建了一个新的表单字段,当你点击区块列表页面中区块右边的配置链接时,即可看到这个字段,如图9-5所示。
 
function approval_block($op = 'list', $delta = 0, $edit = array()) {
    switch ($op) {
        case 'list':
            $blocks[0]['info'] = t('Pending comments');
            $blocks[0]['cache'] = BLOCK_NO_CACHE;
            return $blocks;
 
       case 'configure':
           $form['approval_block_num_posts'] = array(
              '#type' => 'textfield',
              '#title' => t('Number of pending comments to display'),
              '#default_value' => variable_get('approval_block_num_posts', 5),
           );
           return $form;
    }
}
 
9-5 带有区块自定义字段的区块配置表单

老葛的Drupal培训班 Think in Drupal

Drupal版本:

创建一个drupal区块(3)

 

当如图9-5所示的区块配置表单被提交后,它将触发下一个$op,这就是save。你将使用它来保存表单字段的值。
 
function approval_block($op = 'list', $delta = 0, $edit = array()) {
    switch ($op) {
        case 'list':
            $blocks[0]['info'] = t('Pending comments');
            $blocks[0]['cache'] = BLOCK_NO_CACHE;
            return $blocks;
 
        case 'configure':
            $form['approval_block_num_posts'] = array(
            '#type' => 'textfield',
            '#title' => t('Number of pending comments to display'),
            '#default_value' => variable_get('approval_block_num_posts', 5),
            );
            return $form;
 
       case 'save':
           variable_set('approval_block_num_posts',
              (int)$edit['approval_block_num_posts']);
           break;
    }
}

老葛的Drupal培训班 Think in Drupal

Drupal版本:

创建一个drupal区块(4)

 

通过使用Drupal自带的变量系统的函数variable_set(),你将区块中显示的待定评论的数目保存了下来。注意这里使用了类型转换,将其转换为了整数,目的是对数据进行明智检查。最后添加view操作,当区块显示时,返回一个待定评论列表。
 
function approval_block($op = 'list', $delta = 0, $edit = array()) {
    switch ($op) {
        case 'list':
            $blocks[0]['info'] = t('Pending comments');
            return $blocks;
 
        case 'configure':
            $form['approval_block_num_posts'] = array(
                '#type' => 'textfield',
                '#title' => t('Number of pending comments to display'),
                '#default_value' => variable_get('approval_block_num_posts', 5),
            );
            return $form;
 
        case 'save':
            variable_set('approval_block_num_posts',
                (int)$edit['approval_block_num_posts']);
            break;
 
        case 'view':
            if (user_access('administer comments')) {
               // Retrieve the number of pending comments to display that
               // we saved earlier in the 'save' op, defaulting to 5.
               $num_posts = variable_get('approval_block_num_posts', 5);
 
               // Query the database for unpublished comments.
               $result = db_query_range('SELECT c.* FROM {comments} c WHERE
               c.status = %d ORDER BY c.timestamp', COMMENT_NOT_PUBLISHED, 0,
               $num_posts);
 
               // Preserve our current location so user can return after editing.
               $destination = drupal_get_destination();
               $items = array();
               while ($comment = db_fetch_object($result)) {
                   $items[] = l($comment->subject, 'node/'. $comment->nid,
                      array('fragment' => 'comment-'. $comment->cid)) .' '.
                      l(t('[edit]'), 'comment/edit/'. $comment->cid,
                      array('query' => $destination));
               }
 
               $block['subject'] = t('Pending comments');
               // We theme our array of links as an unordered list.
               $block['content'] = theme('item_list', $items);
            }
            return $block;
    }
}

老葛的Drupal培训班 Think in Drupal

Drupal版本:

创建一个drupal区块(5)

老葛的Drupal培训班 Think in Drupal

这里,我们通过对数据库进行查询来获得待定的评论,将评论的标题显示为链接,同时为每一个评论追加一个编辑链接,如图9-6所示。
    注意,在前面的代码中,我们是如何使用方法drupal_get_destination()的。这个方法将记住在你提交表单以前你所在的页面,所以当你更新一个评论以后(或者发布,或者删除),它将自动重定向到你原来所在的页面。
    你还使用下面的代码设置了区块标题:
 
$block['subject'] = t('Pending comments');
 
9-6 “待定评论”列表区块在它启用后的情况。它展示了两个待定评论。
 
    现在“待定评论”区块已经完成,让我们在approval_block()函数中定义另一个区块----它列出了所有未发布的节点,并提供了一个指向它们编辑页面的链接。
 
function approval_block($op = 'list', $delta = 0, $edit = array()) {
    switch ($op) {
        case 'list':
            $blocks[0]['info'] = t('Pending comments');
            $blocks[0]['cache'] = BLOCK_NO_CACHE;
           $blocks[1]['info'] = t('Unpublished nodes');
           $blocks[1]['cache'] = BLOCK_NO_CACHE;
            return $blocks;
    }
}
 
    注意这里是如何为每一个区块指定一个键的($blocks[0], $blocks[1], . . . $blocks[n])。区块模块将最终使用这些键作为$delta参数。这里我们将“待定评论”区块的$delta ID定义为0,“未发布节点”区块的$delta ID定义为1。在这里也可以使用“待定”和“未发布”作为键。根据程序员的判断来决定使用哪种键,而键不一定是数字形式,也可以是字符串。

Drupal版本:

创建一个drupal区块(6)

 

下面是完整的函数,我们的新区块如图9-7所示:
 
function approval_block($op = 'list', $delta = 0, $edit = array()) {
    switch ($op) {
        case 'list':
            $blocks[0]['info'] = t('Pending comments');
            $blocks[0]['cache'] = BLOCK_NO_CACHE;
 
            $blocks[1]['info'] = t('Unpublished nodes');
            $blocks[1]['cache'] = BLOCK_NO_CACHE;
            return $blocks;
 
        case 'configure':
            // Only in block 0 (the Pending comments block) can one
            // set the number of comments to display.
            $form = array();
            if ($delta == 0) {
                $form['approval_block_num_posts'] = array(
                    '#type' => 'textfield',
                    '#title' => t('Number of pending comments to display'),
                    '#default_value' => variable_get('approval_block_num_posts', 5),
                );
            }
            return $form;
 
        case 'save':
            if ($delta == 0) {
                variable_set('approval_block_num_posts', (int)
                    $edit['approval_block_num_posts']);
            }
            break;
 
        case 'view':
            if ($delta == 0 && user_access('administer comments')) {
               // Retrieve the number of pending comments to display that
               // we saved earlier in the 'save' op, defaulting to 5.
                $num_posts = variable_get('approval_block_num_posts', 5);
               // Query the database for unpublished comments.
                $result = db_query_range('SELECT c.* FROM {comments} c WHERE c.status = %d ORDER BY c.timestamp', COMMENT_NOT_PUBLISHED, 0, $num_posts);
 
                $destination = drupal_get_destination();
                $items = array();
                while ($comment = db_fetch_object($result)) {
                    $items[] = l($comment->subject, 'node/'. $comment->nid,
                    array('fragment' => 'comment-'. $comment->cid)) .' '.
                    l(t('[edit]'), 'comment/edit/'. $comment->cid,
                    array('query' => $destination));
                }
 
                $block['subject'] = t('Pending Comments');
                // We theme our array of links as an unordered list.
                $block['content'] = theme('item_list', $items);
            }
            elseif ($delta == 1 && user_access('administer nodes')) {
               // Query the database for the 5 most recent unpublished nodes.
               // Unpublished nodes have their status column set to 0.
               $result = db_query_range('SELECT title, nid FROM {node} WHERE
               status = 0 ORDER BY changed DESC', 0, 5);
               $destination = drupal_get_destination();
               while ($node = db_fetch_object($result)) {
                   $items[] = l($node->title, 'node/'. $node->nid). ' '.
                      l(t('[edit]'), 'node/'. $node->nid .'/edit',
                      array('query' => $destination));
               }
 
               $block['subject'] = t('Unpublished nodes');
               // We theme our array of links as an unordered list.
               $block['content'] = theme('item_list', $items);
               }
            return $block;
    }
}
 
       由于你有多个区块,在view操作下,你使用了if…elseif构造体。在每一种情况下,你检查被查看区块的$delta以决定你是否应该运行该段代码。在脚本形式里,它看起来是这样的:
if ($delta == 0) {
// Do something to block 0
}
elseif ($delta == 1) {
// Do something to block 1
}
elseif ($delta == 2) {
// Do something to block 2
}
 
    在“未发布节点”区块启用后,区块的最终结果如图9-7所示。
9-7一个区块,列出了未发布节点

老葛的Drupal培训班 Think in Drupal

Drupal版本:

额外例子:添加一个“待定用户”区块

老葛的Drupal培训班 Think in Drupal

如果你想扩展approval.module,你可以添加另一个区块,以显示等待管理员批准的用户帐号列表。这将作为作业留给读者,自己动手将其放到approval.module模块中去。这里给出了一个在假定的userapproval.module模块中的区块:

 
function userapproval_block($op = 'list', $delta = 0, $edit = array()) {
switch ($op) {
case 'list':
$blocks[0]['info'] = t('Pending users');
return $blocks;
case 'view':
if (user_access('administer users')) {
 $result = db_query_range('SELECT uid, name, created FROM {users}
 WHERE uid != 0 AND status = 0 ORDER BY created DESC', 0, 5);
 $destination = drupal_get_destination();
 // Defensive coding: we use $u instead of $user to avoid potential namespace
 // collision with global $user variable should this code be added to later.
 while ($u = db_fetch_object($result)) {
    $items[] = theme('username', $u). ' '.
    l('[edit]', 'user/'. $u->uid. '/edit', array(), $destination);
 }
 $block['subject'] = t('Pending users');
 $block['content'] = theme('item_list', $items);
}
return $block;
}
}

Drupal版本:

在安装drupal模块时,启用一个区块

 

有时,你想在安装模块时,将一个区块自动显示出来。这非常直接,通过查询语句直接将区块的设置信息插入到blocks表中即可。查询放在钩子hook_install()中,该钩子位于模块的.install文件中。下面是一个例子,当Drupal被安装时,用户模块启用了用户登录区块(参看modules/system/system.install):
 
db_query("INSERT INTO {blocks} (module, delta, theme, status, weight, region,
    pages, cache) VALUES ('%s', '%s', '%s', %d, %d, '%s', '%s', %d)",
    'user', '0', 'garland', 1, 0, 'left', '', -1);
 
    上面的数据库查询语句将区块插入到了blocks表中,并将它的状态设置为1,这样它就被启用了。这里将其指定给了left区域,也就是左边栏。
 
老葛的Drupal培训班 Think in Drupal

Drupal版本:

drupal区块可见性例子

 

在区块管理界面,你可以在区块配置页面的“页面可见性设置”部分里面加入php代码片段。当一个页面被构建时,Drupal将运行php代码片段来决定区块是否显示。一些常用的代码片段例子如下所示;每段代码都应该返回TRUE或FALSE,来指示区块对于特定请求是否可见。
 
仅将区块显示给登录用户
    当$user->id不为0时,返回TRUE。
 
<?php
    global $user;
    return (bool) $user->uid;
?>
 
仅将区块显示给匿名用户
    当$user->id为0时,返回TRUE。
 
<?php
    global $user;
    return !(bool) $user->uid;
?>
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

drupal区块 总结

 

在本章中,你学到了以下几点:
    drupal区块是什么以及它们与节点的区别
    drupal区块的可见性和位置设置是如何工作的
    如何定义一个或多个区块
    如何在默认情况下启用一个区块

老葛的Drupal培训班 Think in Drupal

Drupal版本:

第10章 drupal表单(form)API

老葛的Drupal培训班 Think in Drupal

Drupal提供了一个应用程序接口(API),用来生成、验证和处理HTML表单。表单API将表单抽象为一个嵌套数组,里面包含了属性和值。在生成页面时,表单呈现引擎会在适当的时候将数组呈现出来。这种方式包含多层含义:
 
              我们没有直接输出HTML,而是创建了一个数组并让引擎生成HTML。
 
由于我们将表单的表示作为结构化的数据进行处理,所以我们可以添加、删除、重新排序、和修改表单。当你想用一种干净利索的方式对其它模块创建的表单进行修改时,这会特别方便。
 
任意的表单元素可以映射到任意的主题函数上。
 
可以将额外的表单验证或处理函数添加到任意表单上。
 
对表单操作进行了保护,从而防止表单注入攻击,比如当用户修改了表单并接着试图提交它时。
 
使用表单的学习曲线有点高
 
在本章中,我们迎难而上。我们将学习表单引擎的工作原理;如何创建、验证、处理表单;以及当我们需要个性化外观时如何编写主题函数:本章所讲的都是在Drupal6中所实现的表单API。我们首先检查一下表单处理引擎的工作原理。如果你是刚刚接触Drupal的表单,需要一个实际的例子作为起步,那么你可以直接跳到后面的“创建基本的表单”一节。如果你想查找单个表单属性的详细,那么你可以参看本章最后的“表单API属性”一节。
 

Drupal版本:

理解drupal表单处理流程

 

图10-1展示了表单构建、验证、和提交流程的概览。在下面的部分中,我们将使用该图作为指南,向大家描述各步骤的细节。
图10-1 Drupal是如何处理表单的
 
为了更好的与表单API进行交互,理解API背后引擎的工作原理,将会对你非常有用。模块使用关联数组向Drupal描述表单。Drupal的表单引擎负责为要显示的表单生成HTML,并使用三个阶段来安全的处理提交了的表单:验证、提交、重定向。接下来的部分解释了,当调用drupal_get_form()时都发生了什么。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

流程初始化

 

流程初始化
在处理表单时,有3个变量非常重要。第一个就是$form_id,它包含了一个标识表单的字符串。第二个就是$form,它是一个描述表单的结构化数组。而第三个就是$form_state,它包含了表单的相关信息,比如表单的值以及当表单处理完成时应该发生什么。drupal_get_form()在开始时,首先会初始化$form_state。
 
设置一个令牌(token)
       表单系统的一个优点是,它尽力的去保证被提交的表单就是Drupal实际创建的,这主要是为了安全性和防止垃圾信息或潜在的站点攻击者。为了实现这一点,Drupal为每个Drupal安装都设置了一个私钥。这个私钥是在安装流程期间随机生成的,它能将这个特定的Drupal安装与其它的Drupal区别开来。一旦私钥生成后,它将作为drupal_private_key存储在variables表中。Drupal将基于私钥生成一个随机的令牌,而该令牌将作为隐藏域发送到表单中。当表单提交时,会对令牌进行测试。相关背景信息请参看drupal.org/node/28420。令牌仅用于登录用户,因为匿名用户的页面通常会被缓存起来,这样它们就没有唯一的令牌了。
 
设置一个ID
一个包含了当前表单ID的隐藏域,将作为表单的一部分被发送给浏览器。该ID一般对应于定义表单的函数,它将作为drupal_get_form()的第一个参数传递过来。例如函数user_register()定义了用户注册表单,它的调用方式如下:
 
$output = drupal_get_form('user_register');

老葛的Drupal培训班 Think in Drupal

Drupal版本:

收集所有可能的表单元素定义

老葛的Drupal培训班 Think in Drupal

接着,调用element_info()。它将调用所有实现了hook_elements()的模块上的这个钩子函数。在Drupal核心中,标准的元素,比如单选按钮和复选框,都定义在modules/system/system.module中的hook_elements()实现中(参看system_elements())。如果模块需要定义它们自己的元素类型,那么就需要实现这个钩子。在以下几种情况中,你可能需要在你的模块中实现hook_elements():你想要一个特殊类型的表单元素时,比如一个图像上传按钮,在节点预览期间可用来显示缩略图;或者,你想通过定义更多的属性来扩展已有的表单元素时。
    例如,第3方的fivestar模块定义了它自己的元素类型:
 
/**
 * Implementation of hook_elements().
 *
 * Defines 'fivestar' form element type.
 */
function fivestar_elements() {
    $type['fivestar'] = array(
        '#input' => TRUE,
        '#stars' => 5,
        '#widget' => 'stars',
        '#allow_clear' => FALSE,
        '#auto_submit' => FALSE,
        '#auto_submit_path' => '',
        '#labels_enable' => TRUE,
        '#process' => array('fivestar_expand'),
    );
    return $type;
}
    TinyMCE模块使用hook_elements(),来潜在地修改已有类型的默认属性。TinyMCE向textarea元素类型添加了一个#process属性,这样当表单正被构建时,它将调用tinymce_process_textarea(),这样就能够修改表单元素了。#process属性是一个数组,里面包含了所要调用的函数名字。
 
/**
 * Implementation of hook_elements().
 */
function tinymce_elements() {
    $type = array();
 
    if (user_access('access tinymce')) {
        // Let TinyMCE potentially process each textarea.
        $type['textarea'] = array(
            '#process' => array('tinymce_process_textarea'),
        );
    }
 
    return $type;
}
 
    钩子element_info()为所有的表单元素收集所有的默认属性,并将其保存到一个本地缓存中。在进入下一步----为表单寻找一个验证器----以前,对于那些在表单定义中尚未出现的任何默认属性,都将在这里被添加进来。

Drupal版本:

寻找一个验证函数

老葛的Drupal培训班 Think in Drupal

通过将表单的属性#validate设置为一个数组,其中函数名为键,一个数组作为值,从而为表单分配一个验证函数。在调用验证函数时,后面的数组中的任何数据都将被传递给验证函数。可以使用下面的方式来定义多个验证器:
 
// We want foo_validate() and bar_validate() to be called during form validation.
$form['#validate'][] = 'foo_validate';
$form['#validate'][] = 'bar_validate';
 
// Optionally stash a value in the form that the validator will need
// by creating a unique key in the form.
$form['#value_for_foo_validate'] = 'baz';
 
如果表单中没有定义属性#validate,那么接下来就要寻找名为“表单ID”+“_validate”的函数。所以,如果表单ID为user_register,那么表单的#validate属性将被设置为user_register_validate。

Drupal版本:

寻找一个提交函数

老葛的Drupal培训班 Think in Drupal

通过将表单的#submit属性设置为一个数组,其中以函数名为键,这里的函数名就是用来处理表单提交的函数的名字,从而为表单分配一个提交函数:
 
// Call my_special_submit_function() on form submission.
$form['#submit'][] = 'my_special_submit_function';
// Also call my_second_submit_function().
$form['#submit'][] = 'my_second_submit_function';
 
    如果表单没有名为#submit的属性,那么接下来就要寻找名为“表单ID”+“_submit”的函数。所以,如果表单ID为user_register,那么Drupal将把#submit属性设置为它所找到的表单处理器函数;也就是user_register_submit。

Drupal版本:

允许drupal模块在表单构建以前修改表单

 

允许drupal模块在表单构建以前修改表单
    在构建表单以前,模块有两个可以修改表单的机会。模块可以实现一个名字源于form_id + _alter的函数,或者可以简单的实现hook_form_alter()。任何模块,只要实现了这两个钩子中的任意一个,那么就可以修改表单中的任何东西。对于由第3方模块创建的表单,我们主要可以使用这种方式对其进行修改、覆写、混合。
 
构建表单
    现在表单被传递给了form_builder(),这个函数将对表单树进行递归处理,并为其添加标准的必须值。这个函数还将检查每个元素的#access键,如果该元素的#access为FALSE,那么将拒绝对该表单元素及其子元素的访问。
 
允许函数在表单构建后修改表单

函数form_builder()每次遇到$form树中的一个新分支时(例如,一个新的字段集或表单元素),它都寻找一个名为#after_build的可选属性。这是一个可选的数组,里面包含了当前表单元素被构建后会立即调用的函数。当整个表单被构建后,最后将调用可选属性$form[‘#after_build’]中定义的函数。$form和$form_state将作为参数传递给所有的#after_build函数。Drupal核心中有一个实际例子,那就是在“管理站点配置文件系统”中,文件系统路径的显示。这里使用了一个#after_build函数(在这里就是system_check_directory()),用来判定目录是否存在或者是否可写,如果不存在或不可写,那么将为该表单元素设置一个错误消息。

 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

检查drupal表单是否已被提交

老葛的Drupal培训班 Think in Drupal

检查drupal表单是否已被提交
    如果你是按照图10-1所示的流程往下走的话,那么你将看到我们现在来到了一个分叉点。如果表单是初次显示的话,那么Drupal将会为其创建HTML。如果表单正被提交的话,那么Drupal将处理在表单中所输入的数据;我们稍后将会讨论这一点(参看本章后面的“验证表单”一节)。现在,我们将假定表单是初次显示。有一点非常重要,那就是不管表单是初次显示,还是正被提交,在此以前,它所走过的流程是一样的。
 
为表单查找一个主题函数
    如果$form['#theme']已被设置为了一个已有函数,那么Drupal将简单的使用该函数来负责表单的主题化。如果没有设置,那么主题注册表将查找一个对应于这个表单的表单ID的条目。如果存在这样的一个条目,那么就会将表单ID分配给$form['#theme'],在后面,当Drupal呈现表单时,它将基于表单ID来寻找主题函数。例如,如果表单ID为taxonomy_overview_terms,那么Drupal将调用对应的主题函数theme_taxonomy_overview_terms()。当然,可以在自定义主题中,使用主题函数或者模板文件来覆写这个主题函数;关于主题化的更多详细,可参看第8章。
 
允许drupal模块在表单呈现以前修改表单
    最后剩下的一件事,就是将表单从结构化的数据转化为HTML。但是在这以前,模块还有最后一个机会来调整表单。对于跨页面表单向导,或者需要在最后时刻修改表单的其它方式,这将会非常有用。此时将会调用$form['#pre_render']属性定义的任何函数,并将正被呈现的表单传递给这些函数。

Drupal版本:

呈现表单

老葛的Drupal培训班 Think in Drupal

为了将表单树从一个嵌套数组转化为HTML代码,表单构建器调用drupal_render()。这个递归函数将会遍历表单树的每个层次,对于每个层次,它将执行以下动作:
 
1. 判定是否定义了#children属性(这句话就是说,是否已经为该元素生成内容了);如果没有,那么按照以下步骤来呈现这个树节点的孩子:
 
    • 判定是否为这个元素定义了一个#theme函数。
 
    • 如果定义了,那么将这个元素的#type临时设置为markup(标识字体)。接着,将这个元素传递给主题函数,并将该元素重置为原来的样子。
 
    • 如果没有生成内容(可能是因为没有为这个元素定义#theme函数,或者因为调用的#theme函数在主题注册表中不存在,或者因为调用的#theme函数没有返回东西),那么逐个呈现这个元素的子元素(也就是,将子元素传递给drupal_render())。
 
    • 另一方面,如果#theme函数生成了内容,那么将内容存储在这个元素的#children属性中。
 
2. 如果表单元素本身还没有被呈现出来,那么调用这个元素所属类型的默认主题函数。例如,如果这个元素是表单中的一个文本字段(也就是说,在表单定义中,它的#type属性被设置为了textfield),那么默认主题函数就是theme_textfield()。如果没有为这个元素设置#type属性,那么默认为markup。核心元素(比如文本字段)的默认主题函数位于includes/form.inc中。
 
3. 如果为这个元素生成了内容,并且在#post_render属性中找到了一个或多个函数名字,那么将分别调用这些函数,并将内容和该元素传递过去。
 
4. 在内容前面添#prefix,在后面追加#suffix,并将它从函数中返回。
 
    这个递归迭代的作用就是为表单树的每个层次生成HTML。例如,一个表单了包含一个字段集,而字段集里面又包含两个字段,那么该字段集的#children属性将包含两个字段的HTML,而表单的#children属性将包含整个表单的HTML(其中包括字段集的HTML)。

    生成的HTML将会返回给drupal_get_form()的调用者。这就是呈现表单所要做的全部工作!我们到达了图10-1中的终点“返回HTML”

Drupal版本:

drupal表单验证

老葛的Drupal培训班 Think in Drupal

现在让我们回到图10-1,找到我们在“检查表单是否已被提交”一节中所提到的分叉点。现在让我们假定表单已被提交并包含了一些数据;这样我们将沿着另一分支前进,看看这种情况是怎么样的。使用以下两点来判定一个表单已被提交:$_POST不为空,$_POST['form_id']中的字符串匹配刚被构建的表单定义中的ID(参看“设置一个ID”一节)。如果这两点都满足了,那么Drupal 将开始验证表单。
    验证的目的是为了检查证被提交的数据的合理性。验证或者通过,或者失败。如果验证在某一点上失败了,那么将为用户重新显示这个表单,并带有错误消息。如果所有的验证都通过了,那么Drupal将对提交的数据进行实际的处理。
 
令牌验证
    在验证中首先检查的是,该表单是否使用了Drupal的令牌机制(参看 “设置一个令牌”一节)。使用令牌的所有Drupal表单,都会有一个唯一的令牌,它和表单一起被发送给浏览器,并且应该和其它表单值一同被提交。如果提交的数据中的令牌与表单构建时设置的令牌不匹配,或者令牌不存在,那么验证将会失败(尽管验证的其余部分也会继续执行,这样其它验证错误也会被标识出来)。
 
内置验证
    接着,检查必填字段,看用户有没有漏填的。检查带有#maxlength属性的字段,确保它没有超过最大字符数。检查带有选项的元素(复选框、单选按钮、下拉选择框),看所选的值是否是位于构建表单时所生成的原始选项列表中。
 
特定元素的验证
    如果为单个表单元素定义了一个#validate属性,那么将会调用这个属性所定义的函数,并将$form_state和$element作为参数传递过去。
 
验证回调
    最后,表单ID和表单值将被传递到表单的验证器函数中(函数名一般为:“表单ID”+ “_validate”)。

Drupal版本:

提交drupal表单

 

如果验证通过了,那么现在就应该把表单和它的值传递到一个函数中,该函数将做些实际的处理,以作为表单提交的结果。实际上,由于#submit属性可以包含一个数组,里面包含多个函数名字,所以可以使用多个函数来处理表单。调用数组中的每个函数,并向其传递参数$form和$form_state。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

重定向

老葛的Drupal培训班 Think in Drupal

用来处理表单的函数,应该把$form_state['redirect']设置为一个Drupal路径,比如node/1234,这样就可以将用户重定向到这个页面了。如果#submit属性中有多个函数,那么将会使用最后一个函数设置的$form_state['redirect']。如果没有函数把$form_state['redirect']设置为一个Drupal路径,那么用户将返回原来的页面(也就是,$_GET['q']的值)。在最后一个提交函数中返回FALSE,将会阻止重定向
    通过在表单中定义#redirect属性,就可以覆写在提交函数中$form_state['redirect']设置的重定向了,比如
 
$form['#redirect'] = 'node/1'
$form['#redirect'] = array('node/1', $query_string, $named_anchor)
 
    如果使用drupal_goto()中所用的参数术语,那么最后的一个例子将被改写为
 
$form['#redirect'] = array('node/1', $query, $fragment)
 
   表单重定向的判定,是由includes/form.inc中的drupal_redirect_form()完成的。而实际的重定向则由drupal_goto()实现,它为Web服务器返回一个Location头部。drupal_goto()的参数与后一个例子中的参数一致:drupal_goto($path = '', $query = NULL, $fragment = NULL)。
 

Drupal版本:

创建基本的drupal表单(1)

老葛的Drupal培训班 Think in Drupal

如果你曾经直接通过HTML创建过表单,那么在刚开始的时候,你可能会很不适应Drupal的这种方式。本节通过示例,帮你快速的创建自己的表单。为了起步,我们将创建一个简单的模块,让你用来输入自己的名字并将其输出到屏幕上来。我们将把它放在我们自己的模块里面,这样就不需要修改任何已有的代码了。我们的表单仅包含两个元素:文本输入字段和提交按钮。我们首先创建一个.info文件,输入以下内容:
 
; $Id$
name = Form example
description = Shows how to build a Drupal form.
package = Pro Drupal Development
core = 6.x

Drupal版本:

创建基本的drupal表单(2)

老葛的Drupal培训班 Think in Drupal

接下来,我们把实际的模块放在sites/all/modules/custom/formexample/formexample.module:
 
<?php
// $Id$
 
/**
 * @file
 * Play with the Form API.
 */
 
/**
 * Implementation of hook_menu().
 */
function formexample_menu() {
    $items['formexample'] = array(
        'title' => 'View the form',
        'page callback' => 'formexample_page',
        'access arguments' => array('access content'),
    );
    return $items;
}
 
/**
 * Menu callback.
 * Called when user goes to http://example.com/?q=formexample
 */
function formexample_page() {
    $output = t('This page contains our example form.');
 
    // Return the HTML generated from the $form data structure.
    $output .= drupal_get_form('formexample_nameform');
    return $output;
}
 
/**
 * Define a form.
 */
function formexample_nameform() {
    $form['user_name'] = array(
        '#title' => t('Your Name'),
        '#type' => 'textfield',
        '#description' => t('Please enter your name.'),
    );
    $form['submit'] = array(
        '#type' => 'submit',
        '#value' => t('Submit')
    );
    return $form;
}
 
/**
 * Validate the form.
 */
function formexample_nameform_validate($form, &$form_state) {
    if ($form_state['values']['user_name'] == 'King Kong') {
    // We notify the form API that this field has failed validation.
        form_set_error('user_name',
            t('King Kong is not allowed to use this form.'));
    }
}
 
/**
 * Handle post-validation form submission.
 */
function formexample_nameform_submit($form, &$form_state) {
    $name = $form_state['values']['user_name'];
    drupal_set_message(t('Thanks for filling out the form, %name',
        array('%name' => $name)));
}

Drupal版本:

创建基本的drupal表单(3)

老葛的Drupal培训班 Think in Drupal

我们在这里实现了处理表单所需的基本函数:一个函数用于定义表单,一个用于验证,一个用于处理表单提交。另外,我们实现了一个菜单钩子和它对应的函数,这样就将一个URL和我们的函数关联起来了。我们的简单表单如图10-2所示:
 
10-2 一个基本表单,其中包含了一个文本输入框和一个提交按钮
 
    工作的重点就是填充表单的数据结构,或句话说,就是向Drupal描述表单。这一信息包含在一个嵌套的数组中,该数组描述了表单的元素和属性,它一般包含在一个名为$form的变量中。
    在前面的例子中,我们在formexample_nameform()中完成了定义表单这一重要任务,在这里我们为Drupal提供了显示表单所需要的最小信息。
 
注意 属性和元素有哪些区别呢?最基本的区别就是,属性没有属性,而元素可以有属性。提交按钮就是一个元素的例子,而提交按钮的#type属性就是一个属性的例子。你一眼便可以认出属性,这是因为属性拥有前缀“#”。我们有时把属性称为键,因为它们拥有一个值,为了得到该值,你必须知道相应的键。一个初学者常见的错误就是忘记了前缀“#”,此时,无论是Drupal还是你自己,都会感到非常困惑。如果你看到了错误消息“Cannot use string offset as an array in form.inc”,那么十有八九就是你忘记了字符“#”。
 

Drupal版本:

drupal表单属性

有些属性是通用的,而有些则特定于一个元素,比如一个按钮。对于属性的完整列表,可参看本章的最后部分。下面这个表单是比前面的例子中所给的表单要复杂一点:

 
$form['#method'] = 'post';
$form['#action'] = 'http://example.com/?q=foo/bar';
$form['#attributes'] = array(
    'enctype' => 'multipart/form-data',
    'target' => 'name_of_target_frame'
);
$form['#prefix'] = '<div class="my-form-class">';
$form['#suffix'] = '</div>';
 
    #method属性的默认值为post,它可以被忽略。表单API不支持get方法,该方法在Drupal中也不常用,这是因为通过Drupal的菜单路由机制可以很容易的自动解析路径中的参数。#action属性定义在system_elements(),默认值为函数request_uri()的结果。通常与显示表单的URL相同。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

drupal表单IDs

老葛的Drupal培训班 Think in Drupal

Drupal需要有一些方式来唯一的标识表单,这样当一个页面有多个表单时,它就可以判定被提交的是哪一个表单,并且可以将表单与处理该表单的函数关联起来。为了唯一的标识表单,我们为每个表单分配了一个表单ID。在drupal_get_form()的调用中,所用的就是ID,如下所示:
 
drupal_get_form('mymodulename_identifier');
 
对于大多数表单,其ID的命名规则为:模块名字+一个表述该表单做什么的标识。例如,由用户模块创建的用户登录表单,它的ID为user_login。
Drupal使用表单ID来决定表单的验证、提交、主题函数的默认名字.另外,Drupal使用表单ID作为基础来为该特定表单生成一个<form>标签中的HTML ID属性,这样在Drupal中所有的表单都有一个唯一的ID。通过设置#id属性,你可以覆写该ID:
 
$form['#id'] = 'my-special-css-identifier';
    生成的HTML标签将会是这样的:
<form action="/path" "accept-charset="UTF-8" method="post"
id="my-special-css-identifier">
 
    表单ID作为名一个为form_id的隐藏域也嵌套在表单之中。在我们的例子中,我们选择formexample_nameform作为表单ID,这是因为它描述了我们的表单。从名称就可以看出,我们表单的目的是让用户输入他/她的名称。我们也可以将它命名为formexample_form,但是它的描述性不好----而且以后,我们可能还想再添加一个表单到我们的模块上。

Drupal版本:

drupal 字段集 fieldset(1)

 

很多时候,你想将你的表单划分到不同的字段集中---使用表单API可以很容易的做到这一点。每一个字段集都定义在表单的数据结构中,而它所包含的字段都定义为它的孩子。让我们向我们的例子中添加一个“喜欢的颜色”字段:
 
function formexample_nameform() {
    $form['name'] = array(
       '#title' => t('Your Name'),
       '#type' => 'fieldset',
       '#description' => t('What people call you.')
    );
    $form['name']['user_name'] = array(
        '#title' => t('Your Name'),
        '#type' => 'textfield',
        '#description' => t('Please enter your name.')
    );
    $form['color'] = array(
       '#title' => t('Color'),
       '#type' => 'fieldset',
       '#description' => t('This fieldset contains the Color field.'),
       '#collapsible' => TRUE,
       '#collapsed' => FALSE
    );
    $form['color_options'] = array(
       '#type' => 'value',
       '#value' => array(t('red'), t('green'), t('blue'))
    );
    $form['color']['favorite_color'] = array(
       '#title' => t('Favorite Color'),
       '#type' => 'select',
       '#description' => t('Please select your favorite color.'),
       '#options' => $form['color_options']['#value']
    );
    $form['submit'] = array(
        '#type' => 'submit',
        '#value' => t('Submit')
    );
    return $form;
}
 
    表单的显示结果如图10-3所示:
10-3 带有字段集的简单表单
老葛的Drupal 培训班 Think in Drupal

Drupal版本:

drupal 字段集 feildset(2)

老葛的Drupal培训班 Think in Drupal

我们使用可选的属性#collapsible和#collapsed来告诉Drupal,在点击第2个字段集标题时,通过使用Javascript让它可以伸缩。
  这里有个问题值得思考:当$form_state['values']传递到验证和提交函数时,颜色字段应该是$form_state['values']['color']['favorite_color'] 还是$form_state['values']['favorite_color']?换句话说就是,是否将该值嵌套在字段集里面?答案是:根据情况而定。默认情况下,在表单处理器中,表单值不用嵌套,所以下面的代码是正确的:
 
function formexample_nameform_submit($form_id, $form_state) {
    $name = $form_state['values']['user_name'];
    $color_key = $form_state['values']['favorite_color'];
    $color = $form_state['values']['color_options'][$color_key];
 
    drupal_set_message(t('%name loves the color %color!',
       array('%name' => $name, '%color' => $color)));
}
 
    更新后的提交处理器所设置的消息可以在图10-4中看到。
 
10-4 提交处理器为表单设置的消息
 
    然而,如果将属性#tree设为TRUE,那么表单的数据结构就会反映到表单值的名字中。所以,如果我们在定义表单时声明了:
 
$form['#tree'] = TRUE;
 
    那么我们就可以使用下面的方式访问数据了:
 
function formexample_nameform_submit($form, $form_state) {
    $name =       $form_state['values']['name']['user_name'];
    $color_key = $form_state['values']['color']['favorite_color'];
    $color =        $form_state['values']['color_options'][$color_key];
    drupal_set_message(t('%name loves the color %color!',
        array('%name' => $name, '%color' => $color)));
}
 
提示 将属性#tree设为TRUE,你将得到一个嵌套的表单值数组。将属性#tree设为FALSE(默认情况),你将得到一个未嵌套的表单值数组。
 

Drupal版本:

主题化drupal表单

老葛的Drupal培训班 Think in Drupal

Drupal拥有内置的函数,用来获取你定义的表单数据结构,并将其转换或者说是呈现为HTML。然后,许多时候你可能需要修改Drupal生成的输出,或者你可能想更好的控制该流程。幸运的是,在Drupal中,很容易实现这一点。
 
使用#prefix、#suffix和#markup
如果你的主题化需求非常简单,那么你就可以使用属性#prefix和#suffix在表单元素前面和/或后面添加HTML,从而满足需求:
 
$form['color'] = array(
    '#prefix' => '<hr />',
    '#title' => t('Color'),
    '#type' => 'fieldset',
    '#suffix' => '<div class="privacy-warning">' .
        t('This information will be displayed publicly!') . '</div>',
);
 
这一代码在颜色字段集上方添加了一条水平线,在其下方添加了一条私有消息,如图10-5所示。
 
10-5.#prefix和#suffix属性在一个元素前面和后面添加内容
 
你甚至可以在你的表单中把HTML标识文本声明为类型#markup(不过很少这样用)。任何不带#type属性的表单元素默认为markup类型。
 
$form['blinky'] = array(
    '#type' = 'markup',
    '#value' = '<blink>Hello!</blink>'
);
 
注意 这个向你的表单中引入了HTML标识文本的方法,一般认为与使用<blink>标签效果差不多。但是与编写一个主题函数相比,它不够干净利落,同时也增加了你网站设计人员的工作量。
 

Drupal版本:

使用主题函数

主题化表单的最灵活的方式,就是为表单或者表单元素使用一个特定的主题函数。这里涉及到了两个步骤。首先,Drupal需要知道我们的模块将实现哪些主题函数。这可以通过hook_theme()(详细请参看第8章)来完成。下面是我们模块的hook_theme()的一个快速实现,它主要说的是“我们的模块提供了两个主题函数,无须额外参数就可以调用它们”:

/**
 * Implementation of hook_theme().
 */
function formexample_theme() {
    return array(
        'formexample_nameform' => array(
            'arguments' => array(),
        ),
        'formexample_alternate_nameform' => array(
            'arguments' => array(),
        )
    );
}
 
    在默认情况下,Drupal会查找名为“‘theme_’+表单ID的名字”的主题函数。在我们的例子中,Drupal将在主题注册表中查找theme_formexample_nameform条目,由于我们在formexample_theme()中定义了它,所以Drupal能够找到它。将会调用下面的主题函数,而它的输出与Drupal的默认主题化完全一样:
 
function theme_formexample_nameform($form) {
    $output = drupal_render($form);
    return $output;
}
 
拥有我们自己的主题函数的好处是,我们可以按照我们的意愿对变量$output进行解析、混合、添加等操作。我们可以很快的将一个特定元素放在表单的最前面。例如在下面的例子中,我们把颜色字段集放在了最前面。
 
function theme_formexample_nameform($form) {
    // Always put the the color selection at the top.
    $output = drupal_render($form['color']);
 
    // Then add the rest of the form.
    $output .= drupal_render($form);
 
    return $output;
}
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

告诉Drupal使用哪个主题函数

通过为一个表单声明#theme属性,你可以命令Drupal使用一个不匹配“‘theme_’+表单ID名字”格式的主题函数:

 
// Now our form will be themed by the function
// theme_formexample_alternate_nameform().
$form['#theme'] = 'formexample_alternate_nameform';
 
或者,你也可以让Drupal为一个表单元素使用一个专门的主题函数:
 
// Theme this fieldset element with theme_formexample_coloredfieldset().
$form['color'] = array(
    '#title' => t('Color'),
    '#type' => 'fieldset',
    '#theme' => 'formexample_coloredfieldset'
);
 
    注意,在前面的两种情况中,你在#theme属性中定义的函数必须是主题注册表中注册过的;也就是说,必须在一个hook_theme()实现中对其进行了声明。
 
注意 Drupal将在你设定的#theme属性的字符串前面添加前缀“theme_”,所以我们将#theme设置为formexample_coloredfieldset而不是theme_formexample_coloredfieldset,尽管后者是所要调用的主题函数的名字。为什么这样呢?请参看第8章。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

使用hook_forms()声明验证和提交函数

老葛的Drupal培训班 Think in Drupal

有时,你会遇到一种特殊的情况,你想让许多不同的表单共用一个验证或者提交函数。这叫做代码复用,在该种情况下,这是一个不错的想法。例如,在节点模块中,所有节点类型都共用该模块的验证和提交函数。那么我们就需要一种方式,将多个表单ID映射到验证和提交函数上。这就需要hook_forms()了。
Drupal取回表单时,它首先查找基于表单ID定义表单的函数(正因为这样,在我们的代码中,我们使用函数formexample_nameform())。如果找不到该函数,它将触发hook_forms(),该钩子函数在所有的模块中查找匹配的表单ID以进行回调。例如,在node.module中,使用下面的代码,将不同类型的节点表单ID映射到了同一个处理器上:
 
/**
 * Implementation of hook_forms(). All node forms share the same form handler.
 */
function node_forms() {
    $forms = array();
    if ($types = node_get_types()) {
        foreach (array_keys($types) as $type) {
            $forms[$type .'_node_form']['callback'] = 'node_form';
        }
    }
    return $forms;
}
 
在我们的例子中,我们也可以实现hook_forms(),以将其它表单ID映射到我们已有的代码上:
 
/**
 * Implementation of hook_forms().
 */
function formexample_forms($form_id, $args) {
    $forms['formexample_special'] = array(
        'callback' => 'formexample_nameform');
    return $forms;
}
 
    现在,如果我们调用drupal_get_form('formexample_special'),Drupal首先检查定义该表单的函数formexample_special()。如果它找不到这个函数,那么将会调用hook_forms(),这样Drupal就会看到我们将表单ID formexample_special映射到了formexample_nameform上,Drupal将调用formexample_nameform()来获得表单定义,接着,分别尝试调用formexample_special_validate()和formexample_special_submit()来进行验证和提交。

Drupal版本:

主题、验证、提交函数的调用次序

你已经看到,在Drupal中,有多个地方可以用来放置你的主题、验证、提交函数。拥有这么多的选项会让人选择,到底要选择哪个函数呢?下面是Drupal查找位置的总结,这里按先后顺序排列,对于一个主题函数,假定你使用基于PHPTemplate的名为bluemarine的主题,并且你正在调用drupal_get_form('formexample_nameform')。然而,这还取决于你的hook_theme()实现。

    首先,如果在表单定义中将$form['#theme']设置为了'foo':
 
1. themes/bluemarine/foo.tpl.php // Template file provided by theme.
2. formexample/foo.tpl.php // Template file provided by module.
3. bluemarine_foo() // Function provided theme.
4. phptemplate_foo() // Theme function provided by theme engine.
5. theme_foo() // 'theme_' plus the value of $form['#theme'].
 
    然而,如果在表单定义中没有设置$form['#theme']:
 
1. themes/bluemarine/formexample-nameform.tpl.php // Template provided by theme.
2. formexample/formexample-nameform.tpl.php // Template file provided by module.
3. bluemarine_formexample_nameform() // Theme function provided by theme.
4. phptemplate_formexample_nameform() // Theme function provided by theme engine.
5. theme_formexample_nameform() // 'theme_' plus the form ID.
 
    在验证期间,表单验证器的设置次序如下:
1. A function defined by $form['#validate']
2. formexample_nameform_validate // Form ID plus 'validate'.
 
    当需要查找处理表单提交的函数时,查找的次序如下:
1. A function defined by $form['#submit']
2. formexample_nameform_submit // Form ID plus 'submit'.
 

    注意,表单可以有多个验证和提交函数。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

编写一个验证函数

老葛的Drupal培训班 Think in Drupal

Drupal有一个内置的机制,能高亮的显示验证失败的表单元素,并为用户显示一条错误消息。检查一下示例中的验证函数,来看一下它是怎么工作的:
 
/**
 * Validate the form.
 */
function formexample_nameform_validate($form, $form_state) {
    if ($form_state['values']['user_name'] == 'King Kong') {
        // We notify the form API that this field has failed validation.
        form_set_error('user_name',
            t('King Kong is not allowed to use this form.'));
    }
}
 
    注意form_set_error()的使用。当King Kong访问我们的表单,并用他的巨大的键盘键入他的姓名的时候,他将在页面顶部看到一条错误消息,而包含错误的字段也被高亮显示,这里用的是红色,如图10-6所示:
10-6 向用户指示验证失败
 
当然,他也有可能仅仅输入他的名字Kong。我们在这里仅仅拿他作为一个例子,来说明form_set_error()为我们的表单设置了一条错误消息,并使得验证失败。

验证函数应该专注于验证。一般来讲,它们不应该修改数据。然而,它们可以向$form_state数组添加一些信息,在下面的一节中,我们将看到这一点。

Drupal版本:

从验证函数中传递数据

老葛的Drupal培训班 Think in Drupal

从验证函数中传递数据
    如果你的验证函数做了大量的处理,而你又想把结果保存下来以供提交函数使用,那么有两种不同的方式。你可以使用form_set_value()或者使用$form_state。
 
使用form_set_value()传递数据
最正式的方式是,当你在表单定义函数中创建表单时,创建一个表单元素来隐藏该数据,然后使用form_set_value()存储该数据。首先,你需要创建一个用来占位的表单元素:
 
$form['my_placeholder'] = array(
    '#type' => 'value',
    '#value' => array()
);
接着,在你的验证程序中,你把数据保存起来:
 
// Lots of work here to generate $my_data as part of validation.
...
// Now save our work.
form_set_value($form['my_placeholder'], $my_data, $form_state);
 
然后你就可以在提交函数中访问该数据了:
 
// Instead of repeating the work we did in the validation function,
// we can just use the data that we stored.
$my_data = $form_values['my_placeholder'];
 
或者假定你想将数据转化为标准形式。例如,你在数据库中存有一列国家代码,你需要对它们进行验证,但是你的不讲道理的老板坚持----用户可以在文本输入框中键入国家名称。你需要在你的表单中创建一个占位表单元素,通过使用一种巧妙的方式对用户输入进行验证,这样你就可以同时将“The Netherlands”和 “Nederland”映射为ISO 3166 国家代码“NL”了。
 
$form['country'] = array(
    '#title' => t('Country'),
    '#type' => 'textfield',
    '#description' => t('Enter your country.')
);
 
// Create a placeholder. Will be filled in during validation.
$form['country_code'] = array(
    '#type' => 'value',
    '#value' => ''
);
在验证函数内部,你将国家代码保存到占位表单元素中:
 
// Find out if we have a match.
$country_code = formexample_find_country_code($form_state['values']['country']);
if ($country_code) {
    // Found one. Save it so that the submit handler can see it.
    form_set_value($form['country_code'], $country_code, $form_state);
}
else {
    form_set_error('country', t('Your country was not recognized. Please use
        a standard name or country code.'));
}
 
    现在,提交处理器就可以使用$form_state['values'] ['country_code']访问国家代码了。

Drupal版本:

使用$form_state传递数据

一个更简单一点的方式是使用$form_state存储该值。由于$form_state在验证和提交函数中都是通过引用传递的,所以在验证函数中,可以将数值存储在这里,而在提交函数中,就可以使用它了。最好在$form_state中加上你模块的命名空间,而不是仅仅使用一个键。

 
// Lots of work here to generate $weather_data from slow web service
// as part of validation.
...
// Now save our work in $form_state.
$form_state['mymodulename']['weather'] = $weather_data
 
    接着,你就可以在你的提交函数中访问该数据了:
// Instead of repeating the work we did in the validation function,
// we can just use the data that we stored.
$weather_data = $form_state['mymodulename']['weather'];
 

    你可能会问,“为什么不把该值存储在$form_state['values']中,这样不就和表单字段值保持一致了吗?”你说的这种方式也能工作,但是要记住,$form_state['values']是用来放置表单字段值的,而不是放置模块存储的随机数据。还记不记得,Drupal允许任意的模块将验证和提交函数附加在任意的表单上,因此你不能假定只有你的模块使用了表单状态,所以应该采用一种兼容的可预期的方式来存储数据。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

针对表单元素的验证

老葛的Drupal'培训班 Think in Drupal

一般情况下,一个表单使用一个验证函数。但是也可以为单个表单元素设置一个验证函数,这和整个表单的验证函数一样。为了实现这一点,我们需要将元素的属性#element_validate设置为一个数组,其中包含了验证函数的名字。表单数据结构中该元素分支的一份完整拷贝,将被作为验证函数的第一个参数。下面是一个专门用来说明这一点的示例,在这里我们强制用户在一个文本字段中只能输入香料(spicy)和糖果(sweet):
 
// Store the allowed choices in the form definition.
$allowed_flavors = array(t('spicy'), t('sweet'));
$form['flavor'] = array(
    '#type' => 'textfield',
    '#title' => 'flavor',
    '#allowed_flavors' => $allowed_flavors,
    '#element_validate' => array('formexample_flavor_validate')
);
    那么你表单元素的验证函数应该如下所示:
function formexample_flavor_validate($element, $form_state) {
    if (!in_array($form_state['values']['flavor'], $element['#allowed_flavors']))  {
        form_error($element, t('You must enter spicy or sweet.'));
    }
}
    在调用完所有表单元素的验证函数以后,仍需调用表单验证函数。
 
提示 在你的表单元素未通过验证,你希望为它显示一条错误消息时,如果你知道表单元素的名字,那么使用form_set_error(),如果你拥有表单元素本身,那么使用form_error()。后者对前者做了简单封装。
 

Drupal版本:

表单重新构建

 

在验证期间,你可能判定你没有从用户那里获取足够的信息。例如,你可能将表单数值放到一个文本分析引擎中进行检查,然后判定这一内容很有可能是垃圾信息。最后,你想重新显示表单(里面包含用户已输入的值),不过这次添加了一个CAPTCHA,用来证明这个用户不是一个机器人。通过在你的验证函数中设置$form_state['rebuild'],你就可以通知Drupal需要进行一次重构了,就像这样:
 
$spam_score = spamservice($form_state['values']['my_textarea'];
if ($spam_score > 70) {
    $form_state['rebuild'] = TRUE;
    $form_state['formexample']['spam_score'] = $spam_score;
}
 
    在你的表单定义函数中,你的代码应该包含如下所示的内容:
function formexample_nameform($form_id, $form_state = NULL) {
    // Normal form definition happens.
    ...
    if (isset($form_state['formexample']['spam_score']) {
        // If this is set, we are rebuilding the form;
        // add the captcha form element to the form.
        ...
    }
    ...
}

老葛的Drupal'培训班 Think in Drupal

Drupal版本:

编写提交函数

老葛的Drupal培训班 Think in Drupal

提交函数是表单通过验证后负责实际的表单处理的函数。只有在表单验证完全通过,并且表单没有被标记为重新构建时,它才会执行。提交函数通常需要修改$form_state['redirect']。
    如果在表单被提交以后,你想让用户跳转到另一页面,那么你就需要返回一个Drupal路径,也就是用户接下来要访问的路径:
 
function formexample_form_submit($form, &$form_state) {
    // Do some stuff.
    ...
    // Now send user to node number 3.
    $form_state['redirect'] = 'node/3';
}
 
    如果你有多个函数用来处理表单提交(参看本章前面的“提交表单”一节),只有最后一个设置$form_state['redirect']的函数返才拥有最后的发言权。可以通过在表单中定义#redirect属性来覆写提交函数的重定向(参看本章前面的“重定向用户”一节)。通常使用hook_form_alter()来实现这一点。
 
提示 $form_state['rebuild'] 标记也可以设置在提交函数中,就像验证函数中一样。如果设置了,那么所有的提交函数都将运行,但是所有的重定向值都将被忽略,而表单将使用提交了的值进行重构。在向一个表单中添加可选字段时,这一点非常有用。
 

Drupal版本:

使用hook_form_alter()修改表单

 

使用hook_form_alter(),你可以修改任何表单。你只需要知道表单的ID就可以了。这里有两种方式可以用来修改表单。
 
修改任意表单
    让我们修改一下登录表单,它位于用户登录区块和用户登录页面中。
 
function formexample_form_alter(&$form, &$form_state, $form_id) {
    // This code gets called for every form Drupal builds; use an if statement
    // to respond only to the user login block and user login forms.
    if ($form_id == 'user_login_block' || $form_id == 'user_login') {
        // Add a dire warning to the top of the login form.
        $form['warning'] = array(
            '#value' => t('We log all login attempts!'),
            '#weight' => -5
        );
        // Change 'Log in' to 'Sign in'.
        $form['submit']['#value'] = t('Sign in');
    }
}
    由于$form是通过引用传递过来的,所以在这里我们对表单定义拥有完全的访问权,并且可以做出任何我们想要的修改。在例子中,我们使用表单默认元素(参看本章后面的“标识文本”一节)添加了一些文本,并接着修改了提交按钮的值。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

drupal表单API属性

老葛的Drupal培训班 Think in Drupal

当在你的表单构建函数中构建一个表单定义时,数组中的键用来声明表单的信息。在下面部分中列出了最常用的键。表单构建器可以自动添加一些键。
 
表单根部的属性
下面所列的属性是特定于表单根部的。换句话说,你可以设置$form['#programmed'] = TRUE,但是如果你设置$form['myfieldset']['mytextfield'] [#programmed'] = TRUE那么对表单构建器来说没有任何意义。
 
#parameters
    该属性是一个数组,包含了传递给drupal_get_form()的原始参数。通过drupal_retrieve_form()可添加该属性。
 
#programmed
    这是一个布尔值属性,用来指示一个表单是以程序的方式来提交的, 比如通过drupal_execute()。如果在表单处理前设置了属性#post,那么可以使用drupal_prepare_form()来设置该属性。
 
#build_id
    该属性是一个字符串(MD5哈希)。#build_id用来标识一个特定的表单实例。它作为一个隐藏域放在表单中,通过使用drupal_prepare_form()来设置这个表单元素,如下所示:
$form['form_build_id'] = array(
    '#type' => 'hidden',
    '#value' => $form['#build_id'],
    '#id' => $form['#build_id'],
    '#name' => 'form_build_id',
);
 
#token
    这个字符串(MD5哈希)是一个唯一的令牌,每个表单中都带有它,通过该令牌Drupal能够判定一个表单是一个实际的Drupal表单,而不是一个恶意用户修改后的。
 
#id
    这个属性是一个由form_clean_id($form_id)生成的字符串,并且它是一个HTML ID属性。$form_id中的任何背对的括号对“][”,下划线“_”,或者空格’’都将被连字符替换,以生成一致的CSS ID。在Drupal的同一个页面中,该ID是唯一的.如果同一个ID出现两次(例如,同一个表单在一个页面显示了两次),那么就会在后面添加一个连字符和一个自增的整数,例如foo-form, foo-form-1, 和foo-form-2。
 
#action
    这个字符串属性是HTML表单标签的动作属性。默认情况,它是request_uri()的返回值。
 
#method
    这个字符串属性指的是表单的提交方法---通常为post。表单API是基于post方法构建的,它将不会处理使用GET方法提交的表单。关于GET 和POST的区别,可参看HTML规范。如果在某种情况下,你想尝试使用GET方法,那么你真正需要可能是Drupal的菜单API,而不是表单API。
 
#redirect
    该属性可以是一个字符串或者一个数组。如果是一个字符串,那么它是在表单提交以后用户想要重定向到的Drupal路径。如果是一个数组,该数组将作为参数被传递给drupal_goto(),其中数组中的第一个元素应该是目标路径(这将允许向drupal_goto()传递额外的参数,比如一个查询字符串)。
 
#pre_render
    该属性是一个数组,它包含了在表单呈现以前所要调用的函数。每个函数都被调用,并且#pre_render所在的元素将被作为参数传递过来。例如,设置$form['#pre_render'] = array('foo', 'bar') 将使Drupal先调用函数foo(&$form),然后调用bar(&$form)。如果#pre_render是设置在一个表单元素上的话,比如$form['mytextfield']['#pre_render'] = array('foo'),那么Drupal将调用foo(&$element),其中$element就是$form['mytextfield']。当你想在表单验证运行以后,呈现以前,使用钩子修改表单结构时,这个属性非常有用。如果想在验证以前修改表单,那么使用hook_form_alter()。
 
#post_render
    该属性是一个数组,它包含了一组函数,这些函数可对刚被呈现的内容进行修改。如果你设置了$form['mytextfield']['#post_render'] = array('bar'),那么你可以这样修改刚创建的内容:
function bar($content, $element) {
    $new_content = t('This element (ID %id) has the following content:',
        array('%id' => $element['#id'])) . $content;
    return $new_content;
}
 
#cache
    该属性控制着表单是否可被Drupal的一般缓存系统所缓存。对表单进行缓存意味着,在表单被提交时,它不需要再被重新构建。如果你想每次都重新构建表单的话,那么你可以设置$form['#cache'] = FALSE。

Drupal版本:

drupal表单元素

 

在本节中,我们将通过例子来展示内置的Drupal表单元素。
 
Textfield(文本字段)
       元素textfield的示例如下:
$form['pet_name'] = array(
    '#title' => t('Name'),
    '#type' => 'textfield',
    '#description' => t('Enter the name of your pet.'),
    '#default_value' => $user->pet_name,
    '#maxlength' => 32,
    '#required' => TRUE,
    '#size' => 15,
    '#weight' => 5,
    '#autocomplete_path' => 'pet/common_pet_names',
);
 
$form['pet_weight'] = array(
    '#title' => t('Weight'),
    '#type' => 'textfield',
    '#description' => t('Enter the weight of your pet in kilograms.'),
    '#field_suffix' => t('kilograms'),
    '#default_value' => $user->pet_weight,
    '#size' => 4,
    '#weight' => 10,
);
 
    表单元素的显示结果如图10-11所示
10-11元素textfield
 
    #field_prefix 和 #field_suffix属性是特定于文本字段的,它们在文本字段输入框的前面或者后面紧接着放置一个字符串。
    #autocomplete属性定义了一个路径,Drupal自动包含进来的JavaScript将使用jQuery向该路径发送HTTP请求。在前面的例子中,它将请求http://example.com/pet/common_pet_names。实际例子可以参看modules/user/user.pages.inc中的user_autocomplete()函数。
    文本字段元素的常用属性如下:#attributes, #autocomplete_path (默认为 FALSE), #default_value, #description, #field_prefix, #field_suffix,#maxlength (默认为128), #prefix, #required, #size (默认为60), #suffix, #title,#process(默认为form_expand_ahah),和 #weight。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

drupal表单元素: Password(密码)

老葛的Drupal培训班 Think in Drupal

该元素创建一个HTML密码字段,在这里用户的输入不直接显示(一般在屏幕上使用符号“·”代替)。user_login_block()中的示例如下:
$form['pass'] = array(
    '#type' => 'password',
    '#title' => t('Password'),
    '#maxlength' => 60,
    '#size' => 15,
    '#required' => TRUE,
);
 
    密码元素的常用属性如下:#attributes, #description, #maxlength, #prefix, #required, #size (默认为 60), #suffix, #title, #process(默认为form_expand_ahah),和#weight。出于安全原因,在密码元素中不使用#default_value属性。

 

Drupal版本:

drupal表单元素: Password with Confirmation(带确认的密码)

老葛的Drupal培训班 Think in Drupal

该元素创建两个HTML密码字段,并附加一个验证器来检查两个密码是否匹配。例如,在用户模块中,当用户修改他/她的密码时,用到了该元素:
$form['account']['pass'] = array(
    '#type' => 'password_confirm',
    '#description' => t('To change the current user password, enter the new
        password in both fields.'),
    '#size' => 25,
);
 

Drupal版本:

drupal表单元素: Textarea(文本域)

 

文本域元素的示例如下:
$form['pet_habits'] = array(
       '#title' => t('Habits'),
       '#type' => 'textarea',
       '#description' => t('Describe the habits of your pet.'),
       '#default_value' => $user->pet_habits,
       '#cols' => 40,
       '#rows' => 3,
       '#resizable' => FALSE,
       '#weight' => 15,
);
       文本域元素的常用属性如下:#attributes, #cols (默认为60) , #default_value, #description, #prefix, #required,#resizable, #suffix, #title, #rows (默认为5) , #process(默认为form_expand_ahah), 和 #weight。
    如果通过设置#resizable为TRUE,启用动态的文本域调整器,那么属性#cols的设置将不起作用。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

drupal表单元素: Select(下拉选择框)

老葛的Drupal培训班 Think in Drupal

一个来自于modules/statistics/statistics.admin.inc的下拉选择框元素的示例:
 
$period = drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800,
    259200, 604800, 1209600, 2419200, 4838400, 9676800), 'format_interval');
 
/* Period now looks like this:
    Array (
        [3600] => 1 hour
        [10800] => 3 hours
        [21600] => 6 hours
        [32400] => 9 hours
        [43200] => 12 hours
        [86400] => 1 day
        [172800] => 2 days
        [259200] => 3 days
        [604800] => 1 week
        [1209600] => 2 weeks
        [2419200] => 4 weeks
        [4838400] => 8 weeks
        [9676800] => 16 weeks )
*/
 
$form['access']['statistics_flush_accesslog_timer'] = array(
    '#type' => 'select',
    '#title' => t('Discard access logs older than'),
    '#default_value' => variable_get('statistics_flush_accesslog_timer',            259200),
    '#options' => $period,
    '#description' => t('Older access log entries (including referrer statistics)
        will be automatically discarded. (Requires a correctly configured
        <a href="@cron">cron maintenance task</a>.)', array('@cron' =>
        url('admin/reports/status'))),
);
 
    通过将属性#options定义为一个包含子菜单选项的关联数组,Drupal支持对下拉选项的分组,如图10-12所示。
 
$options = array(
    array(
        t('Healthy') => array(
            1 => t('wagging'),
            2 => t('upright'),
            3 => t('no tail')
        ),
    ),
    array(
        t('Unhealthy') => array(
            4 => t('bleeding'),
            5 => t('oozing'),
        ),
    ),
);
$form['pet_tail'] = array(
    '#title' => t('Tail demeanor'),
    '#type' => 'select',
    '#description' => t('Pick the closest match that describes the tail
        of your pet.'),
    '#options' => $options,
    '#multiple' => FALSE,
    '#weight' => 20,
);
 
    图10-12 使用分组的下拉选择框
 
    通过将#multiple属性设置为TRUE,可以启用多选。这也将改变$form_state['values']中的值,从一个字符串(例如,'pet_tail' = '2',假定在前面的例子中选择了upright)变为了一个数组(例如,pet_tail = array( 1 => '1', 2 => '2'),假定在前面的例子中同时选择了wagging 和upright)。
    下拉选择框元素的常用属性如下:#attributes, #default_value,#description, #multiple, #options, #prefix, #required, #suffix, #title, #process(默认为form_expand_ahah),和#weight.

Drupal版本:

drupal表单元素: Radio Buttons(单选按钮)

老葛的Drupal培训班 Think in Drupal

来自于modules/block/block.admin.inc的单选按钮元素的示例:
 
$form['user_vis_settings']['custom'] = array(
    '#type' => 'radios',
    '#title' => t('Custom visibility settings'),
    '#options' => array(
        t('Users cannot control whether or not they see this block.'),
        t('Show this block by default, but let individual users hide it.'),
        t('Hide this block by default but let individual users show it.')
    ),
    '#description' => t('Allow individual users to customize the visibility of
        this block in their account settings.'),
    '#default_value' => $edit['custom'],
);
       单选按钮元素的常用属性如下:#attributes, #default_value, #description,#options, #prefix, #required, #suffix, #title, 和 #weight.注意#process属性默认设为expand_radios() (参看 includes/form.inc)。
 

Drupal版本:

drupal表单元素: Check Boxes(复选框)

 

复选框元素的示例如下。该元素的呈现版本如图10-13所示。
$options = array(
    'poison' => t('Sprays deadly poison'),
    'metal' => t('Can bite/claw through metal'),
    'deadly' => t('Killed previous owner') );
$form['danger'] = array(
    '#title' => t('Special conditions'),
    '#type' => 'checkboxes',
    '#description' => (t('Please note if any of these conditions apply to your
        pet.')),
    '#options' => $options,
    '#weight' => 25,
);
10-13 复选框元素示例图
 
    在验证和提交函数中,通常使用array_filter()函数来获取复选框的键。例如,假如在图10-13中前两个复选框被选中了,那么$form_state['values']['danger']将包含以下内容:
array(
    'poison' => 'poison',
    'metal' => 'metal',
    deadly' => 0,
)
    运行array_filter($form_state['values']['danger'])将生成只包含复选框的键的数组:array('poison', 'metal')。
       复选框元素的常用属性如下:#attributes, #default_value, #description, #options, #prefix, #required, #suffix, #title, #tree (默认为TRUE), 和#weight.注意#process属性默认设为expand_checkboxes() (参看 includes/form.inc)。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

drupal表单元素: Value(值)

老葛的Drupal培训班 Think in Drupal

值元素是用来在drupal内部将数值从$form传递到$form_state['values']的,而不需要将其发送到浏览器端,例如:
$form['pid'] = array(
    '#type' => 'value',
    '#value' => 123,
);
    当表单提交后$form_state['values']['pid']将为123。
    不要混淆了type = '#value' 和 #value = 123。前者声明了正被描述的元素的类型,而后者声明了该元素的值。值元素只有属性#type和#value可用。

Drupal版本:

drupal表单元素: Hidden(隐藏域)

老葛的Drupal培训班 Think in Drupal

该元素使用一个类型为hidden的HTML输入字段将一个隐藏值传递到一个表单中,示例如下:
 
$form['my_hidden_field'] = array(
    '#type' => 'hidden',
    '#value' => t('I am a hidden field value'),
);
 
    如果你想在表单中传递一个隐藏值,通常使用值元素会更好一些,只有当值元素不能满足需求时才使用隐藏域元素。用户可以通过web表单的HTML源代码来查看隐藏域元素,但是却查看不了值元素,这是因为后者只存在于Drupal内部。
    隐藏域元素只有属性#type、#value、#prefix, #process(默认为form_expand_ahah), 和#suffix可用。
 

Drupal版本:

drupal表单元素: Date(日期)

老葛的Drupal培训班 Think in Drupal

日期元素,如图10-14所示,它是一个由3个下拉选择框联合而成的元素:
 
$form['deadline'] = array(
    '#title' => t('Deadline'),
    '#type' => 'date',
    '#description' => t('Set the deadline.'),
    '#default_value' => array(
        'month' => format_date(time(), 'custom', 'n'),
        'day' => format_date(time(), 'custom', 'j'),
        'year' => format_date(time(), 'custom', 'Y'),
    ),
);
 
10-14 日期字段
 
    日期元素的常用属性如下:#attributes, #default_value, #description, #prefix, #required, #suffix, #title, 和#weight. 属性#process默认设为expand_date(),在该方法中年选择器被硬编码为从1900到2050。属性#element_validate默认设为date_validate()(两个函数都位于includes/form.inc中)。当你在表单中定义日期元素时,通过定义这些属性,就使用你自己的代码来替代默认的了。

Drupal版本:

drupal表单元素: Weight(重量)

老葛的Drupal培训班 Think in Drupal

重量元素(不要与属性#weight混淆了)是一个用来声明重量的下拉选择框:
 
$form['weight'] = array(
    '#type' => 'weight',
    '#title' => t('Weight'),
    '#default_value' => $edit['weight'],
    '#delta' => 10,
    '#description' => t('In listings, the heavier vocabularies will sink and the
        lighter vocabularies will be positioned nearer the top.'),
);
 
前面代码的显示结果如图10-15所示。
 
10-15 重量元素
 
    属性#delta决定了重量的可供选择范围,默认为10.例如,如果你将#delta设为50,那么重量的范围就应该为从-50到50. 重量元素的常用属性如下:#attributes, #delta (默认为 10), #default_value, #description, #prefix, #required, #suffix, #title, 和#weight。#process属性默认为array('process_weight', 'form_expand_ahah')。
 

Drupal版本:

drupal表单元素: File Upload(文件上传)

老葛的Drupal培训班 Think in Drupal

文件元素创建了一个文件上传接口。下面是一个来自于modules/user/user.module的示例:
 
$form['picture']['picture_upload'] = array(
    '#type' => 'file',
    '#title' => t('Upload picture'),
    '#size' => 48,
    '#description' => t('Your virtual face or picture.')
);
 
    本元素的显示方式如图10-16所示。
 
10-16 文件上传元素
 
注意,如果你使用了文件元素,那么你需要在你表单的根部设置属性enctype:$form['#attributes']['enctype'] = 'multipart/form-data';
文件元素的常用属性如下:#attributes, #default_value, #description, #prefix, #required, #size (默认为 60), #suffix, #title, 和 #weight.
 

Drupal版本:

drupal表单元素: Fieldset(字段集)

老葛的Drupal培训班 Think in Drupal

字段集元素是用来对其它表单元素进行归类分组的。可将其声明为可伸缩的,这样当用户查看表单并点击字段集标题时,由Drupal自动提供的JavaScript能够动态的打开和关闭字段集。注意,在这个例子中,属性#access用来允许或拒绝访问字段集中的所有字段:
 
// Node author information for administrators.
$form['author'] = array(
    '#type' => 'fieldset',
    '#access' => user_access('administer nodes'),
    '#title' => t('Authoring information'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#weight' => 20,
);
 
字段集元素的常用属性如下:#attributes, #collapsed (默认为 FALSE), #collapsible (默认为 FALSE), #description, #prefix, #suffix, #title, #process(默认为form_expand_ahah),和 #weight。
 

Drupal版本:

drupal表单元素: Submit(提交按钮)

老葛的Drupal培训班 Think in Drupal

提交按钮元素是用来提交表单的。按钮内部显示的单词默认为“提交”,但是可以使用属性#value来修改它:
 
$form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Continue'),
);
 
    提交按钮元素的常用属性如下:#attributes, #button_type (默认为 'submit'), #executes_submit_callback (默认为 TRUE), #name (默认为 'op'),#prefix, #suffix, #value, #process(默认为form_expand_ahah),和#weight。
    另外,可以将#validate和#submit属性直接分配给提交按钮元素。例如,如果#submit设置为了array('my_special_form_submit'),那么就会使用函数my_special_form_submit()来替代表单的定义了的提交处理器。

Drupal版本:

drupal表单元素: Button(按钮)

 

按钮元素除了属性#executes_submit_callback默认为FALSE以外,其它属性与提交按钮元素完全相同。属性#executes_submit_callback告诉Drupal是否需要处理表单,为TRUE时处理表单,为FALSE时则简单的重新呈现表单。和提交按钮元素一样,可以将特定的验证和提交函数直接分配给这个按钮元素。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

drupal表单元素: Image Button(图片按钮)

老葛的Drupal培训班 Think in Drupal

图片按钮元素与提交按钮元素基本相同,但有两点例外。首先,它有一个#src属性,使用一个图片的URL作为它的值。其次,它把内部表单属性#has_garbage_value设置为了TRUE,这样就会阻止使用#default_value属性,从而避免在微软IE浏览器中的臭虫。不要在图片按钮中使用#default_value属性。下面是一个图片按钮,它使用内置的“Powered by Drupal”图片作为按钮:
 
$form['my_image_button'] = array(
    '#type' => 'image_button',
    '#src' => 'misc/powered-blue-80x15.png',
    '#value' => 'foo',
);
 
    通过查看$form_state['clicked_button']['#value'],就可以安全的取回图片按钮的值了。
 

Drupal版本:

drupal表单元素: Markup(标识文本)

老葛的Drupal培训班 Think in Drupal

如果没有设置属性#type的话,标识文本元素就是默认的元素类型了。它用来在表单中引入一段文本或者HTML。
 
$form['disclaimer'] = array(
    '#prefix' => '<div>',
    '#value' => t('The information below is entirely optional.'),
    '#suffix' => '</div>',
);
 
    标识文本元素的常用属性如下:#attributes, #prefix (默认为空字符串 ''), #suffix (默认为空字符串''), #value, and #weight.
 
警告 如果你把文本输出在一个可伸缩的字段集的内部,那么要使用<div>标签对其进行包装,如同例子中所展示的那样,这样当字段集被折叠起来时,你的文本也被折叠在了里面。
 

Drupal版本:

drupal表单元素: Item(项目)

 

项目元素的格式与其它输入表单元素的相同,比如文本字段或下拉选择框字段,但是它缺少输入框。
 
$form['removed'] = array(
    '#title' => t('Shoe size'),
    '#type' => 'item',
    '#description' => t('This question has been removed because the law prohibits       us from asking your shoe size.'),
);
 
前面元素的呈现结果如图10-17所示。
 
10-17 项目元素
 
       项目元素的常用属性如下:#attributes, #description, #prefix(默认为空字符串''), #required, #suffix (默认为空字符串''),#title, #value,和 #weight.

老葛的Drupal培训班 Think in Drupal

Drupal版本:

#ahah属性(1)

老葛的Drupal培训班 Think in Drupal

#ahah元素属性是向Drupal提供AHAH实现信息的,AHAH允许使用JavaScript来修改表单元素。
 
提示 你可能已经注意到了,在我们描述过的许多表单元素中,#process的默认值都为form_expand_ahah。在元素中添加#ahah属性,就是向Drupal指示为这个元素使用AHAH。函数form_expand_ahah()用来确保#ahah的值拥有合理的默认值。
 
    在上传模块的用于文件上传的附件按钮中,就用到了这一个属性,如下所示:
 
$form['new']['attach'] = array(
    '#type' => 'submit',
    '#value' => t('Attach'),
    '#name' => 'attach',
    '#ahah' => array(
    'path' => 'upload/js',
    'wrapper' => 'attach-wrapper',
    'progress' => array(
        'type' => 'bar',
        'message' => t('Please wait...'),
    ),
),
'#submit' => array('node_form_submit_build_node'),
);
 
    #ahah属性的值是一个键值数组。下面的键是必须的:
 
• path: JavaScript所要请求的菜单项的Drupal路径。菜单项的回调和菜单项的路径以js结尾,这表示该项目是通过JavaScript调用的。在前面的例子中,Drupal路径就是upload/js,而相应的回调就是upload_js()(不信的话,你可以查看modules/upload/upload.module中的函数upload_menu())。
 
• wrapper: 对应于一个HTML元素的id属性(通常为<div>)。在前面的例子中,上传模块涉及到下面的这个元素:<div id="attach-wrapper">。
 

Drupal版本:

drupal #ahah属性(2)

老葛的Drupal培训班 Think in Drupal

下面的键是可选的:
• effect: 在替换元素时使用的视觉效果。可能的值有none,fade,和slide。默认值为none。
 
• event: 事件,用来触发浏览器执行JavaScript HTTP请求。Drupal基于元素类型设置了一些默认值。这些值显示在表10-1中。
 
10-1.在表单元素中,触发AHAH的事件的默认名字
元素          默认事件
submit          mousedown*
button          mousedown*
image_button    mousedown*
password        blur
textfield       blur
textarea        blur
radio           change
checkbox        change
select          change
*还包括keypress事件。
 
• method: 当JavaScript HTTP请求的响应返回时,用来修改已有HTML的JQuery方法。可能的值有after,append, before, prepend, 和replace。默认的方法是replace。这个值用在下面的JavaScript(参看misc/ahah.js)中:
 
if (this.method == 'replace') {
    wrapper.empty().append(new_content);
}
else {
    wrapper[this.method](new_content);
}
 
• progress: 通知的方式-----一个JavaScript事件发生后,Drupal向用户发送通知的方式。该属性的值是一个数组,包含以下键:type和message,例如:
 
$form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Click Me'),
    '#ahah' => array(
        'event' => 'click',
        'path' => 'poof/message_js',
        'wrapper' => 'target',
        'effect' => 'fade',
        'progress' => array(
            'type' => 'throbber',
            'message' => t('One moment...'),
        ),
    )
);
 
    type的默认值为throbber,它是一个圆形的转动的图标,在JavaScript HTTP请求正在运行时,它还会显示一个可选的消息。另外的一个选择就是bar,它是一个进度条(如果声明为bar的话,那么将会添加一个单独的JavaScript文件misc/progress.js)。如果类型被设置为了bar,那么有以下可选键可用:url和interval。url键用来为进度条声明一个URL,通过调用这个URL,就可以判定它的百分比,一个从0到100的整数;而interval键则是用来声明检查进度的频率(以秒为单位)。
 
• selector: 通过声明一个选择器,就可以将JavaScript HTTP 请求的结果附加在页面中的多个元素上(而不仅仅是表单元素)。

Drupal版本:

drupal #ahah属性(3)

老葛的Drupal培训班 Think in Drupal

下面是一个表单的简单示例,它允许使用AHAH来动态替换一些文本。按钮使用throbber来指示用户应该继续等待,如图10-18所示。这里是sites/all/modules/custom/poof/poof.info:
 
; $Id$
name = Poof
description = Demonstrates AHAH forms.
package = Pro Drupal Development
core = 6.x
 
    而下面则是sites/all/modules/custom/poof/poof.module:
 
<?php
 
/**
 * Implementation of hook_menu().
 */
function poof_menu() {
    $items['poof'] = array(
        'title' => 'Ahah!',
        'page callback' => 'drupal_get_form',
        'page arguments' => array('poof_form'),
        'access arguments' => array('access content'),
    );
    $items['poof/message_js'] = array(
        'page callback' => 'poof_message_js',
        'type' => MENU_CALLBACK,
        'access arguments' => array('access content'),
    );
    return $items;
}
 
/**
 * Form definition.
 */
function poof_form() {
    $form['target'] = array(
        '#type' => 'markup',
        '#prefix' => '<div id="target">',
        '#value' => t('Click the button below. I dare you.'),
        '#suffix' => '</div>',
    );
    $form['submit'] = array(
        '#type' => 'submit',
        '#value' => t('Click Me'),
        '#ahah' => array(
            'event' => 'click',
            'path' => 'poof/message_js',
            'wrapper' => 'target',
            'effect' => 'fade',
        )
    );
 
    return $form;
}
 
/**
 * Menu callback for AHAH additions.
 */
function poof_message_js() {
    $output = t('POOF!');
    drupal_json(array('status' => TRUE, 'data' => $output));
}
 
10-18.点击按钮后,将会显示一个圆形的转动的throbber图标,之后将会进行基于AHAH的文本替换。
 

Drupal版本:

drupal #ahah属性(4)

 

在下面,还是这个模块,不过这次实现了进度条功能,而更新的频率为两秒一次(参看图10-19)。
 
警告 这个模块只是简单的说明了如何与进度条进行交互;在实际中,你应该报告的是实际任务完成的百分比。特别强调的一点是,你不要像示例中所给的那样,使用Drupal的持久化变量系统来存储和读取进度,这是因为多个用户同时运行表单时,这种方式就会出错。替代的方式是,你需要对数据库进行查询来获取已插入记录的百分比。
 
<?php
 
/**
 * Implementation of hook_menu().
 */
function poof_menu() {
    $items['poof'] = array(
        'title' => 'Ahah!',
        'page callback' => 'drupal_get_form',
        'page arguments' => array('poof_form'),
        'access arguments' => array('access content'),
    );
    $items['poof/message_js'] = array(
        'page callback' => 'poof_message_js',
        'type' => MENU_CALLBACK,
        'access arguments' => array('access content'),
    );
    $items['poof/interval_js'] = array(
       'page callback' => 'poof_interval_js',
       'type' => MENU_CALLBACK,
       'access arguments' => array('access content'),
    );
    return $items;
}
 
/**
 * Form definition.
 */
function poof_form() {
    $form['target'] = array(
        '#type' => 'markup',
        '#prefix' => '<div id="target">',
        '#value' => t('Click the button below. I dare you.'),
        '#suffix' => '</div>',
    );
    $form['submit'] = array(
        '#type' => 'submit',
        '#value' => t('Click Me'),
        '#ahah' => array(
            'event' => 'click',
            'path' => 'poof/message_js',
            'wrapper' => 'target',
            'effect' => 'fade',
           'progress' => array(
              'type' => 'bar',
              'message' => t('One moment...'),
              'interval' => 2,
              'url' => 'poof/interval_js',
           ),
        )
    );
 
    return $form;
}
 
/**
 * Menu callback for AHAH additions.
 */
function poof_message_js() {
    $output = t('POOF!');
    for ($i = 0; $i < 100; $i = $i + 20) {
       // Record how far we are.
       variable_set('poof_percentage', $i);
       // Simulate performing a task by waiting 2 seconds.
       sleep(2);
    }
    drupal_json(array('status' => TRUE, 'data' => $output));
}
 
/**
 * Menu callback for AHAH progress bar intervals.
 */
function poof_interval_js() {
    // Read how far we are.
    $percentage = variable_get('poof_percentage', 0);
    // Return the value to the JavaScript progress bar.
    drupal_json(array('percentage' => $percentage));
}
 
10-19.进度条显示了完成的百分比。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

drupal跨页面表单(1)

老葛的Drupal培训班 Think in Drupal

我们已经学习了简单的单页面表单。但是你可能需要,让用户跨越多个页面来填充一个表单,或者使用几个不同的步骤来输入数据。让我们创建一个简短的模块,用来说明跨页面表单技术,在该模块中通过3个单独的步骤来从用户那里收集3种成分(ingredient)。我们的方式是使用Drupal内置的表单存储箱(storage bin)传递数值。我们将模块命名为formwizard.module。当然,我们首先需要一个sites/all/modules/custom/formwizard.info文件:
 
; $Id$
name = Form Wizard Example
description = An example of a multistep form.
package = Pro Drupal Development
core = 6.x
 

Drupal版本:

drupal跨页面表单(3)

老葛的Drupal培训班 Think in Drupal

在这个简单模块中,我们需要注意几点。在我们的表单构建函数formwizard_multiform()中,我们有一个参数$form_state,用来提供表单的状态信息。让我们看一下整个流程。如果我们访问http://example.com/?q=formwizard, 我们得到初始的表单,如图10-7所示:
10-7 多步表单的初始步
 
    当我们点击“Next”按钮时,Drupal处理本表单的方式和其它表单一样:构建表单,调用验证函数,调用提交函数。但是如果我们还没有处于表单的最后一步,那么提交函数将会简单得返回。Drupal将会注意到存储箱$form_state['storage']中存放的数值,所以它将会再次调用表单构建函数,这次带了一个$form_state的拷贝。(我们也可以设置$form_state['rebuild']来进行重新构建,但是当$form_state['storage']中有值时就不再需要设置它了。)再次调用表单构建函数,并将$form_state传递给我们模块的formwizard_multiform(),该函数将通过查看$form_state['storage']['step']的值,来判定我们所处的步骤并构建相应的表单。我们将得到如图10-8所示的表单。
 
10-8 多步表单的第二步
   

Drupal版本:

使用drupal_execute()通过程序来提交表单

老葛的Drupal培训班 Think in Drupal

在网页浏览器中可以显示的任何表单,也都可以使用程序的方式来填充表单。让我们通过程序填充我们的名字和喜欢的颜色。
 
$form_id = 'formexample_nameform';
$form_state['values'] = array(
    'user_name' => t('Marvin'),
    'favorite_color' => t('green')
);
// Submit the form using these values.
drupal_execute($form_id, $form_state);
 
    要做的就是这些!简单的提供表单ID和表单的数值,然后调用drupal_execute()。
 
警告 大多数提交函数都假定,制造请求的用户就是提交表单的用户。当通过程序来提交表单时,你需要对此格外小心,因为此时这两种用户可能不同。
 

Drupal版本:

修改一个特定的表单

老葛的Drupal培训班 Think in Drupal

前面的方式不错,但是如果有很多模块来修改表单,而每个表单都传递给所有的hook_form_alter()实现,你的心里就开始犯嘀咕了。“这太浪费了吧,”你可能会想。“为什么不仅仅根据表单ID构造一个函数并调用它呢?”你想得不错。Drupal完全可以实现你的想法。下面的函数也将修改用户登录表单:
function formexample_form_user_login_alter(&$form, &$form_state) {
    $form['warning'] = array(
        '#value' => t('We log all login attempts!'),
        '#weight' => -5
    );
 
    // Change 'Log in' to 'Sign in'.
    $form['submit']['#value'] = t('Sign in');
}
 
    函数的名字是这样构造的:
modulename + 'form' + form ID + 'alter'
    例如
'formexample' + 'form' + 'user_login' + 'alter'
    将生成
formexample_form_user_login_alter
 
    在这个特定的情况下,第一种方式是比较好的,因为这里涉及到两个表单ID(位于http://example.com/?q=user的表单user_login,和显示在用户区块中的表单user_login_block)。
 

Drupal版本:

总结

 

读完本章后,你应该理解一下概念:
drupal表单API的工作原理
创建简单的drupal表单
使用主题函数修改表单外观
为表单或者独立的表单元素编写一个验证函数
编写一个提交函数并在表单处理完后进行重定向
修改已有的表单
编写跨页面drupal表单(多页面表单,或者多步表单);
你可以使用的表单定义属性和它们的含义
    Drupal中可用的表单元素(文本字段, 下拉选择框, 单选按钮, 复选框,等等
    如何在表单中使用基于AHAH的文本替换
 
    关于表单的更多信息,包括各种提示和技巧,参看Drupal参考手册http://drupal.org/node/37775

老葛的Drupal培训班 Think in Drupal

Drupal版本:

添加到所有表单元素上的属性

老葛的Drupal培训班 Think in Drupal

当表单构建器使用表单定义构建表单时,它需要保证每一个表单元素都要有一些默认设置。这些默认值在includes/form.inc的函数_element_info()中设置,但是可以被hook_elements()中的表单元素定义所覆写。
 
#description
    该字符串属性将添加到所有表单元素上,默认为NULL。通过表单元素的主题函数来呈现它。例如,一个文本字段的描述呈现在textfield的下面,如图10-2所示。
 
#required
    该布尔值属性将被添加到所有表单元素上,默认为FALSE。将它设为TRUE,如果表单被提交以后而字段未被完成时,Drupal内置的表单验证将抛出一个错误消息。还有,如果将它设为TRUE,那么就会为这个元素设置一个CSS类(参看includes/form.inc中的theme_form_element())
 
#tree
    该布尔值属性将被添加到所有表单元素上,默认为FALSE。如果将它设为TRUE,表单提交后的$form_state['values']数组将会是嵌套的(而不是平坦的)。这将影响你访问提交数据的方式。(参看本章中的“字段集”部分)。
 
#post
    该数组属性是原始$_POST数据的一个拷贝,它将被表单构建器添加到所有的表单元素上。这样,在#process 和 #after_build中定义的函数就可以基于#post的内容做出聪明的决定。
 
#parents
    该数组属性将被添加到所有表单元素上,默认为一个空数组。它在表单构建器的内部使用,以标识表单树中的父元素。更多信息,参看http://drupal.org/node/48643
 
#attributes
    该数组属性将被添加到所有表单元素上,默认为一个空数组,但是主题函数一般会填充该数组。该数组中的成员将被作为HTML属性添加进来。例如$form['#attributes'] = array('enctype' => 'multipart/form-data')。

Drupal版本:

跨页面表单(2)

接着,我们将编写实际的模块。该模块将显示两个页面:一个用来输入数据(我们将重复使用这一页面),一个最终页面,用来显示用户的输入以及对用户输入的致谢。这里是sites/all/modules/custom/formwizard.module:

 
<?php
// $Id$
 
/**
 * @file
 * Example of a multistep form.
 */
 
/**
 * Implementation of hook_menu().
 */
function formwizard_menu() {
    $items['formwizard'] = array(
        'title' => t('Form Wizard'),
        'page callback' => 'drupal_get_form',
        'page arguments' => array('formwizard_multiform'),
        'type' => MENU_NORMAL_ITEM,
        'access arguments' => array('access content'),
    );
    $items['formwizard/thanks'] = array(
        'title' => t('Thanks!'),
        'page callback' => 'formwizard_thanks',
        'type' => MENU_CALLBACK,
        'access arguments' => array('access_content'),
    );
 
    return $items;
}
 
/**
 * Form definition. We build the form differently depending on
 * which step we're on.
 */
function formwizard_multiform(&$form_state = NULL) {
    // Find out which step we are on. If $form_state is not set,
    // that means we are beginning. Since the form is rebuilt, we
    // start at 0 in that case and the step is 1 during rebuild.
    $step = isset($form_state['values']) ? (int)$form_state['storage']['step'] : 0;
 
    // Store next step.
    $form_state['storage']['step'] = $step + 1;
 
    // Customize the fieldset title to indicate the current step to the user.
    $form['indicator'] = array(
        '#type' => 'fieldset',
        '#title' => t('Step @number', array('@number' => $step))
    );
 
    // The name of our ingredient form element is unique for
    // each step, e.g. ingredient_1, ingredient_2...
    $form['indicator']['ingredient_' . $step] = array(
        '#type' => 'textfield',
        '#title' => t('Ingredient'),
        '#description' => t('Enter ingredient @number of 3.', array('@number' => $step))
    );
 
    // The button will say Next until the last step, when it will say Submit.
    $button_name = t('Submit');
    if ($step < 3) {
        $button_name = t('Next');
    }
    $form['submit'] = array(
        '#type' => 'submit',
        '#value' => $button_name
    );
 
    switch($step) {
        case 2:
            // Save ingredient in storage bin.
            $form_state['storage']['ingredient_1'] =
                $form_state['values']['ingredient_1'];
            break;
        case 3:
            // Add ingredient to storage bin.
            $form_state['storage']['ingredient_2'] =
                $form_state['values']['ingredient_2'];
    }
 
    return $form;
}
 
/**
 * Validate handler for form ID 'formwizard_multiform'.
 */
function formwizard_multiform_validate($form, &$form_state) {
    // Show user which step we are on.
    drupal_set_message(t('Validation called for step @step',
        array('@step' => $form_state['storage']['step'] - 1)));
}
 
/**
 * Submit handler for form ID 'formwizard_multiform'.
 */
function formwizard_multiform_submit($form, &$form_state) {
    if ($form_state['storage']['step'] < 4) {
    return;
    }
    drupal_set_message(t('Your three ingredients were %ingredient_1,                %ingredient_2, and %ingredient_3.', array(
            '%ingredient_1' => $form_state['storage']['ingredient_1'],
            '%ingredient_2' => $form_state['storage']['ingredient_2'],
            '%ingredient_3' => $form_state['values']['ingredient_3']
            )
        )
    );
 
    // Clear storage bin to avoid automatic form rebuild that overrides our redirect.
    unset($form_state['storage']);
 
    // Redirect to a thank-you page.
    $form_state['redirect'] = 'formwizard/thanks';
}
 
function formwizard_thanks() {
    return t('Thanks, and have a nice day.');
}

老葛的Drupal培训班 Think in Drupal

Drupal版本:

跨页面表单(4)

老葛的Drupal培训班 Think in Drupal

我们有证据证明验证函数的运行,因为它通过调用drupal_set_message()在屏幕上显示了一条消息。而且我们字段集的标题和文本输入框的描述也被恰当的设置了,这意味着用户到达了第2步。让我们继续。在如图10-9所示的表单中,我们将输入最后一个成分。
 
10-9 多步表单的最后一步
 
    注意,在第3步,我们将提交按钮的名字从“Next”改为了“Submit”。还有,当处理完成时,提交处理器可以将用户重定向一个新页面。现在,当我们点击提交按钮时,我们的提交处理器将识别出这就是第四步,与前面几步的简单返回有所不同,它将对数据进行处理。在这个例子中,我们仅仅调用了drupal_set_message(),这将在Drupal提供的下一个页面中显示用户输入的信息,并将用户重定向到formwizard/thankyou。结果页面如图10-10所示。
 
10-10 多步表单的提交处理器已经运行,而用户已被重定向到了formwizard/thankyou
 
    在前面的例子中,我们向你展示了多步表单工作原理的基本轮廓。除了在$form_state中使用存储箱以外,你的模块还可以将数据保存到隐藏域中从而将其传到下一步,你也可以在你的提交处理器中将其保存到数据库中,或者使用表单ID作为键将其保存到全局变量$_SESSION中。需要理解的重点是,表单构建函数将被继续调用,这是因为填充了$form_state['storage'],通过使用前面的方式增加$form_state['storage']['step'],验证和提交函数就能够聪明的决定要做什么了。

Drupal版本:

通用的drupal表单元素属性

老葛的Drupal培训班 Think in Drupal

本部分解释的属性适用于所有的表单元素。
#type
    该字符串声明了一个表单元素的类型。例如,#type = 'textfield'。表单根部必须包含声明#type = 'form'。
 
#access
    该布尔值属性用来判定是否将该表单元素显示给用户。如果表单元素有子表单元素的话,如果父表单元素的#access属性为FALSE的话,那么子表单元素将不显示。例如,如果表单元素是一个字段集,并且它的#access为FALSE,那么字段集里面的所有字段都不显示。
    #access属性可被直接设置为TRUE或FALSE,也可以设置为执行时返回TRUE或FALSE的函数。当表单定义被取回时,将会执行该函数。下面这个例子来自于Drupal的默认节点表单:
$form['revision_information']['revision'] = array(
    '#access' => user_access('administer nodes'),
    '#type' => 'checkbox',
    '#title' => t('Create new revision'),
    '#default_value' => $node->revision,
);
 
#process
    该属性是一个关联数组。在数组的每个条目中,函数名作为键,传递给函数的任何参数作为值。当构建表单元素时将调用这些函数,从而允许在构建表单元素时对该元素进行额外的操作。例如,在modules/system/system.module定义了checkboxes类型,在构建表单期间,将调用includes/form.inc里面的函数expand_checkboxes():
$type['checkboxes'] = array(
    '#input' => TRUE,
    '#process' => array('expand_checkboxes'),
    '#tree' => TRUE
);
还可参看本章中“收集所有可能的表单元素定义”部分中的例子。当#process数组中的所有的函数都被调用以后,将为每个表单元素添加一个#processed属性。
 
#after_build
    该属性是一个函数数组,在构建完表单元素以后它们将被立即调用。每个要被调用的函数都有两个参数:$form 和 $form_state。例如,如果$form['#after_build'] = array('foo', 'bar'),那么Drupal在表单元素构建完以后,分别调用foo($form, $form_state)和bar($form, $form_state)。一旦这些函数都被调用以后,Drupal在内部将为每个表单元素添加一个#after_build_done属性。
 
#theme
    该可选属性定义了一个字符串,当Drupal为该表单元素寻找主题函数时使用。例如,设置#theme = 'foo',Drupal将会在主题注册表中查找对应于foo的条目。参看本章前面的“为表单寻找主题函数”一节。
 
#prefix
    该属性是一个字符串,在表单元素呈现时,它将被添加到表单元素的前面。
 
#suffix
    该属性是一个字符串,在表单元素呈现时,它将被添加到表单元素的后面。
 
#title
    该字符串是表单元素的标题。
 
#weight
    该属性可以是一个整数或者小数。当呈现表单元素时,将根据它们的重量进行排序。重量小的元素将被放到前面,重量大的元素将被放到后面。
 
#default_value
    该属性的类型取决于表单元素的类型。对于输入表单元素,如果表单还没有被提交,那么它就是在该字段中所用的值。不要将它与表单元素#value混淆了。表单元素#value定义了一个内部表单值,尽管用户看不到它,但是它却定义在表单中,并出现在$form_state['values']中。
 

Drupal版本:

第11章 处理用户输入:drupal过滤器系统

老葛的Drupal培训班 Think in Drupal

如果你需要手工的对输入信息进行格式化,那么向网站添加内容将是一个繁琐的工作。相反,如果想让网站上的文本内容看起来很漂亮,那么你需要懂得HTML---但是大多数用户都不了解这一知识。对于我们中的那些熟悉HTML的人,如果在头脑风暴会议或者文章创作期间,不断的暂停,并向我们的文章中插入HTML标签,这也会令人头痛。段落标签、链接标签、换行标签。。。太烦了。幸好,Drupal提供了预制的程序,这就是过滤器,它使得数据输入更加方便和高效。过滤器执行文本处理,比如为URL添加链接,将换行符转化为<p> 和 <br />标签,甚至包括过滤有害的HTML。钩子hook_filter()负责创建过滤器和处理用户提交的数据。
 

Drupal版本:

drupal过滤器

老葛的Drupal培训班 Think in Drupal

过滤器几乎总是一个单独的动作,比如“删除所有的超链接”、“为这篇文章添加一个随机图片”,或者甚至“将它翻译为pirate-speak”(参看pirate.module  http://drupal.org/project/pirate)。如图11-1所示,过滤器获取某种类型的文本输入,处理文本输入,并返回输出。
11-1.过滤器按照某种方式对文本进行转换,并将转换后的文本返回。
 
    过滤器的一个常见用途就是从用户提交的输入中删除不想要的标识文本。图11-2展示了工作时Drupal的HTML过滤器。
11-2. HTML过滤器仅允许特定的标签通过。这个过滤器主要用来阻止跨站点脚本攻击。
 

Drupal版本:

drupal过滤器和输入格式

老葛的Drupal培训班 Think in Drupal

假定你已经理解了过滤器是做什么的,而且你知道自己想找的是已安装的过滤器列表,你还是不能直接的在管理界面中找到它。为了让过滤器执行它们的工作,你必须将其分配给一个Drupal输入格式,如图11-3所示。输入格式将过滤器组合在一起,这样在处理内容时可以把它们作为批处理一同运行。与为每个提交选择一些过滤器相比,这要容易很多。你可以导航到“管理➤站点配置➤输入格式”,通过配置一个已有的输入格式或者新建一个输入格式,这样就可以查看已安装的过滤器列表了。
 
提示 一个Drupal输入格式是由一组过滤器构成的。
 
11-3. 已安装的过滤器列表位于 “添加输入格式”表单中
 
Drupal自带了3中输入格式(参看图11-4):
 
Filtered HTML输入格式由4个过滤器构成:
    HTML校正者过滤器,用来确保所有的HTML标签都正确的关闭和嵌套了。
    HTML过滤器,用于限制HTML标签以阻止跨站点脚本攻击(通常简称为 XSS);
    换行转换器,用于将回车转换为它们的HTML对应标签;
    URL过滤器,用于将web和电子邮件地址转化为超链接。
 
Full HTML 输入格式,它不对HTML进行任何限制,但它使用了换行转换器
 
PHP Code 输入格式由一个名为“PHP求值器”的过滤器构成它负责执行节点中的任意PHP脚本。一个好的经验法则是,永远不让用户使用带有PHP求值器”的输入格式。如果用户可以运行PHP的话,那么他们可以做任何PHP能做到的事情,包括让你的站点当掉,或者更糟糕的是删除了你的所有数据。为了避免这一可能性,Drupal自带的PHP求值器”过滤器默认是禁用的。如果你想使用它,那么需要启用PHP过滤器模块。
 
警告 在你的站点上,为任何用户启用PHP Code输入格式都会带来安全隐患。一个好的原则是不使用这一输入格式。如果你必须使用它,那就尽可能的少用,而且只给超级用户使用(用户ID为1的用户)。
 
11-4 Drupal自带了3种可配置的输入格式
 
    因为输入格式就是一组过滤器,所以它们是可扩展的。你可以添加和删除过滤器,如图11-5所示。你可以修改输入格式的名字,删除一个过滤器,甚至可以对输入格式中过滤器的执行顺序进行重新排列以避免冲突。例如,你可能想在运行HTML过滤器的前面运行URL过滤器,这样HTML过滤器就可以检查由URL过滤器生成的<a>标签了。
 
注意 输入格式(过滤器组)可以通过管理界面进行控制。开发者在定义过滤器的时候不用考虑输入格式。该工作由Drupal站点管理员负责。
11-5. 输入格式由一组过滤器构成。本图展示了Drupal默认的3个输入格式。执行的方向如箭头所示。

Drupal版本:

安装过滤器

老葛的Drupal培训班 Think in Drupal

安装过滤器与安装模块的流程是一样的,这是因为过滤器就是存在于模块文件中。因此,只需要在“管理➤站点构建➤模块”下面启用或者禁用相应的模块,这样就可以启用或禁用过滤器了。一旦安装了过滤器,就可以导航到“管理➤站点配置➤输入格式”,将新添的过滤器分配到你选的输入格式中了。图11-6展示了过滤器和模块之间的关系。
11-6 过滤器是作为模块的一部分创建的
 

Drupal版本:

知道什么时候使用过滤器

老葛的Drupal培训班 Think in Drupal

如果可以使用已有的钩子来处理文本,那么你可能会想,为什么此时还需要使用过滤器呢?例如,使用hook_nodeapi()为URL添加超链接,这也非常方便,比URL过滤器还好用。但是考虑一下这种情况----你需要为节点的主体字段使用5个不同的过滤器。现在假定你查看默认http://example.com/?q=node页面,它一次显示10个节点。那么算一下,现在为了显示这个页面,你就需要运行50个过滤器,而对文本的过滤操作是很费资源的。这同时也意味着无论什么时候调用一个节点,即便是正被过滤的文本未被修改,那么也需要运行这些过滤操作。你在一次又一次的没有必要的运行这一操作。
    过滤器系统有一个缓存层,极大的提升了性能。一旦为给定的文本片段运行完了所有的过滤器,该文本的过滤版本将被存储在cache_filter表中,在文本被再次修改前缓存的内容保持不变(使用过滤内容的MD5哈希值来判断是否被修改)。现在让我们回到前面的例子中,当文本未被修改时,我们就可以绕过过滤器直接从缓存表中加载10个节点的数据了---速度快多了!图11-7给出了过滤器系统处理流程的概览。
 
提示 MD5是一个算法,用来计算文本字符串的哈希值。Drupal使用它作为数据库中的一个高效的索引列,用来查找节点的过滤数据。
11-7 文本过滤系统的生命周期
 
提示 对于包含大量内容的站点,通过将过滤器缓存移到一个内存缓存中,比如memcached,那么可以极大地提升性能。
 
     现在你应该比刚才更聪明一点了,你可能会想,“好吧,如果在我们的nodeapi钩子中直接将过滤后的文本保存到node表中,那不更好吗?它的运行结果和过滤器系统可是一样的啊?” 尽管这一方法也解决了性能问题,但是你破坏了Drupal架构的一个基本原则:永不修改用户的原始数据。假定你的一个初级用户回过头来想编辑一篇文章时,当他看到很多内容都被包含在了HTML标签中时,我想他十有八九会向你打电话寻求支持的。过滤器系统的目标就是保持原始数据不变,同时可以在Drupal框架其余部分中使用过滤数据的缓存拷贝。这一原理应用在Drupal的各个API中,你将会经常看到。
 
注意 即便是禁用了Drupal页面缓存,过滤器系统仍将缓存它的数据。如果你看到的还是以前的过滤数据,那么可以导航到“管理➤站点配置➤性能”,点击底部的“清除缓存数据”按钮,来清空cache_filter表。
 

Drupal版本:

创建一个自定义过滤器

老葛的Drupal培训班 Think in Drupal

当然,Drupal过滤器可以创建链接,格式化你的内容,将文本转换为pirate-speak,但是它能够聪明到帮我们写日志么,或者至少能够把我们创造性火花碰撞出来么?当然,它可以做到这一点!让我们创建一个带有过滤器的模块,用来向日志条目中插入随机的句子。我们将启用这一模块,这样当你在编写文章中绞尽脑汁毫无灵感并需要一些火花时,你可以简单的键入[juice!],当你保存文章时,它将会被替换为一个随机生成的句子。如果你需要更多的智慧火花时,你可以在一个文章中多次插入[juice!]标签。
 
    在sites/all/modules/custom/下面创建一个名为creativejuice的文件夹。首先,向creativejuice文件夹下面添加creativejuice.info文件:
 
; $Id$
name = Creative Juice
description = Adds a random sentence filter to content.
package = Pro Drupal Development
core = 6.x
 
    接着,创建creativejuice.module文件,并将其添加到creativejuice文件夹下:
 
<?php
// $Id$
/**
 * @file
 * A silly module to assist whizbang novelists who are in a rut by providing a
 * random sentence generator for their posts.
 */

Drupal版本:

实现hook_filter()

老葛的Drupal培训班 Think in Drupal

现在模块的基本部分已经有了,让我们在creativejuice.module中添加我们的hook_filter()实现:
 
/**
 * Implementation of hook_filter().
 */
function creativejuice_filter($op, $delta = 0, $format = -1, $text = '') {
    switch ($op) {
        case 'list':
            return array(
                0 => t('Creative Juice filter')
            );
 
        case 'description':
            return t('Enables users to insert random sentences into their posts.');
 
        case 'settings':
            // No settings user interface for this filter.
            break;
 
        case 'no cache':
            // It's OK to cache this filter's output.
            return FALSE;
 
        case 'prepare':
            // We're a simple filter and have no preparatory needs.
            return $text;
 
        case 'process':
            return preg_replace_callback("|\[juice!\]|i",
                'creativejuice_sentence', $text);
 
        default:
            return $text;
    }
}
 
    过滤器API经过多个阶段,从收集过滤器的名字,到缓存,再到进行真实操作的处理阶段。通过检查creativejuice_filter(),让我们看一下这些阶段或者操作。下面是对这个钩子函数中参数的分解:
 
• $op:要执行的操作。在接下来的部分,我们将对它进行详细讨论。
 
• $delta: 一个实现hook_filter()的模块可以提供多个过滤器。你使用$delta来追踪当前执行的过滤器的ID。$delta是一个整数。由于creativejuice模块仅提供了一个过滤器,所以我们可以忽略它。
 
• $format:一个整数,表示正被使用的是哪一个输入格式。Drupal对这些的追踪是在filter_formats数据库表中进行的。
 
• $text:将被过滤的内容。
 
    根据$op参数的不同,可执行不同的操作。

Drupal版本:

实现hook_filter()(1)

老葛的Drupal培训班 Think in Drupal

list操作
    list操作返回的是一个包含滤器名字的关联数组,其中以数字为键,这是由于在单个hook_filter()钩子实例中可以声明多个过滤器的缘故。这些数字键可用在接下来的操作中,并可通过$delta参数传递回钩子。
 
case 'list':
return array(
0 => t('Creative Juices filter'),
1 => t('The name of my second filter'),
);
 
description操作
    该操作返回了一个简短描述,用来描述过滤器能做什么。只有具有“管理过滤器”权限的用户才能看到这些描述。
 
case 'description':
switch ($delta) {
case 0:
return t('Enables users to insert random sentences into their posts.');
case 1:
return t('If this module provided a second filter, the description
 for that second filter would go here.');
 
// Should never reach here as value of $delta never exceeds
// the last index of the 'list' array.
default:
return;
}
 
settings操作
     当过滤器需要一个用于配置的表单界面时,使用该操作。它返回一个表单定义。当表单提交时,将会使用variable_set()自动的将值保存起来。这意味在取回值时使用variable_get()。该操作的使用实例,可参看modules/filter/filter.module中的filter_filter()。
 
no cache操作
    当使用这个过滤器时,过滤器系统应该为过滤文本绕过它的缓存机制么?如果要禁用缓存,那么返回的代码应该为TRUE。在你开发过滤器时,你将需要禁用缓存,这样调试起来方便一些。如果你修改no cache操作所返回的布尔值,在生效以前,你需要编辑一个使用了你的过滤器的输入格式,这是因为编辑输入格式将更新filter_formats表和该过滤器的缓存设置。
 
警告 禁用单个过滤器的缓存,会删除使用了该过滤器的任意输入格式的缓存。
 
prepare操作
    内容的实际过滤流程包含两步。首先,允许过滤器准备待处理文本。这步的主要目的是将HTML转变为相应的实体。例如,有这样一个过滤器,它允许用户粘贴代码片段。prepare步骤会将代码转变为HTML实体,这样就可以阻止接下来的过滤器将它解释为HTML了。如果没有这一步的话,HTML过滤器将会清除掉这个HTML。过滤器使用prepare的例子,可参看codefilter.module,该模块用于处理<code></code> 和 <?php ?> 标签,从而允许用户发布代码而不用担心转义为HTML实体。该模块的下载地址为http://drupal.org/project/codefilter
 
process操作
     在process操作期间,从prepare步骤里面返回的结果将被传递回hook_filter()中。在这里进行实际的本文处理:将URL转换为可点击的超链接,删除恶意数据,添加单词定义,等等。在prepare操作和process操作中都应该返回$text
 
default操作
    包含默认情况非常重要。如果你的模块没有实现一些操作时,那么将调用该操作,要保证在这里总能返回$text(你的模块要过滤的文本)。
 

Drupal版本:

助手函数

老葛的Drupal培训班 Think in Drupal

当$op 为“process”时,每出现一次[juice!]标签你都要执行一个名为creativejuice_sentence()的助手函数。将这个函数也添加到creativejuice.module中。
 
/**
 * Generate a random sentence.
 */
function creativejuice_sentence() {
    $phrase[0][] = t('A majority of us believe');
    $phrase[0][] = t('Generally speaking,');
    $phrase[0][] = t('As times carry on');
    $phrase[0][] = t('Barren in intellect,');
    $phrase[0][] = t('Deficient in insight,');
    $phrase[0][] = t('As blazing blue sky poured down torrents of light,');
    $phrase[0][] = t('Aloof from the motley throng,');
 
    $phrase[1][] = t('life flowed in its accustomed stream');
    $phrase[1][] = t('he ransacked the vocabulary');
    $phrase[1][] = t('the grimaces and caperings of buffoonery');
    $phrase[1][] = t('the mind freezes at the thought');
    $phrase[1][] = t('reverting to another matter');
    $phrase[1][] = t('he lived as modestly as a hermit');
 
    $phrase[2][] = t('through the red tape of officialdom.');
    $phrase[2][] = t('as it set anew in some fresh and appealing form.');
    $phrase[2][] = t('supported by evidence.');
    $phrase[2][] = t('as fatal as the fang of the most venomous snake.');
    $phrase[2][] = t('as full of spirit as a gray squirrel.');
    $phrase[2][] = t('as dumb as a fish.');
    $phrase[2][] = t('like a damp-handed auctioneer.');
    $phrase[2][] = t('like a bald ferret.');
 
    foreach ($phrase as $key => $value) {
        $rand_key = array_rand($phrase[$key]);
        $sentence[] = $phrase[$key][$rand_key];
    }
 
    return implode(' ', $sentence);
}

Drupal版本:

drupal钩子:hook_filter_tips()

老葛的Drupal培训班 Think in Drupal

你使用creativejuice_filter_tips()来向终端用户显示帮助文本。在默认情况下,显示一个短消息,其中带有一个指向http://example.com/?q=filter/tips的链接,而链接页面则包含了所有过滤器的详细说明。
 
/**
 * Implementation of hook_filter_tips().
 */
function creativejuice_filter_tips($delta, $format, $long = FALSE) {
    return t('Insert a random sentence into your post with the [juice!] tag.');
}
 
    在前面的代码中,无论是简洁的帮助文本还是详细的帮助文本,都使用了相同的文本,如果你想返回一个更详细的文本解释时,你可以检查$long参数,如下所示:
 
/**
 * Implementation of hook_filter_tips().
 */
function creativejuice_filter_tips($delta, $format, $long = FALSE) {
    if ($long) {
       // Detailed explanation for http://example.com/?q=filter/tips page.
       return t('The Creative Juice filter is for those times when your
           brain is incapable of being creative. These times come for everyone,
           when even strong coffee and a barrel of jelly beans do not
           create the desired effect. When that happens, you can simply enter
           the [juice!] tag into your posts...'
       );
    }
    else {
        // Short explanation for underneath a post's textarea.
        return t('Insert a random sentence into your post with the [juice!] tag.');
    }
}
 
     一旦在模块列表页面启用了这个模块,那么就可以使用creativejuice过滤器了,你可以将它应用在一个已有的输入格式中,也可以应用在一个新建的输入格式中。例如,将creativejuice过滤器添加到Filtered HTML输入格式中以后,节点编辑表单中的“输入格式”部分,将如图11-8所示。
 
11-8. Filtered HTML输入格式现在包含了creativejuice过滤器,前面的节点编辑表单中的“输入格式”部分给出了相应指示。
 
    你可以使用合适的输入格式创建一个日志条目,然后提交包含[juice!]标签的文本:
 
Today was a crazy day. [juice!] Even if that sounds a little odd,
it still doesn't beat what I heard on the radio. [juice!]
 
     提交的内容将被转换为如下所示的内容:
Today was a crazy day! Generally speaking, life flowed in its accustomed stream
through the red tape of officialdom. Even if that sounds a little odd, it still
doesn't beat what I heard on the radio. Barren in intellect, she reverted to another
matter like a damp-handed auctioneer.
 

Drupal版本:

阻止恶意数据

 

如果你想阻止恶意的HTML,可以使用Filtered HTML过滤器对内容进行过滤,从而阻止XSS攻击。如果在一些情况下,你不能使用Filtered HTML过滤器,那么你可以使用下面的方式来手工的过滤XSS:
 
function mymodule_filter($op, $delta = 0, $format = -1, $text = '') {
switch ($op) {
case 'process':
// Decide which tags are allowed.
$allowed_tags = '<a> <em> <strong> <cite> <code> <ul> <ol> <li>';
return filter_xss($text, $allowed_tags);
default:
return $text;
break;
}
}
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

总结

老葛的Drupal培训班 Think in Drupal

读完本章后,你应该可以:
• 理解什么是过滤器,什么是输入格式,以及如何使用它们转换文本
• 理解为什么过滤器系统比在其它钩子中执行文本处理更加有效
• 理解输入格式和过滤器的工作原理
• 创建一个自定义过滤器
• 理解drupal过滤器中的各种操作是如何工作的
 

Drupal版本:

第12章 drupal搜索和索引内容

老葛的Drupal培训班 Think in Drupal

MySQL和PostgreSQL都有内置的全文搜索能力。你可以很容易的使用这些特定于数据库解决方案来建立一个搜索引擎,但是你却牺牲了对搜索机制的控制,同时也无法实现搜索系统与你的应用行为之间的完美整合。在数据库看来一个优先级比较高的词语,可能实际上在你的应用中则被认为是无用数据。
    由于数据库全文搜索不能很好的满足应用需求,所以Drupal社区决定构建一个自定义的搜索引擎,以实现特定于Drupal的索引和页面等级算法。这样就产生了一个与Drupal其余框架相一致的搜索引擎,它具有标准的配置和用户界面----不管后端使用了什么数据库。
    在本章中,我们讨论如何在模块中使用搜索API的钩子和构建自定义的搜索表单。我们还将学习一下Drupal是如何解析和索引内容的,还有就是如何实现索引器钩子。
 
提示 Drupal能够理解复杂的搜索查询语句,比如包含布尔操作符AND/OR,精确短语,或者甚至可以排除词语。一个包含所有这些情况的实际例子如下所示:
Beatles OR John Lennon "Penny Lane" –insect.
 

Drupal版本:

构建一个自定义搜索页面

老葛的Drupal培训班 Think in Drupal

Drupal内置了对节点和用户名的搜索能力。即便是你开发了一个自定义的节点类型,那么Drupal的搜索系统仍然可以索引呈现给节点视图的内容。例如,假定你有一个食谱节点类型,它包含的字段有“原料”和“用法说明”;你创建一个新的食谱节点,其节点ID为22。当有你访问http://example.com/?q=node/22, 只要管理员能够看到这些节点字段,搜索模块将在下次访cron运行期间将会索引食谱节点和它的附加元数据。
    开始你可能会觉得,节点搜索和用户搜索应该使用了同样的底层机制,事实上它们使用了两种单独的方式分别来扩展搜索功能。对于节点搜索,对于每次搜索都没有直接对node表进行查询;它使用一个索引器把内容提早处理为一种结构化的格式。在执行节点搜索时,将会对结构化的索引数据进行查询,这样就会产生更快更准确的结果。我们将在本章的后面部分学习索引器。
    用户搜索一点也不复杂,这是因为用户名只是数据库中的单个字段,搜索查询只需要对该字段进行检查就可以了。还有,用户名中不允许包含HTML,所以也不需要使用HTML索引器。替代的,你只需要使用几行代码直接对user表进行查询就可以了。
    在前面的两种情况下,Drupal的搜索模块都将实际搜索委托给了适当的模块。简单的用户名搜索位于modules/user/user.module的user_search()函数中,而复杂一点的节点搜索则位于modules/node/node.module的node_search()函数中。这里的一个重点是,搜索模块负责搜索的协调工作,它将具体的实现委托给了其它模块,这些模块最了解可搜索的内容。
 

Drupal版本:

drupal默认的搜索表单

老葛的Drupal培训班 Think in Drupal

如果你知道搜索API有一个默认的搜索表单可供使用(如图12-1所示),那么你一定会很高兴。如果这个界面能够满足你的需求,那么你只需要编写相应逻辑就可以了----为搜索请求查找采集数(hits)。这一搜索逻辑通常是对数据库的一个查询。
 
12-1.搜索API的用于搜索的默认用户界面
 
    尽管默认的内容搜索表单看起来很简单,实际上它的功能却非常强大,它可以对你站点节点内容的所有可见元素进行查询。这意味着,通过该界面,可以对节点的标题、正文、附加的自定义属性、评论、分类术语进行搜索。

Drupal版本:

drupal高级搜索表单

老葛的Drupal培训班 Think in Drupal

高级搜索特性,如图12-2所示,是用来过滤搜索结果的另一种方式。类别选择源自于站点已定义的所有词汇表(参看第14章)。而类型则包含了在站点上启用的所有内容类型。
 
12-2. 默认搜索表单提供的高级搜索选项
 
    通过在一个模块中实现搜索钩子,接着对表单ID search_form使用hook_form_alter()(参看第10章)来为用户提供一个界面。在图12-2中,这两种情形都发生了。节点模块实现了搜索钩子从而使得节点可被搜索(参看modules/node/node.module中的node_search()),同时扩展表单来提供一个界面(参看modules/node/node.module中的node_form_alter())。

Drupal版本:

扩展drupal搜索表单

 

让我们看一个例子。假定我们使用了path.module,并想启用对站点上URL别名的搜索。我们将编写一个简略的模块,用来实现hook_search()以使得别名可被搜索,并在Drupal的搜索界面提供一个附加标签。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

介绍hook_search()

 

让我们先看一下我们将要实现的搜索钩子。hook_search()的函数签名如下所示:
 
function hook_search($op = 'search', $keys = NULL)
 
    $op参数用来描述当前正被执行的操作,它有以下可能值:
 
• name: 调用者期望接收到的一个可翻译的名字,表示这个hook_search()实现将要提供的内容类型。例如,节点模块返回了t('Content'),而用户模块返回了t('Users')。该名字用于构建搜索表单上的标签(参看图12-1)。
 
• search: 对这个类型的内容进行一次搜索。模块应该执行一次搜索并返回结果。$keys参数包含了用户在搜索表单中输入的字符串。注意,这是一个字符串,而不是一个数组。在执行了一个搜索以后,你的模块应该返回一个包含搜索结果的数组。每一个结果都应该至少包含linktitle键。可选的额外的键有type, user, date, snippet, 和extra。下面是node.module中hook_search('search')的实现的部分内容,在这里构建了结果数组(对于如何使用extra键,可参看modules/comment/comment.module中的comment_nodeapi()):
 
$extra = node_invoke_nodeapi($node, 'search result');
$results[] = array(
    'link' => url('node/'. $item->sid, array('absolute' => TRUE)),
    'type' => check_plain(node_get_types('name', $node)),
    'title' => $node->title,
    'user' => theme('username', $node),
    'date' => $node->changed,
    'node' => $node,
    'extra' => $extra,
    'score' => $item->score / $total,
    'snippet' => search_excerpt($keys, $node->body),
);
 
• reset: 搜索索引即将被重建。用于同时实现hook_update_index()的模块。如果你的模块正在追踪它的数据有多少被索引了,那么它应该将它的计数器重置为准备重新索引阶段。
 
• status: 用户想知道,这个模块提供的内容中有多少被索引了。这个操作用于同时实现了hook_update_index()的模块。它返回一个数组,其中包含remainingtotal,前者表示还有多少项等待被索引,后者表示当索引完成时被索引的项目的总数。
 
• admin:“管理➤站点配置➤搜索设置”界面即将显示。返回一个表单定义数组,里面包含了你想添加到该页面的任意元素。这个表单使用system_settings_form()方式,所以元素键名必须与用于默认值的持久化变量的名字匹配。如果你想重新回顾一下system_settings_form()是如何工作的,那么可参看第2章的“添加特定于模块的设置”一节。
 
    在我们的路径别名搜索中,只用到了namesearch操作,所以我们只需要实现这两个就可以了。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

使用hook_search_page()格式化搜索结果

如果你编写了一个提供搜索结果的模块,那么你可能想通过实现hook_search_page()来接管结果页面的外观。如果你没有实现这个钩子,那么就会调用theme('search_results', $results, $type)来进行格式化,它有个默认实现,位于modules/search/search-results.tpl.php。不要将这个与theme('search_result', $result, $type)混淆了,后者用来格式化单个搜索结果,它的默认实现位于modules/search/search-result.tpl.php。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

使得路径别名能被搜索

 

让我们开始我们的例子。我们将实现hook_search()中的namesearch操作。
 
注意 为了使下面的例子能够工作,我们需要启用路径模块,并将一些路径分配给节点(这样就有东西用来搜索了)。在测试这些例子以前,我们还需要重新构建搜索索引数据。导航到“管理➤站点配置➤搜索设置”,点击“重建站点索引”按钮,接着导航到管理➤报告➤状态报告”,手动运行cron 。cron运行时,搜索模块就会执行索引。
 
    在sites/all/modules/custom下面创建一个名为pathfinder的新文件夹,在新目录中创建列表 12-1 和 12-2所示的文件。
 
列表 12-1. pathfinder.info
 
; $Id$
name = Pathfinder
description = Gives administrators the ability to search URL aliases.
package = Pro Drupal Development
core = 6.x
 
列表 12-2. pathfinder.module
 
<?php
// $Id$
 
/**
 * @file
 * Search interface for URL aliases.
 */
 
    在你的文本编辑器中,不要关闭pathfinder.module;我们将继续使用它。接下来要实现的函数是hook_search($op, $keys)。这个钩子基于操作($op)参数的不同而返回不同的信息。
 
/**
 * Implementation of hook_search().
 */
function pathfinder_search($op = 'search', $keys = null) {
    switch ($op) {
        case 'name':
            if (user_access('administer url aliases')) {
                return t('URL aliases');
            }
            break;
        case 'search':
            if (user_access('administer url aliases')) {
                $found = array();
                // Replace wildcards with MySQL/PostgreSQL wildcards.
                $keys = preg_replace('!\*+!', '%', $keys);
                $sql = "SELECT * FROM {url_alias} WHERE LOWER(dst) LIKE LOWER('%%%s%%')";
                $result = pager_query($sql, 50, 0, NULL, $keys);
                while ($path = db_fetch_object($result)) {
                    $found[] = array('title' => $path->dst,
                    'link' => url("admin/build/path/edit/$path->pid"));
                }
                return $found;
            }
    }
}
 
    当搜索API调用hook_search('name')时,它将寻找显示在通用搜索页面的菜单标签的名字(参看图12-3)。在这里,我们返回的是“URL 别名”。通过返回菜单标签的名字,搜索API将为菜单标签的链接创建一个新的搜索表单。
 
12-3.通过从hook_search()中返回菜单标签的名字,这样就可以访问搜索表单了
 
    hook_search('search')是hook_search()中的核心部分。当提交搜索表单时,将调这一操作,它的任务是收集并返回搜索结果。在前面的代码中,我们使用表单中提交的搜索词语对url_alias表进行查询。接着,我们将查询的结果收集到一个数组中并将其返回。返回的结果由搜索模块负责格式化并显示给用户,如图12-4所示。
 
12-4.搜索结果由搜索模块负责格式化。
 
    让我们关注一下搜索结果页面。如果默认的搜索结果页面不能满足你的期望,那么你可以对默认视图进行覆写。在我们这里,我们不想把它只显示为一列匹配的别名,我们想为搜索结果使用一个可排序的表格,其中对于每个匹配的别名都为其添加了一个单独的“编辑”链接。通过对hook_search('search')的返回值进行一些调整,并实现hook_search_page(),从而完成这一工作。
 
/**
 * Implementation of hook_search().
 */
function pathfinder_search($op = 'search', $keys = null) {
    switch ($op) {
        case 'name':
            if (user_access('administer url aliases')) {
                return t('URL aliases');
            }
            break;
        case 'search':
            if (user_access('administer url aliases')) {
              $header = array(
                  array('data' => t('Alias'), 'field' => 'dst'),
                  t('Operations'),
              );
              // Return to this page after an 'edit' operation.
              $destination = drupal_get_destination();
                // Replace wildcards with MySQL/PostgreSQL wildcards.
                $keys = preg_replace('!\*+!', '%', $keys);
              $sql = "SELECT * FROM {url_alias} WHERE LOWER(dst) LIKE LOWER('%%%s%%')" .tablesort_sql($header);
                $result = pager_query($sql, 50, 0, NULL, $keys);
                while ($path = db_fetch_object($result)) {
                  $rows[] = array(
                     l($path->dst, $path->dst),
                     l(t('edit'), "admin/build/path/edit/$path->pid",
                         array('query' => $destination))
                  );
              }
              if (!$rows) {
                  $rows[] = array(array('data' => t('No URL aliases found.'),
                  'colspan' => '2'));
              }
              return $rows;
            }
    }
}
 
/**
 * Implementation of hook_search_page().
 */
function pathfinder_search_page($rows) {
    $header = array(
       array('data' => t('Alias'), 'field' => 'dst'), ('Operations'));
    $output = theme('table', $header, $rows);
    $output .= theme('pager', NULL, 50, 0);
    return $output;
}
 
    在前面的代码中,我们使用drupal_get_destination()来取回我们当前所在的页面位置,如果我们点击“编辑”链接,来编辑一个URL别名,在提交编辑表单以后,我们将自动返回到这一搜索结果页面。由于目的地的路径信息将作为编辑链接的一部分,传递给了编辑表单,所以编辑表单知道将返回到哪个页面。你将在URL中看到一个名为destination的附加参数,它包含的就是表单后所要返回的URL。
    为了对结果表格进行排序,我们将tablesort_sql()函数追加到了搜索查询字符串上,从而确保在查询语句后面追加正确的SQL ORDER BY语句。最后,pathfinder_search_page()是hook_search_page()的一个实现,它允许我们控制搜索结果页面的输出。图12-5显示了最终的搜索结果页面。
 
12-5.搜索结果页面现在将结果呈现为了一个可排序的表格
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

使用搜索HTML索引器

到目前为止,我们通过提供一个简单的hook_search('search')实现,检查了如何与默认搜索表单进行交互。然而,当我们的需求从使用简单的LIKE语句搜索数据库的一个VARCHAR列,提高到了索引网站内容,那么我们需要将该任务外包给Drupal内置的HTML索引器。

    索引器的目标是高效的搜索大块的HTML。在cron运行时(通过http://example.com/cron.php),通过对内容进行处理来实现这一点。因此,从新内容被提交,也就是新内容可被搜索到了,到下次的cron运行之间,会有一个延时。索引器解析数据并把文本切分成词语 (称为分词),基于一个规则集为每个令牌(token)分配一个分数,我们可以使用搜索API来扩展这一规则集。它接着将这些数据保存到数据库中,而当一个搜索被请求时,它没有直接使用节点表,而是使用了这些存储索引数据的表。
 
注意 由于搜索和索引的执行需要使用cron,从新内容被提交,也就是新内容可被搜索到了,到下次的cron运行之间,会有一个延时。还有,索引是个耗费资源的任务。如果你的Drupal站点非常繁忙,在两次cron运行之间会新增数百个节点,那么最好可以使用一个更高级的能与Drupal一同工作的搜索解决方案,比如Solr。(参看http://drupal.org/project/apachesolr。)
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

什么时候使用索引器

 

当搜索引擎的评价方式比标准的“匹配最多词语”方式要求更高时,一般使用索引器。搜索相关度(relevancy),指的是使用一个规则集(通常很复杂)对内容进行处理来判定它在一个索引内部的等级。
    如果你需要对大块的HTML内容进行搜索时,那么你就需要利用索引器的能力了。Drupal的最大优点之一就是,博客、论坛、页面等等都是节点。它们的基本数据结构是相同的,而这个共同纽带也意味着它们还将共享一些基本功能。一个这样的共同特性就是当启用了搜索模块后,所有的节点将被自动索引;而不需要额外的编程工作。即便是你创建了一个自定义节点类型,Drupal也会自动对其内容进行索引,这样你所作的修改在节点呈现时就能显示出来了。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

drupal索引器的工作原理

 

索引器有一个预处理模式,在该模式下将使用一组规则对文本进行过滤从而分配分数。这些规则包括:处理缩略语、URLs、和数字数据。在预处理期间,其它模块有一个机会可用来向这个流程中添加逻辑,从而执行它们自己的数据操作。在针对特定语言调优时,这点会非常方便,如下所示,这里我们使用了第3方模块Porter-Stemmer:
 
• resumé ä resume (accent removal重音符删除)
• skipping ä skip (stemming词干)
• skips ä skip (stemming词干)
 
    另外的一个语言预处理例子就是,对汉语、日语、和韩语所进行的切词,这是为了确保文本被恰当地索引了。
 
提示 Porter-Stemmer模块(http://drupal.org/project/porterstemmer)是一个例子,它通过提供单词词干化来改进英语搜索。同样,中文分词模块(http://drupal.org/project/csplitter)是一个增强的预处理器,用来改进中文、日文、和韩文的搜索。在搜索模块中包含了一个简单的中文分词器,可以在搜索设置页面启用它。
 
    预处理阶段过后,索引器使用HTML标签来查找更重要的字词(称为令牌),基于HTML标签的默认分数和每个令牌出现的次数来为它们分配调整过的分数。这些分数将用来判定令牌的最终相关度。下面是默认HTML标签分数的完整列表(它们定义search_index()中):
 
<h1> = 25
<h2> = 18
<h3> = 15
<h4> = 12
<a> = 10
<h5> = 9
<h6> = 6
<b> = 3
<strong> = 3
<i> = 3
<em> = 3
<u> = 3
 
    让我们摘取一大块HTML,然后使用索引器对其处理,从而来更好的理解索引器的工作原理。图12-6显示了HTML索引器的概览:解析内容,为令牌分配分数,将该信息存储在数据库中。
 
12-6.对一大块HTML进行索引并为令牌分配分数
 
    当索引器碰到一个由标点分隔的数字型数据时,它将删除标点并只对数字进行索引。这使得数字型元素比如日期、版本号、IP地址等将会更容易的被搜索到。图12-6中的中间步骤显示了如何处理一个没有使用HTML标签的字词令牌。这些令牌的重量为1。最后一行显示了使用强调标签<em>的内容。用来决定令牌总分的公式如下:
 
匹配数量 * HTML标签重量
 
    还需要注意的是Drupal索引节点的过滤输出;例如,如果你有一个输入过滤器,它将URL自动转化为了超链接,或者有另外一个过滤器将换行转化为了HTML的<br/>和<p>标签,那么索引器将看到这些带有标签的内容,并会根据这些标签来分配分数。对于使用PHP求值程序过滤器来生成动态内容的节点,对其过滤后的内容进行索引,那么效果会更加明显。索引动态内容可能是非常麻烦的,但是由于Drupal的索引器只看到了由PHP代码生成的内容输出,所以动态内容也是完全可被索引的。
    当索引器碰到内部链接时,也将用一种特殊方式对它们进行处理。如果一个链接指向了另一个节点,那么链接的字词将被添加到目标节点的内容中,这使得能够更方便的搜索常见问题的答案和相关信息。可以使用两种方式钩住索引器:
 
• hook_nodeapi('update index'): 为了调整搜索相关度,你可以向节点中添加在其它情况下不可见的数据。你可以在Drupal核心中看到这方面的实例,比如分类术语和评论,从技术上来讲它们不是节点对象的一部分,但是它们应该能够影响搜索结果。分类模块通过实现nodeapi('update index'),在索引阶段期间,将这些项目添加到了节点中。你应该记起hook_nodeapi()仅用来处理节点。
 
• hook_update_index():通过使用hook_update_index(),你可以使用索引器对那些不属于节点的HTML内容进行索引。Drupal核心中有个hook_update_index()实现,参看modules/node/node.module中的node_update_index()。
 
    在cron运行期间,为了索引新的数据,这两个钩子都将被调用。图12-7显示了这些钩子的运行次序。
 
12-7. HTML索引钩子的概览
 
    我们将在接下来的部分中,来更详细的讨论这些钩子。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

向节点添加元数据:nodeapi('update index')

老葛的Drupal培训班 Think in Drupal

Drupal为了搜索来索引一个节点时,它首先使用node_view()来运行节点,从而生成与匿名用户在浏览器中看到的完全一样的输出。这就意味着节点中任何可见的部分都将被索引。例如,假定我们有一个节点,它的ID为26。当查看URL http://example.com/?q=node/26时,所看到的节点的各个部分也就是索引器所看到的。
    如果我们有一个自定义节点类型,它包含了能够影响搜索结果的隐藏数据,那该怎么办呢?在book.module中有个不错的现成例子,我们看看它是怎么处理这一点的。我们可以把章节标题和每个子页面索引在一起,从而增加这些子页面的相关度。
 
/**
 * Implementation of hook_nodeapi().
 */
function book_boost_nodeapi($node, $op) {
    switch ($op) {
        case 'update index':
            // Book nodes have a parent link ID attribute.
            // If it's nonzero we can have the menu system retrieve
            // the parent's menu item which gives us the title.
            if ($node->type == 'book' && $node->book['plid']) {
                $item = menu_link_load($node->book['plid']);
                return '<h2>'. $item['title'] .'</h2>';
            }
    }
}
 
    注意,我们在这里把标题包装在了HTML<h2>标签中,来通知索引器为这个文本分配一个相对较高的分数。
 
注意 钩子nodeapi仅用于向节点添加元数据。对于那些不是节点的元素,对它们的索引,可以使用hook_update_index()。
 

Drupal版本:

对非节点的内容进行索引:hook_update_index()(1)

在你需要对非Drupal节点的内容进行搜索时,那么你可以钩住索引器并向其提供你需要的任何文本数据,这样它们在Drupal中就可被搜索了。假定你的小组支持一个遗留应用系统,这个系统可用来输入和查看最近几年的产品技术笔记。由于一些政策原因,你还不能完全使用Drupal的解决方案来替代这个遗留系统,但是你想在Drupal内部能够搜索这些技术笔记。没问题。让我们假定遗留系统将它的数据保存在了technote表中。我们将创建一个简短的模块,在里面使用hook_update_index()把这个数据库中的信息发送给Drupal的索引器,使用hook_search()将搜索结果显示出来。

 
注意 如果你想对非Drupal的数据库的内容进行索引,那么就需要连接多个数据库,关于这方面的更多详细,可参看第5章。
 
    在sites/all/modules/custom下面创建一个名为legacysearch的文件夹。由于我们需要一个用来测试的遗留数据库,所以创建一个名为legacysearch.install的文件,并添加以下内容:
 
<?php
// $Id$
 
/**
 * Implementation of hook_install().
 */
function legacysearch_install() {
    // Create table.
    drupal_install_schema('legacysearch');
    // Insert some data.
    db_query("INSERT INTO technote VALUES (1, 'Web 1.0 Emulator',
        '<p>This handy product lets you emulate the blink tag but in
        hardware...a perfect gift.</p>', 1172542517)");
    db_query("INSERT INTO technote VALUES (2, 'Squishy Debugger',
        '<p>Fully functional debugger inside a squishy gel case.
        The embedded ARM processor heats up...</p>', 1172502517)");
}
 
/**
 * Implementation of hook_uninstall().
 */
function legacysearch_uninstall() {
    drupal_uninstall_schema('legacysearch');
}
 
/**
 * Implementation of hook_schema().
 */
function legacysearch_schema() {
    $schema['technote'] = array(
        'description' => t('A database with some example records.'),
        'fields' => array(
            'id' => array(
                'type' => 'serial',
                'not null' => TRUE,
                'description' => t("The tech note's primary ID."),
            ),
            'title' => array(
                'type' => 'varchar',
                'length' => 255,
                'description' => t("The tech note's title."),
            ),
            'note' => array(
                'type' => 'text',
                'description' => t('Actual text of tech note.'),
            ),
            'last_modified' => array(
                'type' => 'int',
                'unsigned' => TRUE,
                'description' => t('Unix timestamp of last modification.'),
            ),
        ),
        'primary key' => array('id'),
    );
    return $schema;
}

老葛的Drupal培训班 Think in Drupal

Drupal版本:

对非节点的内容进行索引:hook_update_index()(2)

老葛的Drupal培训班 Think in Drupal

因为遗留数据库应该已经存在了,所以这个模块实际上不需要这个安装文件。我们这样做,仅仅是为了确保有一个遗留数据库表和数据供我们测试。我们应该在模块中的调整查询语句,让其连接到已有的非Drupal表上。在下面的查询语句中,假定数据存放在一个非Drupal数据库中, 使用settings.php文件中的$db_url['legacy']定义该数据库链接。
    接着,创建sites/all/modules/custom/legacysearch/legacysearch.info,并添加以下内容:
 
; $Id$
name = Legacy Search
description = Example of indexing/searching external content with Drupal.
package = Pro Drupal Development
core = 6.x
 
    最后,创建sites/all/modules/custom/legacysearch/legacysearch.module,并添加以下代码:
 
<?php
// $Id$
 
/**
 * @file
 * Enables searching of non-Drupal content.
 */
 

Drupal版本:

对非节点的内容进行索引:hook_update_index()(3)

老葛的Drupal培训班 Think in Drupal

继续前进,在你的文本编辑器中不要关闭legacysearch.module,我们将添加hook_update_index(),从而将遗留数据提供给HTML索引器。在创建了这些文件以后,现在我们就可以启用这个模块了。
 
/**
 * Implementation of hook_update_index().
 */
function legacysearch_update_index() {
    // We define these variables as global so our shutdown function can
    // access them.
    global $last_change, $last_id;
 
    // If PHP times out while indexing, run a function to save
    // information about how far we got so we can continue at next cron run.
    register_shutdown_function('legacysearch_update_shutdown');
 
    $last_id = variable_get('legacysearch_cron_last_id', 0);
    $last_change = variable_get('legacysearch_cron_last_change', 0);
 
    // Switch database connection to legacy database.
    db_set_active('legacy');
    $result = db_query("SELECT id, title, note, last_modified
                        FROM {technote}
                        WHERE (id > %d) OR (last_modified > %d)
                        ORDER BY last_modified ASC", $last_id, $last_change);
 
    // Switch database connection back to Drupal database.
    db_set_active('default');
 
    // Feed the external information to the search indexer.
    while ($data = db_fetch_object($result)) {
        $last_change = $data->last_modified;
        $last_id = $data->id;
 
        $text = '<h1>' . check_plain($data->title) . '</h1>' . $data->note;
 
        search_index($data->id, 'technote', $text);
    }
}
 
    每片内容都被传递给了search_index(),这里面包括一个标识符(在这里,就是遗留数据库表中的ID列的值),内容的类型(我们将类型设为technote;索引Drupal内容时它一般为节点或者用户),以及要被索引的文本。

Drupal版本:

对非节点的内容进行索引:hook_update_index()(4)

 

register_shutdown_function()分配了一个函数,在为一个请求执行完PHP脚本以后将执行这个函数。因为在索引完所有的内容以前,PHP可能会终止运行,所以使用这个函数来追踪最后索引项目的ID。
 
/**
 * Shutdown function to make sure we remember the last element processed.
 */
function legacysearch_update_shutdown() {
    global $last_change, $last_id;
 
    if ($last_change && $last_id) {
        variable_set('legacysearch_cron_last', $last_change);
        variable_set('legacysearch_cron_last_id', $last_id);
    }
}

老葛的Drupal培训班 Think in Drupal

Drupal版本:

对非节点的内容进行索引:hook_update_index()(5)

老葛的Drupal培训班 Think in Drupal

在这个模块中,我们需要实现的最后一个函数是钩子hook_search(),它允许我们使用内置的用户界面来搜索我们的遗留信息。
 
/**
 * Implementation of hook_search().
 */
function legacysearch_search($op = 'search', $keys = NULL) {
    switch ($op) {
        case 'name':
            return t('Tech Notes'); // Used on search tab.
 
        case 'reset':
            variable_del('legacysearch_cron_last');
            variable_del('legacysearch_cron_last_id');
            return;
 
        case 'search':
            // Search the index for the keywords that were entered.
            $hits = do_search($keys, 'technote');
 
            $results = array();
 
            // Prepend URL of legacy system to each result. Assume a legacy URL
            // for a given tech note is http://technotes.example.com/note.pl?3
            $legacy_url = 'http://technotes.example.com/';
 
            // We now have the IDs of the results. Pull each result
            // from the legacy database.
            foreach ($hits as $item) {
                db_set_active('legacy');
                $note = db_fetch_object(db_query("SELECT * FROM {technote} WHERE
                    id = %d", $item->sid));
                db_set_active('default');
 
                $results[] = array(
                    'link' => url($legacy_url . 'note.pl', array('query' =>                             $item->sid, 'absolute' => TRUE)),
                    'type' => t('Note'),
                    'title' => $note->title,
                    'date' => $note->last_modified,
                    'score' => $item->score,
                    'snippet' => search_excerpt($keys, $note->note));
            }
        return $results;
    }
}
 
    在运行cron并且索引信息以后,就可以搜索技术笔记了,如图12-8所示。索引是在Drupal内部进行的,但是legacysearch_search()返回的搜索结果则来源于(并指向)遗留系统。
 
 
12-8.搜索一个外部的遗留数据库
 

Drupal版本:

总结

 

读完本章后,你应该可以
• 自定义drupal搜索表单。
• 理解如何使用drupal搜索钩子。
• 理解HTML索引器的工作原理。
• 钩住索引器,来索引各种类型的内容。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

第13章 Drupal文件

Drupal中,可以使用多种方式来上传和下载文件。在本章中,我们将讲述什么是公共和私有文件,以及如何提供它们,简要的介绍多媒体文件的处理,并学习一下Drupal的文件认证钩子。

 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal是如何提供文件的

Drupal提供了两种相互排斥的模式,用来管理文件下载的安全性:公共模式和私有模式。在私有模式下,在请求一个下载文件时将会检查用户的权限,如果用户不具有相应的访问权限,那么下载将被拒绝。在公共模式下,任何可以访问文件URL的用户都可以下载文件。这一设置将应用于整个站点,而不是应用于一个模块或者一个文件,所以通常在初始设立站点期间就做出选择,到底是使用私有模式还是使用公共模式,这一设置将会影响到使用了Drupal文件API的所有模块。

 
警告 由于公共和私有文件的存储方法会为文件下载生成不同的URL,所以在你开始上传文件以前,你需要为你的站点做出最佳选择,在以后要一直坚持使用你选的方法,这一点很重要。
 
    为了设立文件系统路径,并指定使用哪种下载方法,导航到“管理➤站点配置➤文件系统”。
 
    如图13-1所示,如果你指定的目录不存在,或者如果PHP对该目录没有写权限,那么Drupal将会给你警告。
 
图13-1 在Drupal中,用来指定文件相关设置的界面。在这里,Drupal警告了----指定的文件系统路径不具有合适的权限;文件系统路径指定的目录必须已经存在并且具有合适的权限。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

drupal公共文件

最简单的配置就是公共文件下载方法,此时Drupal不参与下载流程。在文件被上传时,Drupal简单的将它们保存到了你在“管理➤站点配置➤文件系统”中所指定的目录,并在数据库中追踪文件的URL(这样Drupal就知道有哪些文件可用,谁上传的,等等)。当一个文件被请求时,它将作为一个静态文件通过HTTP被Web服务器直接传递给用户, Drupal一点也没有参与这一流程。由于不需要执行PHP代码,所以这种方式的特点就是非常的快。然而,这里没有检查用户的权限。

 
    当指定文件系统路径时,该文件夹必须存在并且允许PHP对其可写。一般情况下运行Web服务器的用户(在操作系统上)也就是运行PHP的用户。因此,授予该用户对files文件夹的写权限,将允许Drupal上传文件。这些完成以后,一定要在“管理➤站点配置➤文件系统”中指定文件系统路径。一旦保存这些修改,Drupal将在你的files文件夹中自动的创建一个.htaccess文件。这一点是必要的,它可用来保护你的服务器,以避免一个已知Apache安全漏洞----用户可以上传文件并执行嵌入在上传文件中的脚本(参看http://drupal.org/node/66763)。检查以确保你的files.htaccess文件,里面包含以下信息:文件夹下面包含一个
 
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006
Options None
Options +FollowSymLinks
 
提示 当在一个Web服务器集群上运行Drupal时,临时文件目录的位置需要被所有的web服务器所共享。由于Drupal可以使用一个请求来上传文件,使用第二个请求将它的状态从临时的改为持久的。许多负载均衡方案会把临时文件放在一个服务器上,而第二个请求则转到另一个服务器上。当出现这种情况时,文件在上传时看起来是正确的,但是它们将不会显示在它们要添加到的节点或内容中。确保你的所有web服务器共享一个temp目录,并使用一个基于会话的负载均衡器。你的文件目录,和你的数据库一样,对于你的web服务器来讲应该是全局的。
 

Drupal版本:

drupal私有文件

 

在私有下载模式下,files文件夹可以放在PHP可读可写的任何地方,而且不需要(大多数时候不应该)能被web服务器本身直接访问。
 
    drupal私有文件的安全性是有性能成本的。在这里,没有将提供文件服务的工作委托给web服务器,而是Drupal负责检查访问权限和分发文件,Drupal使用完整的引导指令来处理每个文件请求。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

PHP设置

老葛的Drupal培训班 Think in Drupal

php.ini中的一些设置常被忽略,但它们对于文件上传却很重要。第一个就是post_max_size,它位于php.ini中“Data Handling”部分的下面。由于文件的上传是通过一个HTTP POST请求完成的,如果尝试上传的文件的大小比post_max_size还大,这样要发送的POST数据的总大小就超过了post_max_size,因此上传将会失败。
 
; Maximum size of POST data that PHP will accept.
post_max_size = 8M
 
    php.ini里面的“File Uploads”部分中,包含了一些更重要的设置。在这里,你可以设置是否允许上传文件,上传文件的大小的上限:
 
;;;;;;;;;;;;;;;;
; File Uploads ;
;;;;;;;;;;;;;;;;
 
; Whether to allow HTTP file uploads.
file_uploads = On
 
; Temporary directory for HTTP uploaded files (will use system default if not
; specified).
;upload_tmp_dir =
 
; Maximum allowed size for uploaded files.
upload_max_filesize = 20M
 
    如果文件上传失败了,那么你需要检查一下是不是由于这些设置所引起的。还有,注意upload_max_filesize应该小于post_max_size,而post_max_size应该小于memory_limit:
 
upload_max_filesize < post_max_size < memory_limit
 
    你需要注意的最后两个设置是max_execution_time和max_input_time。在上传一个文件时,如果你的脚本执行时间超过了这些设置,那么PHP将终止你的脚本。在你的网络连接比较慢时,如果上传失败,那么你就需要检查一下这些设置。
 
;;;;;;;;;;;;;;;;;;;
; Resource Limits ;
;;;;;;;;;;;;;;;;;;;
 
max_execution_time = 60     ; Maximum execution time of each script, in seconds
                        ; xdebug uses this, so set it very high for debugging
max_input_time = 60         ; Maximum amount of time each script may spend
                        ; parsing request data
 
    在调试的时候,你可能想把max_execution_time的值设置的大一点(例如,1600),这样调试器就不会超时。记住,然而,如果你的服务器非常繁忙,那么文件上传的时间过长,就可能妨碍Apache的进程,从而产生潜在的可升级性问题。

Drupal版本:

drupal多媒体处理

 

文件API(位于includes/file.inc)没有为上传文件提供一个通用的用户界面。为了为大多数终端用户填补这一空白,在Drupal核心中带有了upload.module,而且有多个第3方模块提供了备选方案。
 
上传模块
    上传模块为你选择的节点类型添加了一个上传字段。上传字段如图13-2所示。
13-2.当启用了上传模块并且用户具有“上传文件”权限时,在节点表单中添加了一个“附件”字段。
 
    在节点编辑表单上,上传一个文件以后,upload.module将在节点正文下面为其添加下载链接。拥有“查看已上传文件”权限的用户可以看到这些链接,如图13-3所示。
13-3. 使用核心上传模块为一个节点上传文件后,得到的一个通用的列表视图。
 
    这一通用解决方案对于大多数用户来说可能并不健壮,所以在接下来的一节中,让我们看一些更特殊的例子。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

其它的通用文件处理模块

 

对于文件上传, upload.module的替代模块可参看http://drupal.org/project/Modules/category/62。文件上传的另一选择是,使用CCK模块加上一个第3方的文件处理字段模块,比如imagefield 或 filefield。关于CCK字段类型的更多详细,可参看http://drupal.org/taxonomy/term/88
 
图片和相册
    需要创建一个相册?那么图片模块(http://drupal.org/project/image)是个不错的选择。它能够处理图片的调整大小和创建相册。当使用CCK在节点内部展示图片时,此时也有一些不错的解决方案。Imagecache (http://drupal.org/project/imagecache)能够处理图片衍生物的创建(上传图片的附加的改进拷贝,比如一个缩略图),而imagefield (http://drupal.org/project/imagefield)可以在节点表单中创建一个图片上传字段。
 
视频和音频
   有许多模块,可用来管理多媒体文件,比如视频文件、Flash内容、幻灯放映、等等,大家可以参看http://drupal.org/project/Modules/category/67

老葛的Drupal培训班 Think in Drupal

Drupal版本:

drupal文件API

老葛的Drupal培训班 Think in Drupal

文件API位于includes/file.inc中。在本节中,我们将介绍一些常用函数。更多的详细,感兴趣的读者可直接通过API文档学习当前的文件API http://api.drupal.org/api/6/group/file/6
 
数据库模式
    尽管Drupal将文件存放在磁盘上,但它仍然使用数据库来存储文件的一些合理的元数据。除了上传者、MIME类型、位置,它还为已上传文件维护了修订信息。files表的模式,如表13-1所示:
 
13-1. files表
字段*      类型           默认值 描述
fid        serial                  主键
uid         int             0       与文件相联的用户的ID
filename    varchar(255)    ''      文件的名字
filepath    varchar(255)    ''      文件的路径,这里相对于Drupal的根目录
filemime    varchar(255)    ''      文件的MIME类型
filesize   int             0       文件的大小,以字节为单位
status      int             0       一个标记,用来指示文件是临时的(1)或是持久的(0)
timestamp  int             0       一个Unix时间戳,用来指示文件的添加时间
*粗体指示一个主键,斜体指示一个索引字段
 
    启用文件管理的模块,使用它们自己的数据库表来保存它们自己的数据。例如,由于上传模块将文件与节点关联了起来,所以它在upload表中追踪了这一信息。核心上传模块的数据库表的模式,可参看表13-2。
 
13-2.上传模块使用的upload表
字段*         类型       默认值 描述
fid           int         0       主键(在files表中,文件的fid)
nid             int         0       与已上传文件相联的nid
vid           int         0       与已上传文件相联的节点修订本ID
description     varchar(255)''      已上传文件的描述
list            int         0       一个标记,用来指示文件在节点中是否列出,列出(1)                                   或不列出(0)
weight          int         0       这个上传文件的重量,相对于该节点的其它上传文件
*粗体指示一个主键,斜体指示一个索引字段
 

Drupal版本:

常用任务和函数

 

如果你想对一个文件做些处理的话,那么文件API已经提供了许多方便的函数供你使用。让我们看一些比较常用的函数。
 
查找文件系统路径
    文件系统路径,就是Drupal可以写文件的目录的路径,比如用来上传文件。这个目录在Drupal的管理界面“管理➤站点配置➤文件系统”中被称为“文件系统路径”,它对应于Drupal变量file_directory_path。
 
file_directory_path()
    这个函数实际就是对variable_get('file_directory_path', conf_path().'/files')作了简单包装。在新的Drupal安装中,它的返回值为sites/default/files。
 
向一个文件保存数据
    有时,你只想把数据保存在一个文件中。下面的这个函数可以实现这一点。
 
file_save_data($data, $dest, $replace = FILE_EXISTS_RENAME)
    $data参数将变成文件的内容。$dest参数是目的文件的文件路径。$replace用来判定目的文件已存在时Drupal的行为。可能值如表13-3所示。
 
13-3.当目标文件已存在时,用来判定Drupal行为的常量
名字                 含义
FILE_EXISTS_REPLACE     使用当前文件替代已有文件
FILE_EXISTS_RENAME      添加一个下划线和一个整数来保证新文件名的唯一性
FILE_EXISTS_ERROR       中止并返回FALSE,
 
    下面是一个简单示例,它将一个简短的字符串保存在了一个文件中,而该文件位于Drupal的文件系统目录里面:
 
$filename = 'myfile.txt';
$dest= file_directory_path() .'/'. $filename;
file_save_data('My data', $dest);
 
    这个文件的位置将如同sites/default/files/myfile.txt一样,它里面包含字符串My data
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

复制和移动文件

 

下面的函数可以帮你处理文件系统中已有的文件。
 
file_copy(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME)
    file_copy()函数用来将文件复制到Drupal的文件系统路径下(一般为sites/default/files)。$source参数是一个字符串,用来指定原始文件的位置,可以在函数中还处理了一个文件对象,后者具有属性$source->filepath和可选属性$source->filename(例如上传模块使用了一个文件对象)。注意,由于$source参数是通过引用传递的,所以它必须是一个变量,而不是一个字面上的字符串。列表13-1和13-2显示了一个正被复制到Drupal的默认文件目录中的文件(也就是,没有提供目的文件$dest),前一个为错误的,后一个为正确的。
 
列表 13-1.错误方式:将文件复制到Drupal的默认文件目录(一个字符串无法通过引用传递)
 
file_copy('/path/to/file.pdf');
 
列表 13-2.正确方式:将文件复制到Drupal的默认文件目录
$source = '/path/to/file.pdf';
file_copy($source);
 
    $dest参数是一个字符串,用来指定新复制的文件在Drupal的文件系统路径中目的地。如果没有指定$dest参数,那么将使用文件系统路径。如果$dest位于Drupal的文件系统路径以外(Drupal的临时目录除外),或者如果文件系统路径指定的目录不可写,那么复制将会失败。
    $replace参数用来判定目的文件已存在时Drupal的行为。表13-3总结了$replace参数可用的常量。
 
file_move(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME)
    file_move()函数和file_copy()函数类似(实际上,它调用file_copy()),但是它还会调用file_delete()来删除原始文件。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

检查目录、路径、位置

 

当你使用文件时,你常常需要停下来判定一下一切是否就绪。例如,一个目录可能并不存在或者不可写。下面的函数将帮你解决这类问题。
 
file_create_path($dest = 0)
    这个函数用来获取Drupal的文件系统路径中项目的路径。例如,当启用CSS优化时,Drupal将创建一个css子目录用来存放聚合压缩的CSS文件,它是这样实现的:
 
// Create the css/ within the files folder.
$csspath = file_create_path('css');
file_check_directory($csspath, FILE_CREATE_DIRECTORY);
 
    一些例子,如下所示:
 
$path = file_create_path('foo'); // returns 'sites/default/files/foo'
$path = file_create_path('foo.txt'); // returns 'sites/default/files/foo.txt'
$path = file_create_path('sites/default/files/bar/baz')
// returns 'sites/default/files/bar/baz'
 
$path = file_create_path('/usr/local/') // returns FALSE
 
file_check_directory(&$directory, $mode = 0, $form_item = NULL)
    这个函数检查给定目录的存在和可写。$directory参数是一个目录的路径,由于它是通过引用传递的,所以它必须作为一个变量传递过来。$mode参数是用来判定,当目录不存在或不可写的时候,Drupal应该做什么。表13-4给出了可用的模式。
 
13-4.file_check_directory()的$mode参数的可能值
                      含义
0                           如果目录不存在,不创建目录
FILE_CREATE_DIRECTORY       如果目录不存在,创建目录
FILE_MODIFY_PERMISSIONS     如果目录不存在,创建目录。如果目录已存在,尝试使它可写。
 
    $form_item参数是表单项目的名字,当目录创建失败时,可对其设置错误消息。$form_item参数是可选的。
    这个函数还会测试,正被检查的目录是不是文件系统路径或者临时目录,如果是的话,出于安全性向其添加一个.htaccess文件(参看第20章)。
 
file_check_path(&$path)
    如果你有一个文件路径,你想把它拆分成文件名和基名字,那么可以使用file_check_path()。$path参数必须是一个变量;该变量将被修改为仅包含基名字。这有一些例子:
 
$path = 'sites/default/files/foo.txt';
$filename = file_check_path($path);
 
现在$path为sites/default/files,而$filename为foo.txt。
 
$path = 'sites/default/files/css'; // Where Drupal stores optimized CSS files.
$filename = file_check_path($path);
 
现在$path为sites/default/files;如果css目录不存在,那么$filename为css,否则,$filename为空。
 
$path = '/etc/bar/baz.pdf';
$filename = file_check_path($path);
 
由于/etc/bar不存在或者不可写,所以$path现在为/etc/bar,而$filename现在为FALSE。
 
file_check_location($source, $directory = ‘’)
    有时候你有一个文件路径,但是你却不信任它。可能一个用户输入了它,想使用黑客技巧来获取站点的一些内部信息。(例如,提供了一个files/../../../etc/passwd,而不是一个有效的文件名)。“这个文件真的位于这个目录下面吗?”调用这个函数就可以回答这个问题。例如,如果文件的实际位置不在Drupal的文件系统路径下面,那么将会返回0:
 
$real_path = file_check_location($path, file_directory_path());
 
    如果文件位于Drupal的文件系统路径下面,那么将会返回文件的实际路径。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

上传文件

 

尽管上传模块提供了一个完整的实现,用来为节点上传文件;但是有时候,你不想把上传的文件与节点关联起来。下面的函数可帮你实现这一点。
 
file_save_upload($source, $validators = array(), $dest = FALSE,
$replace = FILE_EXISTS_RENAME)
    $source参数用来告诉函数哪个已上传的文件将被保存。$source对应于web表单中的文件输入字段的名字。例如,如果在“管理➤用户管理➤用户设置”中启用了头像图片支持,那么在“我的帐户”页面的表单上,就会有一个文件字段用来允许你上传自己的图片,该字段的名字就是picture_upload。显示在浏览器中的表单,如图13-4所示。当用户点击保存按钮时,将会得到$_FILES全局变量,如图13-5所示。注意,$_FILES中的信息是以表单的文件字段的名字为键的(这样,就可以在单个表单中支持多个文件字段了)。全局变量$_FILES是由PHP本身定义的,而不是由Drupal。
 
13-4.表单元素的文件字段,它显示在“我的帐户”页面
13-5.HTTP POST之后,得到的$_FILES全局变量
 
    $validators参数是一个数组,里面包含了成功文件上传后所要调用的函数的名字。例如,user_validate_picture()函数,当用户编辑了他/她的“我的帐户”页面以后将调用这个表单验证函数,这个函数在调用file_save_upload()以前添加了3个验证器。如果需要向验证器函数中传递参数,那么可将参数定义在后面的数组中。例如,在下面的代码中,当验证器运行时,对file_validate_image_resolution()的调用应该像file_validate_image_resolution('85x85')一样:
 
/**
 * Validates uploaded picture on user account page.
 */
function user_validate_picture(&$form, &$form_state) {
    $validators = array(
        'file_validate_is_image' => array(),
        'file_validate_image_resolution' =>
            array(variable_get('user_picture_dimensions', '85x85')),
        'file_validate_size' => array(variable_get('user_picture_file_size', '30')
            * 1024),
    );
    if ($file = file_save_upload('picture_upload', $validators)) {
        ...
    }
    ...
}
 
    file_save_upload()函数中的$dest参数是可选的,它包含的是文件将被复制到的目录。例如,在处理把文件附加在一个节点上时,上传模块使用file_directory_path()(默认为sites/default/files)作为$dest的值(参看图13-6)。如果没有提供$dest,那么将使用临时目录。
    $replace参数用来定义,在一个同名文件已存在时,Drupal应该做什么。可能值如表13-3所示。
 
13-6.文件对象已存在,当它传递给file_save_upload()的验证器时的情景
 
    file_save_upload()的返回值是一个包含了完整属性的文件对象(如图13-7所示);如果有地方出错的话,那么将返回0。
 
13-7.成功调用file_save_upload()以后,返回的文件对象
 
    在调用了file_save_upload()以后,在Drupal的临时目录中新增了一个文件,同时向files表中写入了一条新纪录。该纪录包含的值与如图13-7所示的文件对象相同。
 
    注意状态字段被设置为了0。这意味着到目前为止,在Drupal看来,这个仍然是一个临时文件。调用者需要负责将该文件改为持久的。继续使用我们的上传一个用户头像这个例子,我们看到用户模块负责将这个文件复制到了Drupal的user_picture_path变量所定义的目录中,并使用用户的ID对其重命名:
 
// The image was saved using file_save_upload() and was added to the
// files table as a temporary file. We'll make a copy and let the garbage
// collector delete the original upload.
$info = image_get_info($file->filepath);
$destination = variable_get('user_picture_path', 'pictures') .
'/picture-'. $form['#uid'] .'.'. $info['extension'];
file_copy($file, $destination, FILE_EXISTS_REPLACE));
...
 
    这将已上传的图片移到了sites/default/files/pictures/picture-2.jpg。
    在前面的代码注释中所提到的垃圾收集器,用来清理临时目录中的过期的临时文件。对于每个临时文件,在files表中都为其保存了一条状态字段为0的纪录,所以Drupal知道需要清理哪些文件。垃圾收集器位于modules/system/system.module的system_cron()函数中。它将删除那些过期文件,这里的过期指的是超过了常量DRUPAL_MAXIMUM_TEMP_FILE_AGE所指定的秒数。该常量的值为1440秒,也就是24分钟。

    如果提供了$dest参数,并且文件被移动到了它的最终位置,来代替原来的临时目录,那么调用者可以通过调用file_set_status(&$file, $status)将files表中纪录的状态修改为持久的,这里面$file被设置为一个完整的文件对象(如图13-7所示),$status被设置为FILE_STATUS_PERMANENT。依照includes/file.inc,如果你想在你的模块中使用额外的状态常量的话,那么你必须从256开始,因为0, 1, 2, 4, 8, 16, 32, 64, 和128是为核心保留的。

 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

file_save_upload()中可用的验证函数如下所示。

老葛的Drupal培训班 Think in Drupal

file_save_upload()中可用的验证函数如下所示。
 
file_validate_extensions($file, $extensions)
    $file参数是一个文件的名字。$extensions参数是一个字符串,里面包含了使用空格定界的文件扩展名。如果文件的扩展名被允许的话,那么函数将返回一个空数组;如果文件的扩展名不被允许的话,那么函数将返回一个包含错误消息的数组,错误消息通常为只允许使用以下扩展名的文件:jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp。这个函数是一个可用于file_save_upload()的验证器。
 
file_validate_is_image(&$file)
    这个函数获得一个文件对象,并尝试将$file->filepath传递给image_get_info()。如果image_get_info()可以从该文件提取信息的话,那么这个函数将返回一个空数组;如果处理失败的话,那么将返回一个包含错误消息的数组,错误消息通常为仅允许JPEG,PNG和GIF图片。这个函数是一个可用于file_save_upload()的验证器。
 
file_validate_image_resolution(&$file, $maximum_dimensions = 0,
$minimum_dimensions = 0)
    这个函数获得一个文件对象,并在多个操作中使用$file->filepath。如果文件是一个图片的话,那么这个函数将检查该图片是否超过了$maximum_dimensions,如果可能的话将尝试调整它的大小。如果一切正常,那么将返回一个空数组;而$file对象,由于它是通过引用传递的,如果图片的大小被调整了,那么它的$file->filesize将被设置为新的大小。否则,数组将包含一个错误消息,比如该图片太小了;最小尺寸为320x240像素。$maximum_dimensions和$minimum_dimensions就是由“宽”+ “x”+ “高”构成的字符串,(例如,640x480或85x85)这里的“宽”“高”都是像素数。默认值0指示在大小上没有限制。这个函数是一个可用于file_save_upload()的验证器。
 
file_validate_name_length($file)
    $file参数是一个文件对象。如果$file->filename没有超过255字符,那么它返回一个空数组。否则它返回一个包含错误消息的数组,来指示用户使用一个短一点的名字。这个函数是一个可用于file_save_upload()的验证器。
 
file_validate_size($file, $file_limit = 0, $user_limit = 0)
    这个函数检查一个文件的大小低于文件的上限,或者一个用户的累积上限。$file参数是一个文件对象,它必须包含$file->filesize,它是以字节为单位的文件大小。$file_limit参数是一个整数,表示单位的最大字节数。$user_limit参数是一个整数,表示当前用户允许使用的最大字节数。0意味着“没有限制”。如果验证通过,那么将返回一个空数组;否则将返回一个包含错误消息的数组。这个函数是一个可用于file_save_upload()的验证器。

Drupal版本:

为一个文件获取URL

 

如果你知道一个已上传的文件的名字,并想告诉客户该文件的URL,下面的函数将会有用。
 
file_create_url($path)
    不管Drupal是运行在公共下载模式,还是运行在私有下载模式,这个函数都将为一个文件返回正确的URL。$path参数是指向文件的路径(例如,sites/default/files/pictures/picture-1.jpg 或pictures/picture-1.jpg)。生成的URL将会是http://example.com/sites/default/files/pictures/picture-1.jpg。注意这里没有使用文件的绝对路径名字。这样在不同位置(服务器)之间迁移Drupal站点时,会方便一些。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

在一个目录下查找文件

 

Drupal提供了一个功能强大的函数file_scan_directory()。它浏览一个目录,从中查找匹配给定模式的文件。
 
file_scan_directory($dir, $mask, $nomask = array('.', '..', 'CVS'), $callback = 0, $recurse = TRUE, $key = 'filename', $min_depth = 0)
 
让我们简要的学习一下这个函数签名:
 
• $dir是进行搜索的目录的路径。不要在结尾处包含符号“/”。
 
• $mask是一个模式,用来应用于目录中所包含的文件。它是一个正则表达式。
 
• $nomask是一个正则表达式数组。任何匹配$nomask模式的东西都将被忽略。默认数组包含.(当前目录), .. (父目录), 和CVS。
 
• $callback为每个匹配所调用的函数的名字。将向回调函数传递一个参数:文件的路径。
 
• $recurse是一个布尔值,用来指示搜索是否递归到子目录中去。
 
• $key用来决定为file_scan_directory()返回的数组使用什么键。可能的值有filename (匹配的文件的完整路径), basename (filename without path 不带路径的文件名字), 和name (filename without path and without file suffix不带文件路径和后缀的文件名字)。
 
• $min_depth是能够从中返回文件的目录的最小深度。
 
    返回值是一个包含对象的关联数组。数组的键取决于$key参数的值,默认为filename。下面是一些例子。
    扫描themes/bluemarine目录,查找以.css结尾的任意文件:
 
$found = file_scan_directory('themes/bluemarine', '\.css$');
 
    生成的包含对象的数组如图13-8所示。
 
13-8. file_scan_directory()返回的默认结果是一个包含对象的数组,其中以完整的文件名为键
 
    将$key参数修改为basename将改变结果数组的键,如下面的代码和图13-9所示。
 
$found = file_scan_directory('themes/bluemarine', '\.css$', array('.', '..', 'CVS'),
0, TRUE, 'basename');
 
13-9.现在的结果是以文件名为键,原有的完整文件路径被省略了
 
    $callback参数的使用,可使得Drupal方便的清空最优化CSS文件缓存,后者通常位于sites/default/files/css。drupal_clear_css_cache()函数使用file_delete作为回调:
 
file_scan_directory(file_create_path('css'), '.*', array('.', '..', 'CVS'),
'file_delete', TRUE);

老葛的Drupal培训班 Think in Drupal

Drupal版本:

查找临时目录

 

下面的函数用于报告临时目录的位置,通常称为“temp”目录。
 
file_directory_temp()
    这个函数首先检查Drupal变量file_directory_temp。如果该变量没有设置,那么对于Unix,它将查找/tmp目录;而对于Windows,它将查找c:\\windows\temp和c:\\winnt\temp目录。如果这些都不成功,那么它将把临时目录设置为文件系统路径内部的名为tmp的目录(例如,sites/default/files/tmp)。它将返回临时目录的最终位置,并将file_directory_temp变量设为该值。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

中和危险文件

 

假定你使用的是公共文件下载方法,并且你启用了文件上传。那么当有人上传一个名为bad_exploit.php的文件时,会发生什么呢?当攻击者访问http://example.com/sites/default/files/bad_exploit.php时,它会不会执行?但愿不会,有3个原因。首先,在上传的文件设置的允许的扩展名列表中永远不会出现.php。其次,.htaccess文件应该放在sites/default/files/.htaccess中(参看第20章)。然而,在几个常见的Apache配置中,上传文件exploit.php.txt也可能导致把该文件中的代码作为PHP代码进行执行(参看http://drupal.org/files/sa-2006-007/advisory.txt)。这样就给我们带来了第3个原因:修改文件名字从而无害的呈现文件。作为防御上传可执行文件的一个手段,可以使用下面的函数。
 
file_munge_filename($filename, $extensions, $alerts = TRUE)
    $filename参数是要被修改的文件名字。$extensions是一个字符串,包含了使用空格定界的文件扩展名。$alerts参数是一个布尔值,默认为TRUE,通过使用drupal_set_message()来警告用户,该文件的名字已被修改。返回的是修改后的文件名,向里面插入了下划线来禁止潜在的执行。
 
$extensions = variable_get('upload_extensions_default', 'jpg jpeg gif png txt
doc xls pdf ppt pps odt ods odp');
$filename = file_munge_filename($filename, $extensions, FALSE);
 
$filename 现在为 exploit.php_.txt.
 
    通过在settings.php中将Drupal变量allow_insecure_uploads定义为1,你就可以阻止修改文件名了。但这通常是一个坏点子,因为它带来了安全隐患。
 
file_unmunge_filename($filename)
    这个函数尝试撤销file_munge_filename()的影响,它将“_.”替换为了“.”:
 
$original = file_unmunge_filename('exploit.php_.txt);
 
$original 现在为 exploit.php.txt.
 
    注意,如果在原始文件中,故意使用了“_.”,那么它也将被替换掉。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

检查磁盘空间

 

下面的函数用来报告文件所用的空间。
 
file_space_used($uid = NULL)
    这个函数用来返回文件使用的总的磁盘空间。它没有实际的去检查文件系统,而是使用数据库中files表的filesize字段的总计作为返回值。如果向这个函数传递了一个用户ID,那么对files表的查询将限定在匹配该用户ID的文件上。上传模块使用upload_space_used()对此函数作了简单的包装。由于只有当启用了上传模块时,才可以使用upload_space_used(),所以一般直接调用file_space_used()。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

用于下载的认证钩子

 

模块开发者可以通过实现hook_file_download(),来设置私有文件下载的访问权限。该钩子用于判定在什么条件下才把文件发送给浏览器,并为Drupal返回附加头部以追加到文件HTTP请求上。注意,如果你的Drupal安装使用的是公共文件下载设置,那么该钩子将不起任何作用。图13-10显示了下载流程的概览,这里以用户模块里面的hook_file_download()实现为例。
    由于对于每次下载,Drupal将触发所有实现了hook_file_download()钩子的模块,所以指定你钩子的范围就非常重要了。例如,以user_file_download()为例,只有当要下载的文件位于pictures目录时才响应文件下载。如果为真的话,它把头部信息添加到请求中。
 
function user_file_download($file) {
    $picture_path = variable_get('user_picture_path', 'pictures');
    if (strpos($file, $picture_path .'/picture-') === 0) {
        $info = image_get_info(file_create_path($file));
        return array('Content-type: '. $info['mime_type']);
    }
}
 
13-10.私有文件下载请求的生命周期
 
    如果请求被许可了,那么hook_file_download()实现就应该返回一个包含头部信息的数组;否则,返回-1表示拒绝了文件下载。如果没有模块响应这个钩子,那么Drupal将向浏览器返回一个404未找到错误信息。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

总结

 

在本章中,你学到了
• drupal公共和私有文件之间的区别
• 用于处理图片、视频、音频文件的drupal第3方模块
• 用于文件存储的数据库模式
• 操作文件的drupal常用函数
• 用于私有文件下载的认证钩子

老葛的Drupal培训班

Drupal版本:

中英文对照

 

munge:修改,混合,(译者注,这个词在我的金山词霸上没有,我在这里把它翻译为了修改,munge filename,修改文件名,这里的修改指的是把文件名中后缀部分的”.”号替换为“_.”。另外还有munge forms的用法,在本书中,指的是将两个表单定义数组混合(合并)在一起)
 
Public file的翻译
       我最初是翻译为公共文件,在重新翻译drupal6版的时候,使用简体中文包确认了一下,里面翻译为了“公开文件”,我觉得这两种翻译都挺好的,尽管我觉得我自己的翻译好一点,还是采用了“公开文件”。我查了金山词霸,发现里面是“公开的”意思,尽管也隐含了公开的意思。
   最终决定把它翻译为“公开”,把里面的替换了一遍,但是在翻译“public file download method”时,遇到了问题,如果翻译为“公开文件下载方法”的话,与原文出入太大,而使用“公共文件下载方法”则比较贴切。
   所以我又把“公开”改为“公共”。
   另外,我们都知道,有公共财产,私有财产之分,这里的公共财产的公共,英文就是public,而这里的私有就是private。两者是对应的。所以最后,尽管简体中文包里面使用了“公开”,我还是坚持使用了“公共”。
   我在翻译的时候,当一个词语的译法不确定的时候,尽可能使用金山词霸中现有的翻译,尽可能的采用简体中文包中的习惯译法,但是在确定简体中文包中的译文有问题时,或者说有不贴切的地方时,我坚定地把它修改了过来。
   比如vocabulary,这个词,在简体中文包中,有的地方被翻译为了“词汇表”,有的地方被翻译成了“术语表”,由于term一词被翻译成了术语,这里把vocabulary翻译成“术语表”也是很贴切的,但是我还是坚持把它翻译成“词汇表”,因为金山词霸中就是这么翻译的,词汇表下面也是可以放置术语的,词汇表这个概念比术语表更宽泛一些,最重要的一点,有个第3方模块,就是术语表模块(glossary module),如果我们这里把vocabulary翻译成了术语表,假定有一天,有人翻译“glossary module”的话,就会出现冲突。所以,这个也最终决定采用“词汇表”。
 还有“workflow settings”,被我翻译成了“工作流设置”,现有的译文是“流程设定”,现有译文不贴切,设定对应的英文为set up,这里没有“定”的意思,所以翻译的不贴切,另外workflow翻译成“流程”,也不大合适,这里讲的就是Drupal中的工作流,而不是Drupal的流程,这里面涉及的概念就是“工作流”,“触发器”,“动作”,在现有的计算机用语中都有对应的译法,所以我翻译成了“工作流设置”。
 另外,configuration,对应于“配置”, setting对应于“设置”。“配置”和“设置”好像是近义词,我建议将所有出现configuration的地方都统一的翻译成“配置”,所有的setting都翻译成“设置”。所以我大胆的将“站点设置”改为了“站点配置”。
       另外还有很多地方的译文,在坚持原有译法的同时,对现有的简体中文包作了批判。尽可能采用金山词霸中现有的翻译,尽可能的采用简体中文包中的习惯译法,这是我翻译时的准则,所以当你看到与现有简体中文包中有不一致的译文时,不要惊讶。
    另外,我发现简体中文包的许多地方,不是翻译的不贴切,而是翻译错了,由于时间的关系,无力去一一的修正里面的错误。
    另外,我自己的译文中,在本书中,也有个别地方不一致,限于译者的水平有限,以及可能存在的其它疏漏,望请批评指正。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

19. drupal XML-RPC

   Drupal 可与外部系统进行良好的集成。也就是说,如果存在一个开放的标准,drupal可以通过核心模块或者第3方模块这两种方式来支持这一标准。XML-RPC也不例外:Drupal内置了对它的支持。在本章中,你将学习到如何在drupal中,发送和接受XML-RPC调用。

Drupal版本:

1.什么是XML-RPC

                    译者:老葛, Think in Drupal

     一个远程过程调用(remote procedure call)是指一个程序要求另一个程序执行一个函数。XML-RPC就是远程过程调用的标准,它采用XML格式编码并使用HTTP传送。XML-RPC协议是由UserLand软件公司的Dave Winner与微软合作创造出来的。它是专门用于分布式网络系统之间的相互对话的,比如当一个Drupal网站需要另一个Drupal网站的一些信息时,就可以使用XML-RPC。

 

     当一个XML-RPC发生时,存在着两个参与者。一个是产生请求的网站,作为客户端。另一个是接受请求的,作为服务器端。

Drupal版本:

2.drupal XML-RPC的前置条件

译者:老葛, Think in Drupal

如果你的网站仅仅用作服务器, 这就没有任何担心了,因为传入的XML-RPC请求使用标准的web端口(通常为端口80).在你的Drupal安装目录中的文件xmlrpc.php里面,包含了处理XML-RPC请求的代码。它也被称为XML-RPC终端。

 

注意:部分用户可能会悄悄的对文件xmlrpc.php进行重命名,来修改他们的终端,从而增加安全性。这会阻止恶意的网络爬虫,探测到服务器的XML-RPC接口。如果你的网站不接受XML-RPC请求的话,那么完全可以将其删除。

 

为了让你的Drupal网站能够作为客户端,那么它就必须具有向外发送HTTP请求的能力。一些主机服务提供商出于安全原因禁止了这一能力,这样你的drupal网站发送的HTTP就穿过不了他们的防火墙。

Drupal版本:

XML-RPC 客户端

 

客户端是用来发送请求的的计算机。它向服务器端发送一个标准的HTTP POST请求。这一请求的主体是由XML组成的,并且包含一个简单的名为<methodCall>的标签。在<methodCall>标签内部,嵌套了两个子标签,<methodName>和<params>。让我们通过一个实例来看一下它是如何工作的。
 

 

注意:远程过程被调用时是作为一个方法被引用的。这就是为什么XML编码将远程过程的名字包装在<methodName>标签里的原因。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

XML-RPC 客户端例子:获取时间

在网站http://www.xmlrpc.com上可以看到XML-RPC说明,它同时也带有了一些可用于测试的例子。在我们的第一个例子中,让我们通过XML-RPC来向该站点请求当前时间:

 
 
    在这里你调用了Drupal的xmlrpc()函数,告诉它链接到服务器time.xmlrpc.com,并且路径为RPC2,请求服务器端执行一个名为currentTime.getCurrentTime()的方法。在这一调用中,你没有使用任何参数。Drupal将其转化为一个如下所示的HTTP请求:
 
POST /RPC2 HTTP/1.0
Host: time.xmlrpc.com
User-Agent: Drupal (+http://drupal.org/)
Content-Length: 118
Content-Type: text/xml
 
<?xml version="1.0"?>
<methodCall>
<methodName>currentTime.getCurrentTime</methodName>
<params></params>
</methodCall>
 
服务器端time.xmlrpc.com非常高兴的执行该函数,并为你返回如下所示的响应:
 
HTTP/1.1 200 OK
Connection: close
Content-Length: 183
Content-Type: text/xml
Date: Wed, 23 Apr 2008 16:14:30 GMT
Server: UserLand Frontier/9.0.1-WinNT
 
<?xml version="1.0"?>
<methodResponse>
<params>
<param>
<value>
<dateTime.iso8601>20080423T09:14:30</dateTime.iso8601>
</value>
</param>
</params>
</methodResponse>
 
当响应返回后,Drupal解析它,将其识别为一个简单的采用ISO8601国际日期格式的值,并将此值分配给变量$time. Drupa不仅返回了ISO8601格式的时间,并且还包括时间的组成部分比如年,月,日,小时,分钟,和秒。具有这些属性的对象被赋值给$time变量,如图19-1所示。
 
19-1. XML-RPC调用的结果,获取了当前时间
 
 在这里有几个要点需要注意:
    你调用一个远程服务器,接着它响应你。
    请求和响应都是通过XML来描述的。
    你使用了xmlrpc()函数,里面包含一个URL和要调用的远程过程的名字。
    返回给你的值将作为一个特定数据类型来标记。
    Drupal自动识别该数据类型并解析响应。
    你仅用一行代码就搞定了一切。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

XML-RPC 客户端例子:获取州名

  让我们尝试一个稍微复杂的例子。它仅仅复杂了一点点,因为你不但发送了你所调用的远程方法的名称,而且还包括了一个参数。UserLand软件在站点betty.userland.com运行了一个web服务:它将50个美国的州以字母顺序排列。所以如果你请求第1个州,它返回Alabama。第50个州为Wyoming。方法的名称为examples.getStateName。让我们向它请求列表中的第3个州:

 
$state_name = xmlrpc('http://betty.userland.com/RPC2', 'examples.getStateName', 3);
 
它将$state_name设置为Arizona.下面是Drupal发送的XML(为了简洁,从这里起我们省略了HTTP头部)
 
<?xml version="1.0"?>
<methodCall>
<methodName>examples.getStateName</methodName>
<params>
<param>
<value>
<int>3</int>
</value>
</param>
</params>
</methodCall>
 
下面是你从betty.userland.com获得的相应:
 
<?xml version="1.0"?>
<methodResponse>
<params>
<param>
<value>Arizona</value>
</param>
</params>
</methodResponse>
 
注意,Drupal能够自动的识别你传递的参数是一个整数,并在你的请求中以此来对它编码。但是在响应中发生了什么呢?在返回值的周围没有使用任何类型标签。难道不是这种形式么<value><string>Arizona</string></value>?是的,这个也能工作。不过在XML-RPC中,一个没有类型的值将被默认为字符串类型,这样更简洁。
Drupal中,进行一个XML-RPC客户端调用是非常简单的。仅用一行代码:
 
$result = xmlrpc($url, $method, $param_1, $param_2, $param_3...)
 老葛的Drupal培训班  Think in Drupal

Drupal版本:

处理XML-RPC客户端错误

当与远程的服务器打交道时,经常会出错.例如,你可能会遇到语法错误;服务器可能会挂掉;或者网络连接不通.让我们看看Drupal是如何处理这些情况的.

 
网络错误
Drupal使用includes/common.inc中的drupal_http_request()函数来发送HTTP请求,包括XML-RPC请求.在该函数内部,使用PHP函数fsockopen来打开一个套接字,用于连接远程服务器.如果套接字打不开,Drupal将会根据运行的PHP平台,以及在打开套接字时错误的发生点,来设置一个负值的错误代码或者0值错误代码.当获取州名时,假定我们拼错了服务器的名字:
 
$state_name = xmlrpc('http://betty.userland.comm/RPC2', 'examples.getStateName', 3);
if ($error = xmlrpc_error()) {
if ($error->code <= 0) {
$error->message = t('Outgoing HTTP request failed because the socket could
not be opened.');
}
drupal_set_message(t('Could not get state name because the remote site gave
an error: %message (@code).', array(
'%message' => $error->message,
'@code' => $error->code
)
)
);
 
这将生成如下所示的消息:
 
无法获取州名,因为远程网站给出了一个错误: 因为无法打开套接字,所以发出的HTTP请求失败了.( -19891355)。
 

老葛的Drupal培训班  Think in Drupal

Drupal版本:

HTTP错误

老葛的Drupal培训班 Think in Drupal

前面的代码也适用与HTTP错误,比如当服务器不存在,但是该路径上的web服务不存在时.现在,我们向drupal.org请求该服务, drupal.org指出 http://drupal.org/RPC2不存在:

 
$state = xmlrpc('http://drupal.org/RPC2', 'examples.getStateName');
if ($error = xmlrpc_error()) {
if ($error->code <= 0) {
$error->message = t('Outgoing HTTP request failed because the socket could
not be opened.');
}
drupal_set_message(t('Could not get state name because the remote site gave
an error: %message (@code).', array(
'%message' => $error->message,
'@code' => $error->code
)
)
);
 
这将生成如下所示的消息:
 
无法获取州名,因为远程网站给出了一个错误: 未找到 (404).
 

Drupal版本:

调用语法错误

如果你成功的连接到了服务器,尝试从betty.userland.com获取州名,但是却忘记提供州号了,而这个又是必须的参数:

 
$state_name = xmlrpc('http://betty.userland.com/RPC2', 'examples.getStateName');
 
远程服务器返回以下结果:
 
<?xml version="1.0"?>
<methodResponse>
<fault>
<value>
<struct>
<member>
<name>faultCode</name>
<value>
<int>4</int>
</value>
</member>
<member>
<name>faultString</name>
<value>
<string>Can't call "getStateName" because there aren't enough
parameters.</string>
</value>
</member>
</struct>
</value>
</fault>
</methodResponse>
 
    与服务器的连接是好的;前面的代码返回的HTTP响应为200 OK.在XML响应中, faultCode用来标记错误,并使用一个字符串来描述错误.你的错误处理代码应该与前面一样:
 
$state_name = xmlrpc('http://betty.userland.com/RPC2', 'examples.getStateName');
if ($error = xmlrpc_error()) {
if ($error->code <= 0) {
$error->message = t('Outgoing HTTP request failed because the socket could
not be opened.');
}
drupal_set_message(t('Could not get state name because the remote site gave
an error: %message (@code).', array(
'%message' => $error->message,
'@code' => $error->code
)
)
);
 
这将为用户生成如下所示的消息
 
无法获取州名,因为远程网站给出了一个错误:由于参数不够,导致无法调用"getStateName". (4)
 
注意当你报告错误的时候,你应该指出3件事情:你想要做什么,为什么你不能完成它,和其它一些你可以获取到的额外信息。通常一个友好的错误信息将通过drupal_set_message()来显示给用户,同时一个更加详细的错误信息将会写到watchdog中去并可通过“管理➤报告➤最近日志条目”来查看。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

参数类型转换

老葛的Drupal培训班 Think in Drupal

通常你所调用的远程过程要求参数必须是特定的XML-RPC类型,比如整形(integers)或者数组。一种保证这样的方式是使用PHP的类型转换来发送你的参数:
 
$state_name = xmlrpc('http://betty.userland.com/RPC2', 'examples.getStateName',
(int) $state_num);
 
一种更好的方式是保证在你代码的其他地方当给变量赋值时,这一变量已经被设置为相应的类型了。
 

Drupal版本:

一个简单的XML-RPC服务器

正如在XML-RPC客户端例子中所看到的,Drupal为你做了大部分工作。现在让我们看一个简单的服务器端的例子。你需要做3件事来建立你的服务器:

 
1, 定义一个当客户请求到来时你想执行的函数。
2, 将函数映射为一个公共的方法名。
3, 可选的定义一个方法签名。
 
    按照Drupal的惯例,你想将你的代码与系统核心分开并将其作为一个模块插入。下面是一个简单的模块,用来通过XML-RPC说”你好”.创建文件sites/all/modules/custom/remotehello/remotehello.info:
 
; $Id$
name = Remote Hello
description = Greets XML-RPC clients by name.
package = Pro Drupal Development
core = 6.x
 
    下面是 remotehello.module:
 
<?php
// $Id$
 
/**
 * Implementation of hook_xmlrpc().
 * Map external names of XML-RPC methods to PHP callback functions.
 */
function remotehello_xmlrpc() {
$methods['remoteHello.hello'] = 'xmls_remotehello_hello';
return $methods;
}
 
/**
 * Greet a user.
 */
function xmls_remotehello_hello($name) {
if (!$name) {
return xmlrpc_error(1, t('I cannot greet you by name if you do not
provide one.'));
}
return t('Hello, @name!', array('@name' => $name));
}
 老葛的Drupal培训班  Think in Drupal

Drupal版本:

使用hook_xmlrpc()映射你的方法

xmlrpc钩子描述了由模块所提供的外部XML-RPC方法。在我们的例子中,我们仅提供了一个方法。,所以这里,方法名字为:remoteHello.hello。这是请求者使用的名字,它是任意的。一个好的实践是使用“.“分割的字符串,使用你的模块名作为前半部分,使用一个描述性的动词作为后半部分。

 
 
注意:尽管通常在Drupal里面避免使用骆驼形式的字符串,但在XML-RPC方法名中这是个例外。
 
  数组的第2部分,是对remoteHello.hello的请求到来时,所要调用的函数的名称。在我们的例子中,我们将这一函数叫做xmls_remotehello_hello ()。当你开发模块时,你将写很多的函数。通过在函数名字中包含”xmls“(XML-RPC Server的简写), 这样你就可以一眼看出这个函数是与外界交互的。类似的,你可以在函数中使用”xmlc“,用以调用其他网站上的方法。当你写一个主要调用自身的模块时,这是个很好的实践,尽管在另一个网站上,否则的话,调试时你会非常困惑。
  当你的模块认定一个错误发生时,使用xmlrpc_error()来定义一个错误代码和一个用以描述哪里出错的帮助字符串,以显示给客户。数字错误代码是任意的,并且是应用相关的。
  假定带有这一个模块的站点位于example.com上,现在你可以在一个单独的Drupal安装上(比如说,在example2.com)使用以下代码来来发送你的名字:
 
$method_name = 'remoteHello.hello';
$name = t('Joe');
$result = xmlrpc($url, $method_name, $name);
 
$result现在是 "Hello, Joe."
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

在hook_xmlrpc()中进行自动的参数类型验证

    xmlrpc钩子有两种形式。简单的形式,如例子remotehello.module中所展示的,它简单的将一个外部的方法名映射到一个函数上。在一个更高级的形式中,它描述了方法的方法签名;这里指的是,它返回的是什么XML-RPC类型,以及每一个参数的类型(参看http://www.xmlrpc.com/spec来查看类型列表).下面是remotehello.module的升级版,xmlrpc钩子的形式更复杂一些:

 
function xmlrpclucky_xmlrpc() {
return array(
array(
'xmlrpclucky.guessLuckyNumber', // External method name.
'xmlrpclucky_lucky_number', // Drupal function to run.
array('string', 'int'), // Return value's type, then any parameter types
t('Returns a lucky number.') // Description.
)
);
}
 
19-2显示了当一个请求从XML-RPC客户端到达我们的模块时XML-RPC的请求生命周期。如果你在你的模块中使用更复杂的形式来实现xmlrpc钩子,你将得到多个好处。首先,Drupal将根据方法签名自动验证发送过来的类型并返回 -32602:Server error。如果验证失败将标示出无效的方法参数。(这还意味着你的函数具有挑选能力.而不再进行类型转换了,如果是整数3,那么就不能当作字符串“3”使用了).如果你使用xmlrpc钩子的复杂形式, Drupal内置的XML-RPC方法system.methodSignature和system.methodHelp将返回你方法的相关信息.注意,你在你的xmlrpc钩子实现中提供的描述,将会在system.methodHelp方法中作为帮助信息返回,所以你需要写一个有用的描述.
19-2 一个XML-RPC请求的处理流程图
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

drupal内置的XML-RPC方法

Drupal自带了多个XML-RPC方法.在下面的部分中,将讲解这些内置方法.

 
system.listMethods
system.listMethods方法列出了有哪些XML-RPC方法可用.当查询一个Drupal站点提供了哪些方法时,该站点所给出的响应.
 
// Get an array of all the XML-RPC methods available on this server.
$methods = xmlrpc($url, 'system.listMethods');
 
服务器的响应如下:
 
<?xml version="1.0"?>
<methodResponse>
<params>
<param>
<value>
<array>
<data>
<value>
<string>system.multicall</string>
</value>
<value>
<string>system.methodSignature</string>
</value>
<value>
<string>system.getCapabilities</string>
</value>
<value>
<string>system.listMethods</string>
</value>
<value>
<string>system.methodHelp</string>
</value>
<value>
<string>remoteHello.hello</string>
</value>
</data>
</array>
</value>
</param>
</params>
</methodResponse>
 
$methods的内容现在是一个数组,里面包含了服务器上可用的方法名字:
('system.multicall', 'system.methodSignature', 'system.getCapabilities',
'system.listMethods', 'system.methodHelp', 'remoteHello.hello').
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

system.methodSignature

老葛的Drupal培训班 Think in Drupal

system.methodSignature

这个内置的Drupal XML-RPC方法返回一个数据类型的数组.列表中的第一个是函数返回值的数据类型;接着是给定方法所需的任意参数.例如, remoteHello.hello方法返回了一个字符串,并期望一个参数:一个包含了客户端名称的字符串.让我们调用system.methodSignature来看看Drupal是不是这样的.
 
// Get the method signature for our example method.
$signature = xmlrpc($url, 'system.methodSignature', 'remoteHello.hello');
 
果然, $signature的值变成了一个数组:('string', 'string').
 
system.methodHelp
这个内置的Drupal XML-RPC方法,返回xmlrpc钩子中定义的方法的描述.
 
// Get the help string for our example method.
$help = xmlrpc($url, 'system.methodHelp', 'remoteHello.hello');
 
$help的值现在是一个字符串: Greets XML-RPC clients by name.
 
system.getCapabilities
这个内置的Drupal XML-RPC方法描述了Drupal的XML-RPC服务器能力,这里根据实现的规格进行描述. Drupal实现了以下规格:
 
xmlrpc:
specVersion 1
 
faults_interop:
specVersion 20010516
 
system.multicall
specVerson 1
 
introspection
specVersion 1
 
system.multiCall
其它值得一提的内置方法就是system.multiCall了,它允许在一个HTTP请求中调用多个XML-RPC方法。关于这一个规定的更多信息(它没有包含在XML-RPC说明中),参看以下URL(注意这是一个字符串):http://web.archive.org/web/20060502175739/http:// www.xmlrpc.com/discuss/msgReader$1208

Drupal版本:

总结

当读完这一章后,你应该可以:

    能够从一个Drupal站点上发送XML-RPC请求到一个不同的服务器上
    能够实现一个基本的XML-RPC服务器
    理解Drupal是如何将XML-RPC方法映射到php函数上的
    能够实现简单的和复杂的xmlrpc钩子
    了解Drupal内置的XML-RPC方法
老葛的Drupal培训班 Think in Drupal
 

Drupal版本:

Drupal6专业开发指南目录

 老葛的Drupal培训班 Think in Drupal

Drupal6专业开发指南目录,详情:  http://www.thinkindrupal.com/group/616/story/667

 Drupal开发指南目录

 
…………………………………………………………………………………….3
关于作者…………………………………………………………………………….4
技术审稿人………………………………………………………………………….5
关于译者…………………………………………………………………………….6
致谢………………………………………………………………………………….7
译者致谢…………………………………………………………………………….8
译者序……………………………………………………………………………….9
导言………………………………………………………………………………….10
 
1Drupal的工作原理………………………………………..11
       什么是Drupal?……………………………………………………11
     技术堆栈……………………………………………………………..11
       核心…………………………………………………………………...12
       后台管理界面………………………………………………………...13
       模块…………………………………………………………………...13
       钩子…………………………………………………………………...14
       主题……………………………………………………………………15
       节点……………………………………………………………………15
       区块……………………………………………………………………15
       文件布局………………………………………………………………16
       服务一个请求…………………………………………………………18
              Web服务器的角色………………………………………………...18
              引导指令流程…………………………………………………….18
              处理一个请求…………………………………………………….20
              主题化数据……………………………………………………….20
       总结……………………………………………………………………20

Drupal版本: