Drupal6 专业开发指南中文版

这是一本Drupal开发人员的圣经。

第一章 Drupal 是如何工作的

本章将主要对Drupal的概貌进行介绍。关于系统是如何工作的那些细节,将留待后面的章节再讲解。此处,我们首先会介绍Drupal运行在什么样的技术堆栈(technology stack)上,还有Drupal的文件系统结构,最后是Drupal所使用的各种概念型术语,比如节点、钩子、区块和主题等。

第一节 什么是Drupal?

Drupal是用来建立网站的工具。它是一个高度模块化,开源的Web内容管理框架,同时它还专注于协作式的开发。它具有良好的扩展性,而且兼 容标准,同时还致力于清洁的代码和脚步递进式开发。Drupal内嵌了基础的核心功能,同时还能使用第三方开发的附加功能。可以说Drupal生来就是为 定制做准备的,但定制开发是由覆盖核心功能或是通过添加新模块来完成的,而非由修改核心模块代码来实现。Drupal的设计也成功的将内容管理与内容展现 分离开来。
 
Drupal可以被用来创建一个互联网上的门户网站;一个个人的,部门的,或是公司的网站;一个电子商务网站;一个资源目录;一个在线报纸;一个图片库;一个内网应用。这里只不过提了很少的几种可能性,Drupal甚至可以被用来教授远程在线课程(类似于LMS)。
 
有一支全职的安全团队正在努力通过应对各种安全问题和发布安全补丁包来使得Drupal变得更加安全。一个非盈利性的组织Drupal Association通过改进Drupal.org的网络设施来支持Drupal,同时它还组织了许多Drupal大会和聚会。并且,一个日益繁荣的 Drupal社区,一群网管们,网页设计者还有开发者们都在不停的贡献着自己的力量。参见http://drupal.orghttp://groups.drupal.org

 

第二节 技术堆栈

 
Drupal的设计目标包括了两点,在便宜的互联网主机租用提供商的主机上运行良好,以及,能够分布式的运行在大规模分布式网站上。前者意味着使用最流行和最被广泛接受的技术,后者,则意味着谨慎的,优秀的编码。Drupal的技术堆栈如图 1-1 所示
 
在这个技术堆栈中,操作系统的位置很低,也就是说Drupal并不是很关心你使用了什么操作系统,只要它能支持PHP就行。
 
