Sealyu

--- 博客已迁移至: http://www.sealyu.com/blog

  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  618 随笔 :: 87 文章 :: 225 评论 :: 0 Trackbacks

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

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

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

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

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

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

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

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

在一年多以前,我为本书的第一版写了序言。那时,在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培训班 http://zhupou.cn

 
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培训班 http://zhupou.cn

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

致谢

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

首先,感谢我的家庭,在编写本书的过程中,特别是在“一个简单的修订”转变为了一个和第一版同样巨大的工程时,他们的理解和支持,给了我莫大的鼓舞。
 
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培训班 http://zhupou.cn

去 年的现在,经过半年的时间,终于基本上完整的译完了本书的第一版,尽管译文中错误千出,但是还是被很多网友传阅。最初翻译这本书的时候,仅仅是因为我在看 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培训班  http://zhupou.cn

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

第一章 Drupal的工作原理

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

在本章中,我们将为你展示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培训班 http://zhupou.cn

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培训班 http://zhupou.cn

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

模块

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

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

钩子

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

可以把钩子看做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培训班 http://zhupou.cn

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

节点

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

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

区块

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

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

文件布局

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

通过了解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培训班 http://zhupou.cn

引导指令流程
    对于每个请求,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培训班 http://zhupou.cn

完成
    该阶段是引导指令的最后一个阶段,它包括加载一个通用函数库,主题支持,和支持回调映射,文件处理,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浏览器。

总结

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

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

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

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

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

创建相应的文件

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

首 先我们要做的是为模块起一个名字。名字“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培训班 http://zhupou.cn

回想一下,我们曾经说过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钩子的完整列表和它们的使用说明。
 