Drupal最为广泛使用的Web服务器是Apache,但是其它服务器Drupal也支持,包括微软的IIS。由于DrupalApache之间悠长的历史,Drupal自带了.htaccess文件来为Drupal的安装提供安全保证。Clean URL功能也就是那些不带有问号、&号或其它奇怪字符的URL – 是由Apachemod_rewrite模块来提供的。这点尤其重要,因为当从其它CMS系统或是静态文件移植内容时,内容的URL地址不应该被改变,按照Tim Berners Lee(http://www.w3.org/Provider/Style/URI)的说法,不改变URI的做法是非常酷的。Clean URLs这种功能在其它Web服务器上也是允许存在的,只要使用了对应Web服务器的URL重写功能即可。
 
Drupal堆栈中的下一层,也就是数据库层,Drupal提供了轻量级的数据库抽象层。该层提供了一种公用SQL查询方式,可实现在无需重构任何业务代码的情况下使用不同厂商的数据库。这一点已经在MySQLPostgreSQL上被广泛测试,但是对于Microsoft SQL ServerOracle的支持还正在开发中。
 
Drupal是用PHP写的。因为PHP是一门很容易上手的语言,所以有很多PHP的程序,都是由新手写的。这些新手开发的程序中的代码质量,带给了PHP很不好的名声。然而事实是,PHP也是可以写出高质量代码的。所有的Drupal内核代码都遵守了严格的编程规范(http://drupal.org/nodes/318) 并且接受了开源软件流程非常彻底的审查。对Drupal来说,PHP的低学习曲线意味着为软件贡献者设置了很低的进入门槛,但审查流程确保了门槛低的同时不会牺牲最终产品的质量。同时从Drupal社区中收到的反馈又能帮助初学者们提高来改进他们的技巧。

第三节 Core 核心

Drupal的核心代码包括了处理用户请求时的完整流程,Drupal常用功能函数库,还有提供Drupal基本功能的模块代码,比如用户管理,分类(taxonomy),还有显示模板,参考下图所示:
 

第四节 管理界面

Drupal的管理界面是与它的整个网站坚密结合的,默认情况下,它们共同使用同一种主题。第一个用户,也就是user 1,是网站的超级用户,拥有至高无上的权限。使用user 1登录后,在你的User区块中(相关内容请参看“区块”章节),将会看到一个进入管理界面的链接。点击后,你就进入了Drupal的管理界面中。每个用户的User区块都会包含不同的链接,这是由用户自身的权限级别所决定的。

第五节 模块

Drupal是一个真正意义上的模块化的框架。它所有的功能,都包括在模块中,并且可以被启动或禁用(有些核心必须模块是无法被禁用的)。为一个网站添加功能,是可以通过启用新的模块来实现的,通常这些模块都已由Drupal开源社区的广大成员提供好了。当然,你也可以开发自己的模块。如此一来,在网站本身不需要某些功能时,就可以将它们禁用,使网站“瘦身”,或是在需要额外的功能时,可以按需任意添加。参见下图:
不管是添加新的诸如菜谱,博客,或文件等内容类型(Content types),还是添加如邮件提醒,点对点信息发布,RSS聚合等功能,它们都是由添加模块来实现的。Drupal使用了“控制反转”的设计模式,也就是让框架本身在适当时机去调用模块提供的功能。这种调用机制,在Drupal中,被称为钩子(Hook)

第六节 钩子

钩子可以被看作是Drupal的内部事件,它们也被叫做“回调函数”。这里需要注意的是,由于它们是按照函数命名规范来构建的,而非真正的注册了一个监听器listener,所以它们不算真正意义上的“回调”。
 
设想,一个用户登录了你的Drupal网站。在用户登录的时候,Drupal会调用hook_user这个钩子。这也就意味着,所有函数,只要是按照了“模块名称_钩子名称”这样的命名规范进行命名的,都会被调用。例如,comment_user()这个函数位于comment模块中,locale_user()这个函数则位于locale模块中,node_user()处于node模块中,它们会统统的被调用。事实上所有实现了这个钩子的模块都会被调用。如果你想写一个自己定制的模块,比如叫spammy.module,并且包括了一个函数叫spammy_user(),这个函数的功能是给用户发一封email。这个spammy_user()方法同样会在用户登录时被调用,并且那个不幸的用户在每次登录时都会收到一封邮件。
 
Drupal中,要想修改或覆盖Drupal的核心功能,最常见的方式就是在模块中实现相应的钩子。
 
Tip: 想了解更多关于Hooks的内容,请参见在线文档:http://api.drupal.org/api/6 。在Components of Drupal下,点击“Module system(Drupal hooks)”。

 

第七节 主题

当创建一个网页发往浏览器时,通常有两件事需要考虑:如何组装数据和如何为数据添上WEB标签以便在浏览器中显示。在Drupal中,主题层Theme layer负责生成发往浏览器的HTML(或是JSON,XML)Drupal能使用多种流行的模板技术,比如SmartyTemplate Attribute Language for PHP(PHPTAL),还有PHPTemplate
 
很重要的一点是,Drupal鼓励内容与表示层的分离。
 
Drupal允许多种方法来定制和覆盖网站的外观。最简单的方法是使用CSS来覆盖Drupal内置的classesIDs。然而,如果你希望更进一步,完全定制真正的HTML输出内容,你会发现这其实也很简单。Drupal的模板方件是由标准HTMLPHP文件组成。页面上每一动态的部分(比如一个区块,或是一个导航条breakcrumb trail)都可以通过简单的声明一个函数来对其进行覆盖,当然前提是该函数必须符合一定的命令规范。只要这样,Drupal就会使用你的方法来创建页面内容。

第八节 节点

Drupal中的各种内容类型是由一个基础的类型继承而来的,这个基础类型,就叫节点(node)。不管它是一个博客文章,还是一个菜谱,甚至于是一个项目中的任务,其所使用的底层数据结构都完全一样。如此架构的天才之处,在于它良好的扩展性。模块开发者可以添加新的功能,比如评分,评论,文件附件,地理信息等等,而且无须考虑节点类型是博客,菜谱还是别的什么。网站管理员然后将它们混合,并按照内容类型来映射各项功能。例如,管理员可能选择起用对博客的评论功能,但不起用对菜谱的评论功能,又或者起用对项目任务的文件上传功能。
 
节点还包含一组基本的行为属性,这些属性会被所有的内容类型所继承。任意一个节点,都能被显示于网站首页,被发布或是取消发布,甚至是被搜索。正是由于这样统一的架构,管理界面才能提供对于节点的批量编辑功能。

 

第九节 区块block

区块是网页中一个小区域,它的内容可以由管理员起用或是禁用。例如,一个区块可以显示你的网站中当前在线用户的总数。又或许,你会有一个区块,它包含了关于你的网站中最为流行的内容的链接,或者是网站将要进行的活动。区块经常被放在模板的侧边栏,顶栏或是页脚中。区块可以被设置为显示某一类型的几个节点,还可以设置只在首页显示该区块,或者是按其它条件进行显示。
 
通常,区块用来显示为当前用户所特定的信息。例如,User这个区块通常包含了当前用户所能够访问的管理页面链接,比如“My account”页面。区块能显示的区域是在网站的主题中预先定义的。区块的位置和可见性都可以在后台管理界面中进行管理。

 

第十节 文件结构

理解Drupal的默认文件系统结构可以教给你很多重要的技巧。比如,在哪里放置下载的新的模块和主题,或是如何安装不同配置的Drupal。默认的Drupal安装有下面的文件结构:
以下是对上图目录结构中所包含的各个元素的详细说明:
 
Includes目录中包含了Drupal所使用的一些通用函数库。
 
Misc目录中存储了Javascript脚本和各式图标、图片,在Drupal的默认安装中会用到它们。
 
Modules目录中包括了Drupal的核心模块,都分别在各自的目录内。比较好的做法是永远不去修改这个目录中的任何东西(或者说,除了Profilessites目录之外所有的目录,都不应该被修改)。用户添加的新模块,应该被放置于sites目录中。
 
Profiles目录存放着某个网站的不同的Drupal安装描述文件。如果除了默认安装外,这个目录中还有别的安装描述文件,Drupal会请示用户希望使用哪一个安装描述文件来安装Drupal。一个安装描述文件的主要作用是来自动的启用某些核心和第三方模块。比如说,如果使用了某个e-commerce安装描述文件,就可以自动的装网站设置成为一个电子商务平台。
 
Scripts目录中包含了一些脚本用来检查语法,清理代码,从命令行运行Drupal或是一些使用CRON的特殊的例子。这个目录在Drupal处理用户HTTP请求周期中并不会被用到;它们都是shellPerl的脚本。
 
Sites目录中存放着你对于Drupal的所有修改和定制,这包括系统设置、模块和主题。当你向Drupal中添加第三方模块或是你自己开发的模块时,只需将模块放到sites/all/modules目录中。这样就使得你对于当前网站的所有定制集中在一个目录下。在sites目录下是一个名为default的子目录,其中存放了默认的系统配置文件default.settings.phpDrupal的安装程序不会改动这些原始默认设置,而是会根据你所提供的配置信息,重新创建一个名为settings.php的文件,并将其置放于该目录下。通常来说,网站的创建者,会创建一个以网站URL为名的目录,这样一来,你最终的配置文件很有可能就会是sites/www.example.com/settings.php
 
默认情况下Drupal并不会创建Sites/default/files这个目录,但的确需要它来存储所有上传到你的网站上的文件。比如用户可能会上传一个定制的logo,或是个人头像或是一些和网站相关的视频。Files这个目录,需要Web服务器必须有读和写的权限才能正常使用。Drupal安装程序会自动检查该目录的读写权限,然后创建它。
 
Theme目录包含了模板引擎和一些默认的主题。用户所下载或自己创建的主题不应该被放在这,它们应该放在sites/all/themes
 
Cron.php是用来执行定时任务的,比如定时维护,备份数据库或是计算一些统计信息等。
 
Index.phpDrupal处理用户请求时的主要入口。
 
Install.phpDrupal安装程序的主要入口。
 
Update.php会在Drupal版本更新后,更新相应的数据库表。
 
Xmlrpc.php接收XML-RPC请求,如果不想使用XML-RPC功能,那就完全可以将其删除。
 
Robots.txt里面包含了一些默认的设置,用来保证搜索引擎在爬取网站时,可以避开一些关键内容。
 
还有一些没有列在这里的文件,都是属于文档性质:
 

第十一节 接受一个Web请求

从概念上了解一下Drupal在收到一个Web请求后都做了哪些事,是对学习Drupal非常有帮助的。这一小节会带领读者快速了解一下这一过程。如果你希望自己来跟踪调试这整个过程,那么请使用一个好的调试工具,并且从Index.php开始,因为Drupal的大部分请求都是被这个文件所接收的。本小节中所讲述的生成一个简单页面的过程,看起来似乎有些复杂,但这个过程中的每一步,都是相当灵活的。
 
Web服务器的角色
Drupal跑在一个web服务器之上,比如Apache。如果Web服务器尊循Drupal.htaccess文件,那么某些PHP设置就会被初始化而且URL会被检查。几乎所有对Drupal的访问,都要通过index.php文件。比如,当你调用http://example.com/foo/bar时,会经历以下步骤:
1.       定于在Drupal.htaccess文件中的Mod_rewrite规则,会将请求的URL进行切分,把基础路径取出,在我们的例子中,也就是foo/bar
2.       这个路径会被赋值给查询变量q
3.       于是,最终的URL变为http://example.com/index.php?q=foo/bar
4.       Drupalfoo/bar当成内部Drupal路径,并开始运行index.php进行处理。
 
由于这样的处理流程,所以Drupal认为,http://example.com/index.php?q=foo/barhttp://example.com/foo/bar实际上是同样的链接。因为从内部看来,两者的路径实际上是一样的。这也就使得Drupal处理的URL无须包含那些怪怪的字符。通常我们把这样的URL称为clean URLs
 
如果使用其它web服务器,比如IIS,可以通过使用Windows Internet Server Application Programming Interface(ISAPI)的模块,比如ISAPI Rewrite来使用clean URL的功能。IIS 7 以后的版本直接就能支持Clean URL
 
Drupal请求初始化过程
Drupal的请求初始化处理过程包括一系列的步骤。这些步骤定义在bootstrap.inc文件中,其内容如下如述:
(1)     初始化配置
这一步骤中,Drupal配置其各种内部变量,并建立起网站的base_urlDrupal会使用include_once方法来装载Settings.php文件,同时还装载所有的用来覆盖默认配置的的字符串和变量。关于这一部分内容,请参见sites/all/default/default.settings.php文件中“String Overrides”和“Variable Overrides”那两节。
(2)     初步页面缓存(Early Page Cache)
在某些需要适应高并发的场合,需要启用缓存系统来防止对数据库的过度访问。初步页面缓存这一步骤把一个PHP文件通过include方法引入,在该PHP文件中有一个方法名为page_cache_fastpath(),它会取得控制权并把内容发送回浏览器。这种初步页面缓存的启用需要将page_cache_fastpath这个变量值设置为TRUE,还需要把cache_inc这个变量值设置到正确的文件路径上,才能够把前面所说的需要引入的PHP文件装载成功。可以查看后面关于缓存的那一章节以了解更多。
(3)     初始化数据库
在数据库这一步骤中,首先会决定数据库的类型,并会创建一个初始的连接用于后面的数据库查询。
(4)     基于域名/IP的访问控制
Drupal是可以限制某个IP/域名的访问的,在访问控制这一步骤中,Drupal会检查是否当前请求来自于一个被限制访问的地址或域名。如果是的话,这一请求就会被拒绝。
(5)     初始化Session
Drupal使用了PHP内置的session处理机制,但是它重写了某写session的处理函数,同时使用了它自己的基于数据库的session管理。这一步骤中会为请求初始化或重建session。同时,代表当前用户的全局的$user变量也在这一步骤中被初始化,只不过基于效率的考虑,并不是所有的属性都被赋值(它们会在后面显示调用user_load()时被赋值)
(6)     后期页面缓存(Late Page Cache)
在后期页面缓存步骤中,Drupal会加载足够的代码来决定是否从缓存取得某个页面。这部分代码包括了把数据库中的设置与在初始化配置时创建的数组进行合并,并会检查加载模块代码。如果Session显示当前的页面请求是由一个匿名用户发出的,那么页面缓存就会被调用,用户浏览器会收到从缓存发出的页面,然后运行结束。
(7)     决定语言
在决定语言这一环节,Drupal的多语言系统被初始化,接下来它会决定用什么语言来返回当前页面,这是由当前用户设置以及站点设置所决定。Drupal支持使用多种方式来决定当前的语言,比如使用路径前缀方式,或是域名级别的语言选择。
(8)     路径
在路径这一步骤中,用来处理路径和路径别名的代码被加载。这一步骤使得用户可读式的URL能够被解析,并管理着Drupal内部的路径缓存和查找。
(9)     完整启动
这一步骤实际上完成了整个启动过程,在这一步骤中,通用函数库会被加载,比如,系统的主题,回调函数映射,文件处理,UnicodePHP图片工具,表单的创建和处理,邮件处理,自动排序表格还有结果集分页等功能。Drupal定制的错误处理工具被启用,并且所有启动的模块会被加载。最后,Drupal会调用各个模块的_init钩子,以使得模块有机会在正式处理业务逻辑前进行相应的初始化工作。
 
一旦Drupal完成了整个初始化过程,它的框架的各部分就都进入运行状态。接下来就可以接收真正的浏览器请求,并将请求发到相应的PHP方法。URLPHP方法间的映射是通过回调函数注册来完成的,在映射的同时,还考虑了访问权限的问题。各个模块都通过_menu这个钩子来注册各自的回调函数(关于menu钩子,请看第四章以获取更多内容)。
 
Drupal发现在当前请求URL和某个回调函数间存在着映射时,并且当前用户的权限也可以访问该回调函数,那么Drupal就会将控制权交给该函数,以进行相应的处理。
 
处理一个请求
回调函数负责完成自己的工作,然后会生成相应的数据以返回给客户端。例如,一个请求http://example.com/?q=node/3Drupal接收到,这个URL会映射到函数node_page_view(),它位于node.module中。接下来,Drupal会从数据库中获取该节点的数据,然后放入一个数据结构中,再然后,用页面模板对其进行包装。
 
用页面模板包装数据
使用页面模板(也就是主题)对数据进行包装,将其转换成HTML(或是XML以及其它格式)Drupal会使用管理员已经选定的主题,来给网站一个正确的外观。然后将输入的HTML数据返回给网页浏览器。

 

Your rating: None Average: 2.7 (15 votes)

总结

阅读本章后,关于Drupal是如何工作的,读者应该已经有一个基本的概念,同时读者也应该对Drupal是如何处理一个Web请求也有了大致的了解。构成处理流程的各部分模块,将会在后面的章节进行详细描述。

第二章 开发一个模块

在许多开源软件中,你可以通过修改核心代码的方式来定制软件。这确实是得到你想要的功能的方法之一,但一般来说Drupal社区中都不太赞成这么做,而只是把它作为一种万不得已的选择。因为定制了代码意味着每次Drupal升级,你就要多做很多额外的工作——你必须再次调试以确定你定制的内容是否还能继续工作。然而,Drupal从整体上都是高度模块化和可扩展的。

如果只是对Drupal进行了默认安装,那么作为建立网站的框架功能上还是很贫乏的。在Drupal中,将各种功能加入到核心中去是通过启用“模块”(一种包含php代码的文件)来实现的。核心模块就在你安装Drupal的子目录里面。你可以看看他们,并将他们和你的Drupal网站中Adhminister/Site building/Modules页面上的模块列表比较一下。

在这一章的学习中,我们将从零开始建立一个模块。在学习的过程中,你将了解建立模块所必须遵循的准则。由于需要一个实际的目标,让我们将注意力集中到“注释”这个实际问题上吧。当浏览一个Drupal网站的页面时,如果管理员启用了“评论模块”,用户就可以对页面内容进行评论。那么给一个网页加入注释(一种只有用户自己能看见的记录)怎么样呢?也许用户秘密回顾网页内容的时候会用到吧?(我知道这听起来很牵强,但各位读者就容忍我一下吧。

第一节 建立文件

我们要做的第一件事就是为我们的模块起个名字。“annotate”(注释)就很不错——简洁而且具有一定的描述性。然后,我们要找个地方放置它。可以选择和核心模块一起放在modules目录下,但这样维护就会变得很困难,因为必须记住哪些是核心模块哪些是我们自己的。所以,让我们把它和核心模块分开放置吧,就在sites/all/modules目录下。
 
建立sites/all/modules/custom目录。再在其中建立annoatae子目录,这样做是为了将你开发的模块(custom modules)和你所下载的第三方模块区分开来。是否采用这样的结构其实取决于你,不过它将使接手你的网站的其他开发者感到更加适应。而建立一个文件夹annotate而不是只建立一个文件annotate.module的原因在于我们的模块在发布的时候将包含一些其他的文件。例如:需要一个READEME.txt文件来向其他用户解释我们的模块用来做什么以及如何使用它,而一个annotate.info文件可以向Drupal主体提供一些相关信息。
 
那么,准备好开始了么?
我们的annotate.info文件如下所示:
 
 
: $id$
name = Annotate
description = Allows users to annotate nodes.
Core = 6.x
Package = Pro Drupal Developments
 
文件以简单的格式定义了些键和值。我们从CVS系统(concurrent versions system)的ID标签($id$)开始。如果我们想将这个模块加入到Drupal社区的模块库中来和其他人分享它,这个值将被CVS系统自动替换。然后我们建立了Drupal用于在模块管理页面中显示的名字和描述信息。再之后是详细的定义了我们的模块主要兼容的Drupal的版本,此时,版本是6.xDrupal 6 或者更高版本将不允许启用不兼容的模块。模块的显示是分组的,组别将决定于package的值,因此,假如我们有三个不同的模块都定义了Package = Pro Drupal Developments 那么它们将显示在同一组中。我们还可以为这个列表增加一些可选的值。下面是一个需要PHP5.2环境以及必须开启forumtaxonomy模块的例子:
 
: $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
 
 
Note:你可能不明白为什么我们需要一个独立的.info文件。为什么不在主模块文件中定义一个函数来返回这些数据呢?因为当调用模块管理页面,必须读取和分析每一个模块是否开启,这将导致内存的使用量远大于平时,甚至会超出PHP所指配的内存量的界限。而通过使用.info文件,信息将消耗最小的内存就进行快速的读取。
 
 
现在,我们已经准备好创建模块的主体了。在sites/all/modules/custom/annotate子目录下建立名为annotate.module的文件吧。在文件的开头写上PHP开头标签和CVSID标签,之后的内容如下:
 
<?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的结束标签(?>):这在PHP中是可选的行为,但是如果加上它,将可能由于文件末尾处出现由空白区域导致一些问题。(详见http://drupal.org/node/545
 
Note:为什么我们对于格式如此挑剔呢?因为当全世界数以百计的人们共同开发一个项目的时候,所有人遵循同一准则工作能节省许多时间。而Drupal编程所要求的风格细节可以在Drupal开发手册中“编码准则(coding standards)”一节中找到。(详见http://drupal.org/node/318).
 
我们下一道工序是定义一些设定,以便形成一个网页表单——让我们选择到底要注释哪些节点类型。有两步工作需要完成:首先,定义一个可以用来访问我们的设定的路径;然后,建立设定表单。

 

第二节 实现一个钩子

让我们再回忆一下:Drupal的建立是基于一个钩子系统的,有时我们也称之为回调系统。在Drupal运行的过程中,会询问模块是否有什么要做的。例如,当需要确定哪个模块该对当前请求做出回应的时候,所有模块都会被要求将自己所处理的路径提供出来。这个行为是如何实现的呢?Drupal会将所有模块进行列表,然后调用这些模块中以模块名加上“_menu”命名的函数。当进行到annotate模块(将会在早些时候按照字母表的顺序被默认定义在列表中)的时候,就会调用annotate_menu()函数——其返回值是一个由菜单项组成的数组。每一项(我们此时只有一项内容)都以路径为键,在这个例子中,路径是admin/settings/annotate。而我们的菜单项的内容是由键和值组成,并用来描述当这个路径被访问Drupal应该做些什么事情。如果想详细了解这些,请阅读第四章,在那里我们将会讨论Drupal的menu/callback系统。
 
下面是将要加入到我们的模块中的内容:
 
 /**
 *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中搜寻描述这个表单的函数。而且,只有在administer site configuration中获得权限的用户才能访问这个菜单项。”当进行到显示这个表单的时候,Drupal会要求我们对此表单提供一个定义(如何定义稍后再说)。在Drupal查询完所有模块的菜单项时,就会为当前路径的生成一个菜单,用来选择恰当的函数以应对当前请求。
 
Note:如果你有兴趣了解一下hook函数的工作机制,去看看includes/module.inc文件中的module_incoke_all()函数吧。
 
现在你知道为什么叫它hook_menu()或者menu hook了吧?Drupal的钩子(hook)通常都是通过将钩子名附在你的模块名的后面建立起来的。
 
Tip:Drupal的钩子几乎使你可以修改这个软件的所有方面。关于hook及其功能的完整列表可以在DrupalAPI文件的网站上找到(http://api.drupal.org).

第三节 给模块加些具体的设定

Drupal有各种节点类型(在用户界面叫做内容类型),例如新闻和页面。为了达到只对其中一些做出注释的目的,我们需要建立一个页面用来告知模块究竟有哪些节点类型是我们想注释的。在此页面中,我们需要有一个对每一个存在的节点类型都生效的复选框(check boxes)。这样用户就可以通过是否选中某些选项来决定是否对相应的节点类型进行注释。这种页面叫做管理页面,其代码只在有必要的时候才会进行读取和分析。所以,我们不将它放在每次有页面请求都会被读取的annotate.module文件中,而是单独的放在一个名为annotate.admin.inc的文件里。
 
首先要做的是建立sites/all/modules/custom/annotate/annotate.admin.inc文件,然后把下面这段代码添加进去:
 
<?php
//$Id$
 
/**
 *@file
 *Administration page callbacks for 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中是以一种树状结构出现的,准确的说是多维数组。这种结构向durpal的表单生成引擎描述了表单究竟该如何呈现。为了可读性方面的考虑,我们将数组的每个元素独立的放在一行。每个表单的属性(实际上是数组的键)用一个井号(‘#’)标出。一开头,我们就将表单元素中type一项声明为“checkboxes”,这是说将会把一个带有键值的数组以复选框的形式展现出来。而这个数组就是我们在变量 $options中得到的。
 
就这样,node_get_types('names')输出结果从它本来的返回值——一个带有键值的数组,现在已经变成了方便可用的选项。
 
此时的输出结果大致如下:
 
 
数组的键值就是Drupal内在的节点类型,在它的右边,是用来展示给用户看的比较通俗的名字。假设你的Drupal有一个叫做Savory Recipe的节点,那么数组的输出可能就会变成下面这样:
 
 
也就是说,在网页表单中,Drupal将会生成一个选项是page和story的复选区域。
 
然后我们通过定义#title的值来给以上的表单元素加上一个恰当的标题。
 
提示:t()函数是Drupal用来使字符翻译变的更容易的函数,任何用来给用户阅读的文本都应该用它来定义和返回,比如我们表单中的#title和#description。这样通过让字符翻译函数去执行所有的文本,就可以使你的模块在其他语言环境下的本地化过程变得容易许多。而我们并没有对菜单项这样做的原因是——菜单项的翻译工作是自动进行的。
 
下一个指令#default_value,其值将会是这个表单元素的默认值。由于checkboxes是一个复数的表单元素(可以有不只一个checkbox存在于其中),所以#default_value的值也是一个数组,这个数组很值得讨论一下:
 
variable_get(‘annotate_node_types’,array(‘story’))
 
Drupal允许开发者们通过使用这样一对特殊的函数:variable_get() 和 variable_set()来实现任意变量的存取。数据存在variables数据表中,而且当程序需要的时候随时可以读取。因为这些变量是在每个请求进行的过程中都要从数据库中检索出来的,那么用这种方式来储存大量的数据显然不是一个好主意。但它用来存储诸如模块的配置设定之类的值显然是非常方便的系统。这里要注意的是,我们传给variable_get()函数的首先是一个对键值的描述,然后是这个键的默认值。在这个例子中,键的值就是允许添加注释的节点的类型数组,而其默认值我们设置为story这个节点类型。
 
小贴士:当使用system_settings_form()的时候,表单元素的名字(这里是annotate_node_types)必须和variable_get()中键的名字保持一致。
 
最后,我们用一个描述来为网站管理员提供了一些信息。
 
保存你刚刚建立的文件,然后访问Administer > Site building > Modules。你的模块应该在列表的末尾,其组别是Pro Drupal Development(如果它不在那,再次检查你的annotate.info和annotate.module文件,并确保他们在sites/all/modules/custom 目录下)。那么,启用你的新模块吧。
 
现在annotate模块已经启用了,从导航条进入到Administer >Settings > Annotate应该就能看见annotate.module的设置页面了。(如图2-1所示)
 
             图2-1 annotate.module为我们生成的配置表单
 
看,只用了几行代码,我们的模块就有了一个可以自动保存设定的函数配置表!OK,其中一行字有些太长了,但这完全不影响你感受Drupal给你带来的能力大增的美妙感觉。

 

第四节 添加数据登记表

为了让用户可以在网页上输入注释内容,我们需要为此提供一个区域。让我们再为annotate.module添加一个表单吧:
 
/**
* Implementation of hook_nodeapi().
*/
function annotate_nodeapi(&$node, $op, $teaser, $page) {
global $user;
switch ($op) {
// The 'view' operation means the node is about to be displayed.
case 'view':
// Abort if the user is an anonymous user (not logged in) or
// if the node is not being displayed on a page by itself
// (for example, it could be in a node listing or search result).
if ($user->uid == 0 || !$page) {
break;
}
// Find out which node types we should annotate.
$types_to_annotate = variable_get('annotate_node_types', array('page'));
// Abort if this node is not one of the types we should annotate.
if (!in_array($node->type, $types_to_annotate)) {
break;
}
// Add our form as a content item.
$node->content['annotation_form'] = array(
'#value' => drupal_get_form('annotate_entry_form', $node),
'#weight' => 10
);
break;
}
}
 
这些代码看其来十分复杂,所以让我们一起搞定它吧。首先要注意的是我们实现了另外一个Drupal的钩子——nodeapi钩子,它会在Drupal对节点做各种行为的时候被调用,这样其他模块(比如说我们的)就可以在程序进行下去之前对节点进行改动。变量$node给出了一个节点。而它前面的&符号表明这是对它本身的一次真实提取,这意味着我们在模块中对$node进行的任何改动都将被保存下来。因为我们打算呈现一个表单,所以能够修改节点是一件很值得高兴的事。
 
Drupal同样提供了一些信息来说明当我们的模块被调用的时候Drupal本身在进行什么活动。这些信息体现在变量$op(operation)的取值上,它们可以是insert(节点正在被创建),delete(节点正在被删除),或者其他一个或多个值。而现在的情况是,我们只对即将被显示的节点有兴趣,那么$op的值也就应该是view。代码结构上,我们使用switch语句,这样可以方便的指出我们的模块在各种情况下应该做些什么。
 
然后,我们迅速的检查一下在哪些情况下不应该把注释显示出来。其中一种情况就是当访问节点的用户没有登录(要注意的是,我们使用的关键字global将变量$user引入进来,这样就可以检验当前用户是否已登录了),另外一种应该避免显示注释的情况是变量$page的值不为TRUE的时候。如果$page的值是FALSE,那么节点本身就并没有显示出来,而是显示在某个列表里,比如搜索引擎的结果或者最新节点的列表。在这中情况下我们可不想添加任何东西。所以使用break语句来结束switch语句以避免对页面做出任何修改。
 
在把注释表单添加到网页中之前,我们需要确认当前编写的页面的类型是否在设置页面中启用了注释功能,所以我们将实现settings功能时保存的数组再检索出来——我们曾用一个很具有描述性的名字记录了它,$types_to_annotate。再次使用variable_get()函数,我们仍然为它定义了默认值,以应对网站管理员未曾在annotate的设定页面做出选择的情况。而下一步就是检验我们正在处理的节点的类型是不是被包含在$types_to_annotate之中了。然后,如果此节点不是我们想添加注释的种类,则再次使用break语句。
 
最后的尝试便是创建表单并把它添加到节点中去。首先是在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
$form['annotate']['nid']
'#type' => 'value',
'#value' => $node->nid,
);
// Define a submit functi
$form['annotate']['submit
'#type' => 'submit',
'#value' => t('Update')
);
return $form;
}
 
函数有两个参数。第一个$form_state,对于它Drupal的所有表单函数都是自动传递的,我们先不管它,详细的情况将在第10章formAPI进行讨论。第二个参数是$node,是我们用来传给早先实现的nodeapi钩子中的drupal_get_form()函数的。
 
建立表单的过程和我们之前在annotate_admin_settings()函数中做的基本一致,都是通过建立一个带有键值的数组——只不过这次我们要将文本框和提交按钮放在同一个区域,这样在网页上他们就可以显示在同一组内容中了。首先,建立一个数组,将它的#type键的值设定为‘fieldset’,并给它加个标题。然后建立一个数组用来描述文本区。注意一点,textarea数组的键是fieldset数组的一员。换句话说,我们将使用$form['annotate']['note']而不是$form['note']。这样的话Drupal就能判断出textarea的元素是fieldset元素的一员。我们使用了一个三元操作符来判断注释的默认值是要显示一个已存在的注释,或者是当前没有注释的话就显示一个空的字符串。最后,我们建立了提交按钮,并返回数组,完成了对我们这个表单的定义。
 
将注意力回到annotate_nodeapi()函数上,我们通过对节点内容赋予“值”和“权重”来实现将表单显示在网页内容中。这里“值”主要指明应该显示什么,而“权重”则是告知Drupal应把你的内容和这节点中的其他内容按照一个什么样顺序关系显示出来。我们要的是一个在页面下部的注释表单,所以我们给了它一个比较大的权重,10。而我们想要显示的是表单,所以就要调用drupal_get_form()来使我们的表单从一个描述它内容的数组变成一个完整的HTML表单。注意一下我们是如何将$node传给表单函数的,通过它能获得以前的已经存在的注释,并把它预先填在表单中 。
 
建立一个节点,并在你的浏览器中访问它,你应该看到注释表单了吧。(见图2-2)
                        图2-2 注释表单显示在网页上的情形
 
当我们点击提交按钮,会发生什么事情呢?什么也不会发生,因为我们还没有写任何代码去对这个表单进行操作。我们现在就去加上吧。不过在这之前,我们必须考虑一下用户输入的内容将要被存储在哪里。
 
在数据库表中存储数据
最常用的方法是建一个独立的数据库表来为模块存储数据,这样可以将数据和Drupal的核心表区分开来。当决定模块使用什么字段的时候,你应该问问自己:我都需要存储什么数据?如果我对这个表进行查询,我需要哪些字段和索引呢。最后是,我对自己的模块有什么更长远的计划么?
 
我们需要保存的数据只是一些来自annotation的简单文本:节点的ID,撰写注释的用户的ID。记录时间戳也可能有用,那样我们就可以建立一个按时间顺序排列的列表,来显示最近更新的注释。最终我们要问的问题是:“注释对于这个用户和这个节点有什么用?”我们将基于uid(用户ID)和nid(节点ID)建立一个混合索引,这样可以使查询变得尽可能的快。我们的表所用到的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),
);
 
我们可以只是把以上的SQL语句放在模块附带的README.txt文件中,让其他安装我们模块的人自己在数据库中添加相应的数据表。不过,我们可以利用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;
}
 
在这里,我们通过实现schema钩子来描述了一个模式,而当annotate模块第一次被启用的时候,Drupal会搜寻这个叫annotate.install的文件,并运行annotate.install()函数,从而解读这个模式。我们描述了我们希望Drupal创建的数据表和字段,而Drupal将他们翻译成我们所使用的数据库的语句(如果想知道这一切工作是如何进行的,请看第5章)。如果一切顺利,数据表就已经建好了。但是因为我们刚才已经启用的这个模块,所以现在要在有.install文件的情况下将它重新安装一次。
 
请按如下步骤操作:
 
1 Administer > Site building >Modules page页面关闭这个模块。
2 Administer > Site building >Modules page页面的卸载表中卸载这个模块。这是正确操作模块的方法,它将使Drupal当模块从未存在过。
3 启用模块。而这次,数据表会在同一时间被创建。
 
Tip:如果你因为在install文件中范了语法错误或者其他的失误而导致了失败,按照类似上面的方法卸载模块和数据表可以使一切还原。而到了万不得已的时候,删除数据库中system表中关于此模块的那行也可以达到同样的目的。
 
Drupal创建了用来储存数据的anntantions表之后,我们就得对代码做些修改了。不为别的,只因为一旦用户输入了注释的内容并点击提交按钮,我们必须用程序来对这些行为做出处理。
 
提交函数如下:
 
/**
* 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)。第三,我们在查询语句中使用了文本占位符(%d是整型变量的文本占位符,而%s是字符型的)以供变量进行替换,好让Drupal内置的安全机制就可以起到防止SQL注入攻击的作用。然后,drupal_set_message()函数在用户会话中放置了一条信息,并在用户访问下一个页面的时候作为反馈信息显示出来。
 
最后,在将表单指派到$node->content中去之前,为了将可能已经存在的注释从数据库中提取出来填到注释表单中去,我们要修改一下nodeapi钩子的代码。下面代码中黑体的部分就是要添加的:
 
/**
* Implementation of hook_nodeapi().
*/
 
function annotate_nodeapi(&$node, $op, $teaser, $page) {
global $user;
switch ($op) {
// The 'view' operation means the node is about to be displayed.
case 'view':
// Abort if the user is an anonymous user (not logged in) or
// if only the node summary (teaser) is being displayed.
if ($user->uid == 0 || !$page) {
break;
}
// Find out which node types we should annotate.
$types_to_annotate = variable_get('annotate_node_types', array('page'));
// Abort if this node is not one of the types we should annotate.
if (!in_array($node->type, $types_to_annotate)) {
break;
}
 
// Get the current annotation for this node from the database
// and store it in the node object.
$result = db_query('SELECT note FROM {annotations} WHERE nid = %d
AND uid = %d', $node->nid, $user->uid);
$node->annotation = db_result($result);
 
// Add our form as a content item.
$node->content['annotation_form'] = array(
'#value' => drupal_get_form('annotate_entry_form', $node),
'#weight' => 10
);
break;
 
case 'delete':
db_query('DELETE FROM {annotations} WHERE nid = %d', $node->nid);
break;
}
}
 
我们第一步做的就是在数据库中搜寻属于当前用户和节点的注释。然后,使用了db_result()函数,这个函数的作用是得到上面的查询结果的第一条数据的第一个字段,在这个例子里由于我们只允许每人对每字节发表一个注释,所以数据应该始终只有一条。
 
之后是又给nodeapi钩子加了个删除的功能,使得注释将会随着节点的删除而删除。
 
调试一下你的模块吧,它应该可以存取注释了。该是小小的高兴一下的时候了——你已经从零开始完成了一个Drupal的模块,现在,你算是踏上了成为Drupal高手的路了。

第五节 定义你自己的管理页面

Drupal有几种不同类别的管理设定,像内容管理,用户管理,都是展现在主管理页面上的。如果你的模块需要一个自己的分类,创建它是非常容易的事情。在这个例子中,我们通过修改模块的menu钩子来创建一个新的、名为“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 导向annotation的设定页面的链接现在是一个新的独立的分类了
 
如果你是在本地进行操作,你需要清空菜单缓存才能看见以上链接。去掉数据库中的cache_menu表,或者点击development模块(devel.module)中的“Rebuild menus”选项,又或者使用Administer > Site configuration > Performance页面中的Clear cached data按钮都可以达到目的。
 
Tipdevelopment模块(http://drupal.org/project/devel)是专为支持Drupal开发而写的。它可以让你快捷的使用清空缓存,访问变量,追踪请求,以及其他大量的开发用函数,它简直是开发必备模块之一。如果你还没安装,那赶快去下载放在sites/all/modules/devel目录下吧,然后就是Administer > Site building > Blocks页面开启它了。
 
我们分两步建立了新的分类。第一步,我们添加了个菜单项(实际是一个特殊的路径admin/annotate)用来描述这个分类的标题。通过将它的权重设置为-5,我们将它放置在了一个正确的位置,正如图2-3中所见,就在“Site configuration”分类的上面。
 
第二步是让Drupal把指向annotation settings的链接放在“Node annotation”分类中。修改一下我们创建的路径就可以做到这点,所以路径就从admin/settings/annotate变成了admin/annotate/settings。以前,这个菜单项是admin/settings的一个子类,就像表2-1中所示,其路径是指向“Site configuration”分类的。当Drupal重建整个菜单树的时候,它会分析和确定各个菜单项之间的从属关系,此时由于admin/annotate/settings是admin/annotate的子类,所以在显示上也依据于这个关系。也就是说将模块的菜单项呈树状放在表2-1那种路径之下,就可以让它们在Drupal的管理页面显现出来了。
 
Drupal在应对请求的时候,只会读取必要的文件,这样可以节约内存的使用。因为我们的page callback指向的函数并不在我们的模块包含的范围之内(实际是syatem.module文件中的system_admin_menu_block_page()函数),所以我们需要让Drupal读取的文件就是modules/system/system.admin.inc,而不是sites/all/modules/custom/annotate/
system.admin.inc文件。为了做到此点,我们让Drupal获取了系统模块的路径,并将结果作为菜单项中file path一项的值。
 
当然,这个例子显得有些不自然,在实际应用中,你应该有更好的理由去创建一个新的分类,以免太多的分类困扰着你的管理员(通常这个管理员就是你自己!)。
 
       表2-1 指向对各个分类进行管理的路径

第六节 为用户做一个设定用的表单

在annotate模块中,我们给了管理员以设置哪些节点类型可以支持注释功能的权限。(如图2-1)。现在让我们深入了解一下其工作原理吧。
 
当一个网站管理员想要改变对annotata模块的设定的时候,我们就要显示一个表单,好让他对我们提供的选项进行选择。在我们的菜单项中,我们将page callback指向了drupal_get_form()函数,并将page arguments设置成一个包含了annotate_admin_settings的数组。这意味着。当用户访问http://example.com/?q=admin/annotate/settings的时候就会调用drupal_get_form ('annotate_admin_settings')函数,这个过程就是让Drupal建立annotate_admin_settings()函数所定义的表单的过程。
 
让我们看一下这个函数,它定义了一个关于节点类型的复选框(见图2-1),以及添加了另外两个可选择性区域。它就在sites/all/modules/ custom/annotate/annotate.a-
dmin.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_value' => variable_get('annotate_deletion', 0)
 // Default to Never
);
 
$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处理表单的时候,就会调用nnotate_admin_settings_validate()函数来进行验证了。如果我们认为用户输入了一个不合规则的数据,我们就对发生错误的地方报错,反映到显示上就是弹出报错信息并用对出错的区域进行高亮标记,如图2-5.
 
                    图2-5 验证函数弹出报错信息
 
Drupal是如何知道该调用我们的函数了呢?实际是通过这样一种特殊的命名方式——用定义表单的函数名(annotate_admin_settings)加上_ validate。对于表单验证函数的调用过程的详细说明,参见第10章。

第八节 存储设定

在这个例子中,改变设定并点击“Save configuration”按钮,结果就被储存下来。而点击“Reset to defaults”按钮,字段就都被重置成默认值。接下来的内容就讲明了这一切的工作机制。
 
使用Drupal的变量表
 
我们先看一下“Annotations per node”。它的#default_value键的值被设置成variable_get('annotate_limit_per_node', 1)
 
在数据库中,Drupal有一个关于变量的表,通过使用variable_set($key, $value)和variable_get($key, $default)可以实现键—值的成对存和取。所以上面的代码实际上是在说:“将‘Annotations per node’的默认值作为变量annotate_limit_per_node的值存在变量表中,但如果没找到任何值,就将1作为默认值。”所以当“Reset to defaults”按钮被点击,Drupal就会从变量表中删除annotate_limit_per_node的当前值,并将其替换为默认值1.
 
Note:为了使变量的存取不会出现命名冲突,请保持你表单元素和变量的命名上的一致性(例如当前例子中的annotate_limit_per_node),而创建表单元素或者变量时命名的规则都是使用你的模块名加上一个具有描述性质的名字。
 
“Annotations will be deleted”的情况将会复杂一些,因为它是一个单选按钮。其#options键如下:
 
'#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) // Default to Never
 
这段代码的意思是,将将数组的返回结果初始化为0,其值是t('Never')。
 
用variable_get()函数读入已经存储的值
variable_get()用于读取你模块中已经保存好的设置。
 
// Get stored setting of maximum number of annotations per node.
$max = variable_get('annotate_limit_per_node', 1);
 
要注意到的是,这里variable_get()函数也使用了默认值,这是为了应对没有已存储好的值可以使用的情况(可能管理员还没有访问过这个设定页面)。

第九节 更进一步的工作

我们将会在开源社区中分享这一模块,那么自然就需要一个README.txt文件,就建在annotate目录下,放在annotate.info,annotate.module和annotate.install文件的旁边吧。README.txt文件大致内容就是谁开发了这个模块以及如何安装它。版权信息不必包含在内,因为所有上传到drupal.org的模块都要求是GPL许可的,而且网站在为你的软件打包的时候会自动加入LICENSE.txt文件。那么下面你就可以将它上传到drupal.org的社区中去了,记得建立个项目页面以便获取其他社区成员的反馈信息。

总结

在这一章的学习结束之后,你应该可以做到以下几点:
 
    从头开始创建一个Drupal模块
    明白如何用hook进入到Drupal的代码执行过程中去
    可以进行模块设置信息的保存和读取
    使用Drupal的formsAPI处理简单的表单
    使用你模块的数据表进行数据的存取
    在主管理页面建立一个新的管理分类
    为网站管理员定义一个使用复选、单选、文本输入等功能的表单
    验证设定,并且如果验证不通过则发送报错信息
    理解Drupal如何利用内置的连续变量系统进行变量的存取

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

当使用Drupal时,一种常见的目标是,当某个事件发生时,能够执行一段操作。例如,网站管理员可能希望当有新的消息发布时,能够收到一封email。或者是如果用户发布了一些包含敏感词的评论,用户应该被禁止继续操作。本章讨论了如何在Drupal的事件发生时,能够触发一些你自己定义的代码。

第三节 触发器用户界面

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

第一节 理解事件和触发器

理解事件和触发器
 
Drupal在运行时,会经历一系列的事件。这些内部事件实际上是一种时间点,在这些时间点上,可以通过代码介入Drupal的运行过程,并与其进行交互。下图显示了某些Drupal的事件:
对于Drupal的开发者来说,这些内部事件,也被称为“钩子”。这是因为当某个事件发生时,Drupal允许模块“钩入”代码执行的轨迹(当然可以进而改变和定制其行为)。你在前面章节中应该已经见过几个钩子了。通常,开发模块的过程中要考虑到你希望修改哪些Drupal的行为,也就是你希望在自己的模块中实现哪些钩子。
 
设想你有一个刚刚建立的网站,此刻你正从你家的地下室的电脑中操纵这个网站。当网站访问量越来越大时,你计划把它卖给一家大公司以变得有钱。同时,你希望每次有用户登录时,都会被提醒。你于是决定,一旦有用户登录时,就让计算机叫一声beep。但是,因为你的猫在睡觉,它很讨厌这样beep的一声,所以你决定暂时用一个简单的日志项来代替beep。你可以很快的写个.info文件,然后把它放在sites/all/modules/custom/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!');
}
 
这样,就把”beep”这个信息写入了Drupal的日志。目前来说这已经足够了。接下来,就该让告诉Drupal,下次有人登录的时候,要发出一声响。想要实现这个非常简单,只需要在我们的模块中实现hook_user()这个钩子,捕获用户登录(login)的这个操作就可以了。
 
/**
* Implementation of hook_user().
*/
function beep_user($op, &$edit, &$account, $category = NULL) {
if ($op == 'login') {
beep_beep();
}
}
 
看到了吧,这很容易。那么,如果我们想让网站有任何新内容添加时,也beep的响一下,该怎么实现呢?只需要通过在我们自己的模块中实现hook_nodeapi()这个钩子,捕获到节点的insert这个操作就可以了:
 
/**
* Implementation of hook_nodeapi().
*/
function hook_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
if ($op == 'insert') {
beep_beep();
}
}
 
那么,如果我们想在有人添加一条新的评论时让机器beep一下呢?这也很简单,只需要实现hook_comment()这个钩子,捕获到insert这个操作就可以了。让我们停下来想想,前面这些操作,我们实际上一直在重复一件事,就是实现钩子,捕获某个操作。如果此时,能够有一个图形界面,让我们轻松的把”beep”这个动作同任意一个钩子的任意一个操作关联起来,这是不是很酷呢?很好,这实际上,就是Drupal内置的trigger模块所做的事。它允许把某个动作(action)同某个事件(event)关联起来。在代码中,一个事件被定义为一个“钩子+针对于该钩子的一个操作”的组合。例如,“用户钩子+登录操作”又或是“nodeapi钩子+insert操作”。当某一个这种操作发生时,trigger.module这个模块会让你触发一个动作。
 
为避免混淆,让我们来澄清一些术语:
 
    事件(Event):从一种通用的编程思维上看,这个术语可以被理解为系统中一个组件发向另一组件的消息。
    钩子(Hook):这一种在Drupal使用中的编程技巧,它允许代码可以“钩入”正常的程序执行流程,以达到扩展功能的效果。
    操作(Operation):这指的是在某一个钩子内发生的具体的执行流程,例如,在hook_user()这个钩子里,有一个流程就叫login,也就是用户登录。
    触发器(trigger):它指的是某个钩子和操作的组合,同时,还有一个或多个动作被关联到这个组合上来。例如,beeping这个动作,是与user_hooklogin这个操作进行关联的。

第二节 理解动作(Action)

一个动作,实际上就是Drupal做的一件事。以下是一些例子:
    把一个节点推荐到首页
    把一个节点从已发布状态改变到未发布状态
    删除一个注册用户
    发送一封email
 
上面的这些,全都是清晰定义好的任务,编程人员会发现,前面的列表实际上与PHP的函数间有某种相似性。例如,你可以通过调用includes/mail.inc中的drupal_mail()这个函数,来发送一封email。动作(action),有点类似于函数,因为实际上动作本身是多个函数的集成体。也就是说,动作是由那些Drupal能够调用的或是可以与事件进行松散耦合的函数所组成的。下面,让我们来看看trigger这个模块。
 
Trigger模块的用户界面
浏览至Administrator > Site building > Modules,然后启动trigger模块。进到Administrator > Site building > Triggers。你应该能看到与下图类似的界面:
 
请注意最上方的几个标签页,它们与Drupal的钩子完全对应!在上图中,我们实际上正在看的是nodeapi这个钩子中的操作(operation)。这些操作被给予了好听的名字,比如,”delete”这个操作,被称为”after deleting a post”。因此,所有的钩子中被允许的操作都被显示出来,同时,用户还可以给这些操作关联上一个动作,比如当某个操作发生时,执行“把节点推荐到首页”这个动作。所有的可以使用的动作都在”Choose an action”这个下拉列表中。
 
Note: 并非所有的动作都适用于所有的触发器,因为在某些情况下,有的动作没有任何意义。例如,你在删除某个节点后,通常不会去运行“推荐到首页”这个动作,因为这根本无意义。根据你的安装,有些触发器会显示“No actions available for this trigger”。
 
下表显示了触发器以及它们相应的钩子和操作的组合:
 
 
你自己开发的第一个动作(Action)
如果我们想把前面所写的让机器beeping一声的动作,添加入触发器中,有个步骤:
1.       通知Drupal我们所写的动作将支持哪个触发器
2.       创建Action的函数
其中,第一步是通过实现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这个模块中,实际上现在只有一个action,因此在我们将要返回的数组中一共也只有一项。这一项的键名,也是它的key,是使用代表Action的那个函数的名称,在本例中,就是beep_beep_action()这个函数。事实上,当浏览代码的时候,如果能知道某个方法是一个Action,这样总是很方便的。所以我们在方法的最后加上了_action这样的后缀,因此使用了方法名beep_beep_action(),而非beep_beep()
 
我们再仔细看一下返回的数组中的各个键名:
    Type: 这表示着你所构建的动作的类型,Drupal使用这个信息来对各种动作进行分类,这种分类最终被体现于触发器配置的用户界面里出现的各个下拉框下。取值的范围包括system, node, user, comment, 还有taxonomy。那么,如何决定我所写的Action是何种类型的呢?基本上这取决于你所开发的新动作将和什么对象打交道。如果这个动作将会和多种类型的对象打交道,那么就使用system类型。
    Description: 顾名思义,这是关于当前这个Action的描述信息。它将会被显示于触发器配置界面的下拉选择框内。
    Configurable: 这用于决定是否当前Action需要接收参数。
    Hooks: 在这个数组中,可以看到,键名就是钩子名称,而键值实际上是遍历了一遍这个钩子下的操作,这些操作会被当前所定义的这个Action所支持。Drupal正是使用了这里所定义的内容来决定在触发器页面上是否为某个触发器显示当前这个Action
 
前面我们已经描述过了我们将要添加的Action,现在让我们来完成它:
/**
* Simulate a beep. A Drupal action.
*/
function beep_beep_action() {
beep_beep();
}
好吧,这看上去也不是很难,是吧?在继续之前,让我们删掉beep_user()beep_nodeapi(),因为我们将要使用图型界面来进行配置,而不再使用直接的钩子实现。
 
Action与触发器关联
现在让我们再一次来到administrator > site building > Triggers这个页面。如果你已经把上面说的每件事都按正确的方式完成了,那么你写的动作,应该会出现在这个界面中,如下图所示:
 
变更一个动作所支持的触发器
如果你想改变当前动作支持哪个触发器,你应该能在界面中进行相应的选择。例如,如果你把代码改成下面那样,那么”Beep”这个action就只支持”After deleting a post”这一个触发器了。
 
/**
* Implementation of hook_action_info().
*/
function beep_action_info() {
$info['beep_beep_action'] = array(
'type' => 'system',
'description' => t('Beep annoyingly'),
'configurable' => FALSE,
'hooks' => array(
'nodeapi' => array('delete'),
),
);
return $info;
}
 
支持所有触发器的动作
如果你不想限制你的Action只支持少数几个触发器,你可以声明你的Action将支持任意的触发器,代码如下:
 
/**
* Implementation of hook_action_info().
*/
function beep_action_info() {
$info['beep_beep_action'] = array(
'type' => 'system',
'description' => t('Beep annoyingly'),
'configurable' => FALSE,
'hooks' => array(
'any' => TRUE,
),
);
return $info;
}
 
更高级的Action操作
Drupal中事实上有两种Action,一种是支持参数的,另一种不支持。前面我们所讨论的”Beep”这个Action就没有使用任何参数。当它被执行的时候,系统beep一下,然后就结束了。这是一种比较简单的情况。然而,很多时间,我们会需要更复杂一些的功能。比如,发送email这样一个动作,我们需要知道接收者的email地址,还要知道邮件的标题是啥。像这样的动作,我们需要设置一个配置表单,并且我们把这样的动作称为高级的动作(advanced action),或者也可以称为可配置的动作。
 
简单的动作是不需要参数的,同时也不需要配置表单,并且系统自动激活(trigger模块被激活后)。通过把hook_action_info()实现中的configurable键值设置为true,就可让Drupal知道刚刚所写的动作是一个高级的动作,同时还需提供一个表单,一个验证表单的方法以及一个表单的submit提交函数来配置这个action。下表列出了简单动作和高级动作之间的区别:
(
我这儿简单用中文翻译一下这张表哈:
SA代表简单ActionAA代表高级Action
SA不需要参数,AA需要
SA不需要配置用的表单,AA需要
SA设置完成后,系统会自动将其发布到trigger页面中,AA必须使用动作管理页面对其进行发布。
SA时,把configure那个键设为false, AA则设为真
)
 
接下来让我们创建一个高级的动作,该动作可以beep多次。我们可以使用一个配置表单来注明到底要让它beep多少次。
 
首先,我们需要告诉Drupal当前正在创建的这个动作是需要被配置的,也就是configurable要设为true。让我们为beep_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;
}
 
接着让我们快速检查一下在administrator > site configuration > Actions中,是否已经有相应的内容出现。很显然,如下图所示你能看到,我们所添加的高级动作,已经出现在了这个界面中的下拉框里:
 
现在,我们需要提供一个表单,这样管理员才可以选择到底每次要beep多少下。我们通过Drupal的表单API来定义一个或多个的字段来实现它。同时,我们也会给出关于表单验证和提交的函数。这些函数的名字是基于所定义的actionID,也就是hook_action_info()方法中所给出的beep_multiple_beep_action,同时,按照惯例,我们需要在这个actionID后面加上_from来构成一个表单的方法名,也就是beep_multiple_beep_action_formDrupal的表单验证函数名通常是由表单名称再加上_validate来构成(beep_multiple_beep_action_validate),而提交函数名通常是由表单名称再加上_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的表单。在这里我们只定义了一个单文本字段,这样的话,管理员就可以输入beep的次数。当管理员选择添加高级动作”Beep multiple times”时,Drupal就会展现给用户我们所创建的配置表单,以便让用户进行配置,就如下图所示:
 
 
Drupal本身为这个配置表单添加了一个Description的字段,这里面的值是可以更改的,并且会被用于替换默认的描述信息,也就是我们在beep_action_info这个方法里所定义的那个description。这一点很有用,是因为我们有可能会创建一个高级动作,并把description命名为“响两声”,然后可能再创建一个高级动作,并把description命名为“响五声”。这样的话,当我们想把一个动作同一个触发器相关联时,我们能很清楚的知道哪个动作代表什么意义,对管理员来说也就更容易清楚的知道动作的具体含义。
 
 
Tip: 这两个高级动作,“响两声”和“响五声”,可以被看作是Beep multiple times这个Action的两个实例。
 
上面所写的验证方法与任何Drupal中的表单验证方法是一样的(具体内容查看章节10),在这里,我们检查了用户是否真正输入了一个合法的数字,同时我们也检查了这个输入的数字是否超过一定范围。
 
对于高级动作的配置表单来说,它的提交方法的返回值是有些特殊的。它应当是一个由我们所感兴趣的字段作为键名的数组。这个数组中的值会在Action真正运行时生效。由于在上面的表单中,description这个字段是由系统自动管理的,因此在这里我们只需要返回我们提供的字段,也就是beep的次数这个字段。
 
最后,来写一下action本身的代码:
/**
* 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。这正是与前面我们所写的简单action的最大不同之处,前面的简单action是没有参数的。
 
NOTE: 事实上,简单的Action也可以接收到那些高级的需要配置的Action所能接收的参数。因为PHP本身会忽略那些没有在方法签名中声明过的参数,所以如果我们需要了解一下当前的上下文内容话,我们只需简单的把方法beep_beep_action()改为been_beep_action($object, $context)。所有的Action实际上都是使用相同的参数来调用的,只不过在接收时,高级Action使用了参数,而简单的Action不使用而已。

第四章 菜单系统

Drupal菜单系统比较复杂,但是很强大。单词“菜单系统”有些用词不当。如果将菜单系统作为一个拥有三个主要责任的话,可能会更恰当一些:1,回调映射,2,访问控制,3,菜单定制。关于菜单系统的主要代码位于includes/menu.inc里,而比如启用定制菜单这些特性的可选代码位于menu.module.

在本章,我们将探索一下什么是回调映射以及它是如何工作的,看一下如何通过访问控制来保护菜单项,并逐条列出了所有的各种内建的菜单项。最后通过检验如何覆写,添加,和删除存在的菜单项来结束本章,这样你就可以随心所欲的定制Drupal了。
 

将URLs映射为函数

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

1.建立Drupal路径。如果路径是一个真实路径的别名,Drupal将找出真实路径并使用它来代替别名。比如,如果管理员使用别名http://example.com/?q=about 来代替http://example.com/?q=node/3(比如,使用路径模块),那么Drupal将会使用node/3作为内部路径。

2.Drupal使用menu_router表来追踪路径与回调函数之间的映射,使用menu_links表来追踪菜单项链接。首先会检查是否需要重新构建menu_router和menu_links表,不过一般在Drupal安装或者更新以后,就很少再会重新构建了。

3.计算出menu_router表中的哪个条目对应于Drupal路径,并构建出一个路由项,来描述即将被调用的回调。

4. 加载需要传递给回调的对象。

5. 检查用户是否有权访问该回调。如果没有的话,返回一个“拒绝访问”消息

6. 根据当前语言,将菜单项的标题和描述本地化。

7. 加载需要的.inc文件

8. 调用回调并返回结果,index.php将调用theme_page(),从而为浏览器返回一个网页。

图4-1和4-2图形表示了这一流程。

 

图4-1菜单分发流程概貌

注意图4-1中的获取菜单数组(Get menu array)这一部分。在这一流程中Drupal构造了数组$menu,它包含了每一菜单项的详细信息,比如菜单项的路径,允许那些人访问,子菜单项,等等。图4-2展示了Drupal如何构造菜单数组的概貌。

 

图4-2菜单数组构造流程概貌

第一节:回调映射

回调映射(Callback Mapping)

当一个Web浏览器向Drupal发送一个请求时,它向Drupal传递一个URL。通过这一信息,Drupal必须指出要运行哪段代码以及如何处理这一请求。这通常称为分发。Drupal截取URL的基础部分并使用后面的部分,称之为路径。举例来说,如果URL是http://example.com/?q=node/3,则Drupal路径为node/3。

 

第五章 操作数据库

Drupal depends on a database to function correctly. Inside Drupal, a lightweight database
abstraction layer exists between your code and the database. In this chapter, you’ll
learn about how the database abstraction layer works and how to use it. You’ll see how
queries can be modified by modules. Then, you’ll look at how to connect to additional
databases (such as a legacy database). Finally, you’ll examine how the queries necessary to
create and update database tables can be included in your module’s .install file by using
Drupal’s schema API.

第六章 操作用户对象

第七章 操作节点对象

第八章 主题系统

第九章 操作区块对象

第九章 操作区块对象

第十章 表单API

第十一章 控制用户的输入:过滤器系统

第十二章 查找和索引内容

第十三章 操作文件

第十四章 操作分类对象

第十五章 缓存

第十六章 会话

第十七章 使用jQuery

第十八章 本地化和翻译

第十九章 XML-RPC

第二十章 编写安全的代码

第二十一章 最佳开发实践

第二十一章 最佳开发实践

第二十二章 优化Drupal

第二十二章 优化Drupal

第二十三章 安装Profiles

第二十三章 安装Profiles

附录A:数据库表参考

附录A:数据库表参考

附录B:资源