添加特定于模块的设置(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培训班 http://zhupou.cn

添加特定于模块的设置(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培训班 http://zhupou.cn

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

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

这里值得讨论一下#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的力量了。

添加数据输入表单(1)

添加数据输入表单(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培训班 http://zhupou.cn

添加数据输入表单(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培训班 http://zhupou.cn

把数据存储到数据库表中(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培训班 http://zhupou.cn

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

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

我 们可以把这段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;
}

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

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

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

把数据存储到数据库表中(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培训班 http://zhupou.cn
 

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

定义你自己的管理部分

       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培训班 http://zhupou.cn

为用户呈现一个设置表单(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培训班 http://zhupou.cn

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

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

下面让我们看一下定义表单的函数,它为节点类型定义了一个复选框(参看图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 使用了复选框,单选按钮,文本输入框的增强表单

验证用户提交的设置

  如果由函数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培训班 http://zhupou.cn

存储设置

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

 
使用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培训班 http://zhupou.cn

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

篇外话

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

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

 

总结

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

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

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

理解事件和触发器

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

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培训班 http://zhupou.cn
 

触发器用户界面

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

导航到“管理➤站点构建 ➤模块”,并启用触发器模块。接着导航到“管理➤站点构建 ➤触发器”。你将看到的界面应该与图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        当用户资料被浏览时
 

你的第一个动作

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

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培训班 http://zhupou.cn

分配该动作

修改动作所支持的触发器

支持所有触发器的动作

高级动作(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培训班 http://zhupou.cn

高级动作(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培训班 http://zhupou.cn

高级动作(3)

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

第一个函数向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培训班 http://zhupou.cn

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

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

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

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

让 我们设定一个场景。假定你有一个网站,是用来呈现争议性问题的。下面是它的业务模型:用户通过付费注册进来,并只能在网站上发布一条评论。一旦他们发布了 评论,他们就会被封号,直到再次付费后才被解封。我们不关心这样的网站是否有经济前景,这里主要考虑的是:如何使用触发器和动作来实现它。我们需要一个动 作来阻止当前用户。检查一下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_alter()修改已有的动作

建立上下文

由于我们已经分配了动作,所以当一个新评论被发布时,当前用户将被阻止。让我们仔细的看一下,这里都发生了什么。我们已经知道,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培训班 http://zhupou.cn

检查上下文

将钩子和操作放在上下文中,这一点非常有用。我们举个例子,动作“发送电子邮件”就大量的利用了这一点。这个动作的类型为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培训班 http://zhupou.cn

动作的存储

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

动作就是在给定时间运行的函数。简单的动作不带有可配置的参数。例如,我们创建的动作“嘟嘟”只会简单的嘟嘟一下。它不需要任何其它的信息(当然,如果需要的话还是可以使用$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中就使用了同样的方式,来区分内容和菜单常量。
 

直接使用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培训班 http://zhupou.cn

使用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培训班 http://zhupou.cn

向已有钩子中添加触发器

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

有时候,如果你的代码新增了一个操作的话,那么你可能想要在一个已有的钩子上添加触发器。例如,你可能想向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()函数起作用,我们需要访问“管理➤站点构建 ➤模块”,这样菜单将被重建。

总结

第4章 Drupal 菜单系统

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

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

回调映射

将URL映射为函数

创建一个菜单项

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

通过在你的模块中使用菜单钩子来钩住这一流程。这样你就可以定义包含在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()函数。
 

定义一个标题

前面所写的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培训班 http://zhupou.cn

页面回调参数(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培训班 http://zhupou.cn

页面回调参数(2)

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

你定义在页面参数中的回调参数将会首先传递给回调函数(也就是说,在传递给回调函数的参数列表中,它放在最前面),其次才是从路径中生成的参数。来自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会假定你把页面回调 放在了.module文件中。在Drupal6中,对于每个页面请求,为了尽可能的降低为其加载的代码总量,许多模块被拆分成了多个部分。如果回调函数不 在当前的.module文件中的话,可以使用菜单项中的file键,来指定哪个文件包含了该函数。我们在第2章中编写注释模块的时候,就用到了file 键。

 

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

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

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

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

我们把我们的菜单项的类型声明为了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培训班 http://zhupou.cn

到目前为止,我们仅仅定义了一个静态菜单项。让我们再添加一个与它相关的子项:
 
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.嵌套菜单
 

访问控制

到目前为止,在前面的所有例子中,我们都简单的将菜单项的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培训班 http://zhupou.cn

标题的本地化和定制

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培训班 http://zhupou.cn

定义标题回调

标题参数

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培训班 http://zhupou.cn

菜单项中的通配符

到目前为止,我们在菜单项中所用的都是普通的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培训班 http://zhupou.cn

通配符和页面回调参数

使用通配符的值

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

为了使用路径中匹配的那部分,可以在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路径的一部分通常是用来查看或者修 改一个对象的,比如一个节点对象或者一个用户对象。例如,路径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培训班 http://zhupou.cn

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

如果需要向加载函数传递额外的参数,那么可以使用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培训班 http://zhupou.cn

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

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

有两个特殊的加载参数。%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。

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

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

还记不记得我曾经说过,对于包含通配符的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

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

改变其它模块的菜单项

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

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,
)

改变其它模块的菜单链接

菜单项的种类

当你在菜单钩子 中添加一个菜单项时,经常用到的一个键就是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拥有下列标记:000000000110表4-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培训班 http://zhupou.cn

常见任务

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

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

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

将菜单项显示为标签

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培训班 http://zhupou.cn

隐藏已有菜单项

使用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培训班 http://zhupou.cn
 

常见错误

总结

第5章 Drupal的数据库层

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

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

定义数据库参数

在建立数据库连接时,通过查看你站点的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培训班 http://zhupou.cn
 

理解数据库抽象层

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

使 用一个数据库抽象层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培训班 http://zhupou.cn

作为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培训班 http://zhupou.cn

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);

取回查询结果

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

 
获得单个值
如果你需要的仅仅是来自数据库的单个值,那么你可以使用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培训班 http://zhupou.cn

获得限制范围内的结果

将结果分页显示

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

我们可以使用一个更好的方式来显示这些日志:分页显示。我们可以使用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的分页器,为结果集包含了内置的导航链接
 

模式(Schema)API

创建数据库表

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

安装钩子函数一般将数据库表的安装委托给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.使用模式定义创建数据库表
 

创建数据库表(1)

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

让我们看一下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类型的字段。它还有一个主键,一个唯一索引(这意味着该字段中的所有条目都是唯一的)和一个普通索引。注 意,在字段描述中,引用另一个表中的字段时,需要为其使用花括号。这样模式模块(参看下一节)可以为表的描述构建方便的超链接。
 

使用模式模块

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

在模式定义中声明的字段类型,将会映射成数据库中的本地字段类型。例如,一个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培训班 http://zhupou.cn

数字型

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

数字型数据类型是用来存储数字的,它包括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.'),
);

日期和时间: Datetime

使用mysql_type声明特定字段类型

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

如果你知道你的数据库引擎的准确字段类型, 那么你可以在你的模式定义中使用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培训班 http://zhupou.cn

当你为一个模块创建新版本时,你可能需要修改数据库模式。可能,你添加了一列,或者为某一列添加了索引。由于该表已经包含了数据,所以你不能简单地删除并重建该表。下面给出了如何保证平稳的修改数据库表:
 
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表中删除该模块的记录。
 

在Uninstall中删除数据库表

使用hook_schema_alter()修改已有模式

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

一般来说,模块会创建和使用它们自己的数据库表。但是,如果你的模块想修改一个已有的表时,那该怎么办呢?假定你的模块需要向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_write_record()进行插入和更新

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

程序员常遇到的一个问题,就是处理数据库中新纪录的插入和已有记录的更新。代码一般都会先检查当前的操作是一个插入操作还是一个更新操作,接着再执行合适的操作。
 
    因为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()会在内部将其转化为一个对象。
 

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

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

这个钩子可以用来修改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()       当你想修改其它模块中的查询时
 

使用hook_db_rewrite_sql()

修改其它模块的查询

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

使用临时表

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

总结

第6章 Drupal用户

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

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

对象$user

向对象$user中存储数据

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

表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添加数据”一节进行讨论。
 

测试用户是否登录了

hook_user()入门

理解hook_user('view')

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

模块可以使用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']);
}
 

用户注册流程

用户注册流程(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培训班 http://zhupou.cn

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

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

如 果你想扩展用户注册表单以收集用户信息,在你打算编写自己的模块以前,你可以先试用一下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,或者日期字段。
 

登录流程

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

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

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

为了存储登录信息,我们需要使用一个.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培训班 http://zhupou.cn

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

外部登录

简单的外部认证(1)

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

让我们实现一个非常简单的外部认证模块,来说明外部认证的工作原理。假定你的公司只雇用名为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

简单的外部认证(2)

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

而下面则是实际的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():如果没有成功的加载用户,那么设置一个错误信息“对不起,无效的用户名或密码。你是不是忘记密码了?”,并向系统写入一个日志记录“用户尝试登录失败”。

简单的外部认证(3)

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

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

总结

第7章 Drupal节点

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

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

那么什么才是节点呢?

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

对于刚刚接触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 源于基本节点的节点类型和可能添加的字段
 

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

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

通常通过为节点类型添加它们自己的属性,来扩展基本节点。节点类型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/modr8和http://drupal.org/project /revision_moderation,使用了该字段。
 
• sticky:当Drupal在一个页面中显示一列节点时,默认情况是将标记为“置顶”的节点列在前面,接着按照创建日期列出剩下的“不置顶”节点。换句话说就是“置顶”的节点位于节点列表的顶部。1表示“置顶”,0表示“不置顶”。你可以在同一列表中包含多个“置顶”节点。
 
• tnid:当一个节点作为另一个节点的翻译版本时,被翻译的源节点的nid将被存储在这里。例如,如果节点3的语言为英语,节点5是节点3的瑞典语翻译,那么节点5的tnid字段就为3。
 
• translate:有两个可选的值,1意味着翻译需要被更新;0意味着翻译是最新的。
 
如果你使用了Drupal的修订本系统,Drupal将创建一个内容的修订本,同时又追踪了谁在最后修改了节点。
 

不是所有的东西都是节点

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

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

创建一个节点模块

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

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

创建.install文件

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

你将需要在你的数据库表中存储一些信息。首先,你需要节点的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;
}
 

创建.info文件

创建.module文件

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

修改菜单回调

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

使用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培训班 http://zhupou.cn

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

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

到目前为止,你已经为你的新节点类型定义了元数,还有定义了访问控制权限。接着,你需要构建节点表单,这样用户就可以输入笑话了。你可以通过实现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.笑话的提交表单
 

添加过滤器格式支持

由于主体字段是一个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培训班 http://zhupou.cn

使用hook_validate()来验证字段

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

当 用户提交一个你节点类型的节点时,将会调用你模块中的钩子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字段是你添加到该节点类型表单中的一个额外字段,所以你需要负责它的验证(加载、保存)。
 

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

使用hook_update()保持数据同步

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

当 编辑完一个节点,并且节点核心数据已被写入到数据库中时,将会调用钩子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);
    }
}
 
在这里,你首先检查看是否设置了节点修订本标记,如果设置了,你为笑话妙语创建一个新的拷贝来替代旧的版本。
 

使用hook_delete()清理数据

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

使用hook_view()显示笑话妙语

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

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

你需要清除主题注册表的缓存,这样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支持,来隐藏笑话妙语
 

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

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

前面的钩子只有在基于模块的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. 显示一个节点页面的执行路径
 

如何存储节点

使用CCK创建节点类型

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

在前面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培训班 http://zhupou.cn

有 多种方式可用于限制对节点的访问。你已经看到了,如何使用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将拒绝对节点的访问。
 

定义节点授权(Grants)

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

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

什么是领域(Realm)?

什么是授权(Grant)ID

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

一个授权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;
}
}

节点访问流程

总结

第8章 主题系统

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

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

模板语言和主题引擎

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

主题系统可以使用多个模板语言。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的行话来说,主题就是一组负责你站点外观的文件。你可以从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培训班 http://zhupou.cn

安装主题

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

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

构建一个PHPTemplate主题

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

创建一个主题,可以有多种方式,这取决于你的起始材料。假定你的设计者已经为你的站点提供了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主题)。
 

使用已有的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培训班 http://zhupou.cn

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

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

每个主题都需要包含一个文件,用来向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文件,所以你就可以在管理界面中启用它了。导航到“管理➤站点构建➤主题”,将它设置为默认主题。

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

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

恭喜恭喜!现在你应该可以实际的看到你的设计了。外部的样式表还没有加载进来(我们将在后面讨论它),访问你的站点中的任何页面,都会一而再再而三的显示同一个页面,尽管如此,这也是一个了不起的开始!由于访问你的站点中的任何页面,都会显示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培训班 http://zhupou.cn

一些主题包含各种模板文件,而有些仅包含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中的其它模板,也同样适用。
 

theme()函数介绍

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

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

theme()工作原理概览

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

当一个简单的节点页面显示时,比如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培训班 http://zhupou.cn

正如你看到的,可主题化的项目是通过它们的函数名来标识的,每个函数名前都带有前缀“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_”开头的主题函数,这样设计者就可以对它们进行覆写了。

覆写模板文件

添加和操纵模板变量

问题又来了:如果你可以创建你自己的模板文件并控制传递给它们的参数,那么你如何操纵或者添加传递给页面和节点模板文件的变量呢?
 
注意 只有实现为模板文件的可主题化项目,才有变量的聚合和传递一说。如果可主题化项目采用主题函数的实现方式,那么就不需要向其传递变量了。
 
每次加载一个模板文件都需要调用一系列的预处理函数。这些函数负责聚集变量,以将其传递给合适的模板文件。 让我们继续使用面包屑作为我们的例子。首先,让我们修改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培训班 http://zhupou.cn

适用于所有模板的变量

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培训班 http://zhupou.cn

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培训班 http://zhupou.cn

node.tpl.php

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

节点模板负责控制一个页面内部的单独的内容片段的显示。节点模板影响的不是整个页面,而仅仅是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变量中。

node.tpl.php(1)

block.tpl.php

comment.tpl.php

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

模板文件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片段。
 

box.tpl.php

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

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

其它的.tpl.php文件

多页面模板

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

当你想为站点上的不同页面创建不同的布局时,而一个单独的页面布局不能再满足所有需要了,你该怎么办呢?下面有一些好的实践,用来创建其它的页面模板。
  你可以基于站点的当前系统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培训班 http://zhupou.cn

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

注册表是如何构建的

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

当启用一个新的主题时,就会重新构建主题注册表,此时它按照下面的顺序来查找主题钩子:
 
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的值,在显示期间被指定。

逐步分析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培训班 http://zhupou.cn

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

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

首先,传递给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。

定义新的区块区域

总结

第9章 Drupal区块

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

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

什么是区块?

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

区块配置选项

区块位置

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

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

定义区块

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

可 以在模块中使用钩子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培训班 http://zhupou.cn

在用代码创建区块时,我们在区块钩子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包含了从区块配置表单提交过来的表单数据。
 

创建一个区块(1)

创建一个区块(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培训班 http://zhupou.cn

创建一个区块(3)

创建一个区块(4)

创建一个区块(5)

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

这里,我们通过对数据库进行查询来获得待定的评论,将评论的标题显示为链接,同时为每一个评论追加一个编辑链接,如图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。在这里也可以使用“待定”和“未发布”作为键。根据程序员的判断来决定使用哪种键,而键不一定是数字形式,也可以是字符串。

创建一个区块(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培训班 http://zhupou.cn

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

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

有时,你想在安装模块时, 将一个区块自动显示出来。这非常直接,通过查询语句直接将区块的设置信息插入到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培训班 http://zhupou.cn  

区块可见性例子

总结

第10章 表单API

理解表单处理流程

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

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

流程初始化

流程初始化
在处理表单时,有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培训班 http://zhupou.cn

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

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

接 着,调用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培训班 http://zhupou.cn

通过将表单的#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。

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

允许模块在表单构建以前修改表单
    在构建表单以前,模块有两个可以修改表单的机会。模块可以实现一个名字源于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培训班 http://zhupou.cn

检查表单是否已被提交

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

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

呈现表单

验证表单

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

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

提交表单

重定向

创建基本的表单(1)

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

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

创建基本的表单(2)

创建基本的表单(3)

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

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

表单属性

有些属性是通用的,而有些则特定于一个元素,比如一个按钮。对于属性的完整列表,可参看本章的最后部分。下面这个表单是比前面的例子中所给的表单要复杂一点:
 
$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培训班 http://zhupou.cn

表单IDs

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

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,但是它的描 述性不好----而且以后,我们可能还想再添加一个表单到我们的模块上。

字段集(1)

字段集(2)

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

我们使用可选的属性#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需要知道我们的模块将实现哪些主题函数。这可以通过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培训班 http://zhupou.cn

告诉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培训班 http://zhupou.cn

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

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

有时,你会遇到一种特殊的情况,你想让许多不同的表单共用一个验证或者提交函数。这叫做代码复用,在该种情况下,这是一个不错的想法。例如,在节点模块中,所有节点类型都共用该模块的验证和提交函数。那么我们就需要一种方式,将多个表单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查找位置的总结,这里按先后顺序排列,对于一个主题函数,假 定你使用基于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培训班 http://zhupou.cn

编写一个验证函数

从验证函数中传递数据

使用$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培训班 http://zhupou.cn

针对表单元素的验证

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

一般情况下,一个表单使用一个验证函数。但是也可以为单个表单元素设置一个验证函数,这和整个表单的验证函数一样。为了实现这一点,我们需要将元素的属性#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培训班 http://zhupou.cn

提交函数是表单通过验证后负责实际的表单处理的函数。只有在表单验证完全通过,并且表单没有被标记为重新构建时,它才会执行。提交函数通常需要修改$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'] 标记也可以设置在提交函数中,就像验证函数中一样。如果设置了,那么所有的提交函数都将运行,但是所有的重定向值都将被忽略,而表单将使用提交了的值进行重构。在向一个表单中添加可选字段时,这一点非常有用。
 

使用hook_form_alter()修改表单

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

修改一个特定的表单

总结

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

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

当表单构建器使用表单定义构建表单时,它需要保证每一个表单元素都要有一些默认设置。这些默认值在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')。

表单API属性

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

当在你的表单构建函数中构建一个表单定义时,数组中的键用来声明表单的信息。在下面部分中列出了最常用的键。表单构建器可以自动添加一些键。
 
表单根部的属性
下 面所列的属性是特定于表单根部的。换句话说,你可以设置$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表单元素。
 
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培训班 http://zhupou.cn

Password(密码)

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

该元素创建一个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属性。

Password with Confirmation(带确认的密码)

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培训班 http://zhupou.cn

Select(下拉选择框)

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

一个来自于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.

Radio Buttons(单选按钮)

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

来自于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)。
 

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培训班 http://zhupou.cn

Value(值)

Hidden(隐藏域)

Date(日期)

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

日期元素,如图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中)。当你在表单中定义日期元素时,通过定义这些属性,就使用你自己的代码来替代默认的了。

Weight(重量)

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

重量元素(不要与属性#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')。
 

File Upload(文件上传)

Fieldset(字段集)

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

字段集元素是用来对其它表单元素进行归类分组的。可将其声明为可伸缩的,这样当用户查看表单并点击字段集标题时,由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。
 

Submit(提交按钮)

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

提交按钮元素是用来提交表单的。按钮内部显示的单词默认为“提交”,但是可以使用属性#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()来替代表单的定义了的提交处理器。

Button(按钮)

Image Button(图片按钮)

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

图片按钮元素与提交按钮元素基本相同,但有两点例外。首先,它有一个#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'],就可以安全的取回图片按钮的值了。
 

Markup(标识文本)

Item(项目)

#ahah属性(1)

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

#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">。
 

#ahah属性(2)

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

下面的键是可选的:
• 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 请求的结果附加在页面中的多个元素上(而不仅仅是表单元素)。

#ahah属性(3)

#ahah属性(4)

跨页面表单(1)

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

我 们已经学习了简单的单页面表单。但是你可能需要,让用户跨越多个页面来填充一个表单,或者使用几个不同的步骤来输入数据。让我们创建一个简短的模块,用来 说明跨页面表单技术,在该模块中通过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
 

跨页面表单(2)

跨页面表单(3)

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

在这个简单模块中,我们需要注意几点。在我们的表单构建函数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 多步表单的第二步
   

跨页面表单(4)

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

我们有证据证明验证函数的运行,因为它通过调用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培训班 http://zhupou.cn

本部分解释的属性适用于所有的表单元素。
#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']中。
 

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

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

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

过滤器

过滤器和输入格式

安装过滤器

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

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

知道什么时候使用过滤器

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

如果可以使用已有的钩子来处理文本,那么你可能会想,为什么此时还需要使用过滤器呢?例如,使用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培训班 http://zhupou.cn

当然,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.
 */

实现hook_filter()

实现hook_filter()(1)

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

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(你的模块要过滤的文本)。
 

助手函数

hook_filter_tips()

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

你使用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.
 

阻止恶意数据

总结

第12章 搜索和索引内容

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

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

构建一个自定义搜索页面

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

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培训班 http://zhupou.cn

高级搜索特性,如图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())。

扩展搜索表单

介绍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培训班 http://zhupou.cn

使用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培训班 http://zhupou.cn

使得路径别名能被搜索

让我们开始我们的例子。我们将实现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培训班 http://zhupou.cn

使用搜索HTML索引器

什么时候使用索引器

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

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

索引器的工作原理

索 引器有一个预处理模式,在该模式下将使用一组规则对文本进行过滤从而分配分数。这些规则包括:处理缩略语、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培训班 http://zhupou.cn

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

对非节点的内容进行索引: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培训班 http://zhupou.cn

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

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

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

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

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

在这个模块中,我们需要实现的最后一个函数是钩子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.搜索一个外部的遗留数据库
 

总结

第13章 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服务器来讲应该是全局的。
 

私有文件

PHP设置

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

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的进程,从而产生潜在的可升级性问题。

多媒体处理

其它的通用文件处理模块

文件API

常用任务和函数

复制和移动文件

下面的函数可以帮你处理文件系统中已有的文件。
 
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培训班 http://zhupou.cn

检查目录、路径、位置

当你使用文件时,你常常需要停下来判定一下一切是否就绪。例如,一个目录可能并不存在或者不可写。下面的函数将帮你解决这类问题。
 
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培训班 http://zhupou.cn

上传文件

尽管上传模块提供了一个完整的实现,用来为节点上传文件;但是有时候,你不想把上传的文件与节点关联起来。下面的函数可帮你实现这一点。
 
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培训班 http://zhupou.cn

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

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

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()的验证器。

为一个文件获取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培训班 Http://zhupou.cn 

在一个目录下查找文件

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培训班 http://zhupou.cn

查找临时目录

下面的函数用于报告临时目录的位置,通常称为“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培训班 http://zhupou.cn

中和危险文件

假定你使用的是公共文件下载方法,并且你启用了文件上传。那么当有人上传一个名为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培训班 http://zhupou.n

检查磁盘空间

用于下载的认证钩子

模块开发者可以通过实现 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培训班 http://zhupou.cn

总结

中英文对照

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培训班 http://zhupou.cn

19. XML-RPC

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

1.什么是XML-RPC

                    译者:老葛, http://zhupou.cn

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

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

2.XML-RPC的前置条件

译者:老葛, http://zhupou.cn

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

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

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

XML-RPC 客户端

客户端是用来发送请求的的计算机。它向服务器端发送一个标准的HTTP POST请求。这一请求的主体是由XML组成的,并且包含一个简单的名为<methodCall>的标签。 在<methodCall>标签内部,嵌套了两个子标签,<methodName>和<params>。让我们通过 一个实例来看一下它是如何工作的。
 
注意:远程过程被调用时是作为一个方法被引用的。这就是为什么XML编码将远程过程的名字包装在<methodName>标签里的原因。
 

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

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

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

 
$time = xmlrpc('http://time.xmlrpc.com/RPC2',currentTime.getCurrentTime');
 
    在这里你调用了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培训班 http://zhupou.cn

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培训班  http://zhupou.cn

处理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培训班  http://zhupou.cn

HTTP错误

调用语法错误

参数类型转换

一个简单的XML-RPC服务器

使用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)使用以下代码来来发送你的名字:
 
$url = 'http://example.com/xmlrpc.php';
$method_name = 'remoteHello.hello';
$name = t('Joe');
$result = xmlrpc($url, $method_name, $name);
 
$result现在是 "Hello, Joe."
 老葛的Drupal培训班 http://zhupou.cn

在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培训班 http://zhupou.cn

内置的XML-RPC方法

system.methodSignature

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

system.methodSignature

这个内置的Drupal XML-RPC方法返回一个数据类型的数组.列表中的第一个是函数返回值的数据类型;接着是给定方法所需的任意参数.例如, remoteHello.hello方法返回了一个字符串,并期望一个参数:一个包含了客户端名称的字符串.让我们调用 system.methodSignature来看看Drupal是不是这样的.
 
// Get the method signature for our example method.
$url = 'http://example.com/xmlrpc.php';
$signature = xmlrpc($url, 'system.methodSignature', 'remoteHello.hello');
 
果然, $signature的值变成了一个数组:('string', 'string').
 
system.methodHelp
这个内置的Drupal XML-RPC方法,返回xmlrpc钩子中定义的方法的描述.
 
// Get the help string for our example method.
$url = 'http://example.com/xmlrpc.php';
$help = xmlrpc($url, 'system.methodHelp', 'remoteHello.hello');
 
$help的值现在是一个字符串: Greets XML-RPC clients by name.
 
system.getCapabilities
这个内置的Drupal XML-RPC方法描述了Drupal的XML-RPC服务器能力,这里根据实现的规格进行描述. Drupal实现了以下规格:
 
xmlrpc:
specURL http://www.xmlrpc.com/spec
specVersion 1
 
faults_interop:
specURL http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
specVersion 20010516
 
system.multicall
specURL http://www.xmlrpc.com/discuss/msgReader$1208
specVerson 1
 
introspection
specURL http://scripts.incutio.com/xmlrpc/introspection.html
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站点上发送XML-RPC请求到一个不同的服务器上
    能够实现一个基本的XML-RPC服务器
    理解Drupal是如何将XML-RPC方法映射到php函数上的
    能够实现简单的和复杂的xmlrpc钩子
    了解Drupal内置的XML-RPC方法
老葛的Drupal培训班 http://zhupou.cn
 
posted on 2010-08-13 09:30 seal 阅读(3883) 评论(0)  编辑  收藏 所属分类: PHP

只有注册用户登录后才能发表评论。


网站导航: