Drupal6专业开发指南(Drupal6版,下部)

太多了,Drupal的手册拖动起来不灵活,希望这份译文能够帮助更多的人学会Drupal,尽管开始的时候,希望拿它卖钱的,结果非常惨淡,现在免费提供出来,希望能给需要的人提供帮助.

错误很多,希望大家共同改进,不过比第一版的相比,错误的数量应该降低了一个数量级,就像我说的,第一版的译文是错误千出,第2版是错误百出,限于译者的精力有限,就不一一改进了.

我也买过看过很多翻译的技术书籍,我相信,第2版的译文好于市面上的80%的技术书籍的译文,第一版翻译完,我从来不看中文的,因为我觉得错误太多了.第2版的中文,我经常拿来翻的.

不多说了,加速连载中.

Drupal版本:

Drupal专业开发指南 第14章 使用分类(Taxonomy)

分类是对事物的划分归类。Drupal自带了一个分类模块,它允许你对节点(也就是主要的“事物”)进行分类。在本章中,你将看到Drupal支持的不同种类的分类。你还将看到数据是如何存储的,如何在你自己的模块中查询分类数据库表。最后,你将会看到,当分类改变时,你的模块如何收到相关通知,并且我们将介绍一些常用的分类相关的任务。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第14章 什么是分类

 

分类涉及到把事物放进不同的类别中。在管理界面“管理➤内容管理➤分类”下面(如果在这里没有的话,请确认启用了分类模块),你将找到Drupal对分类的支持。当涉及到Drupal的分类系统时,用词的准确性是非常重要的。让我们学习一下你将会遇到的一些常用词。
 
术语
    术语就是一个实际的标签,它将要应用到节点之上。例如,假定你有一个包含产品评估的网站。你可以使用术语“劣”,“合格”,“优”来为每个评估加个标签。术语有时也称为标签,将一个术语分配到一个对象(比如说一个产品评估节点)上的动作称之为标签化。
 
抽象层次
    过一会,当你查看数据结构时,你将会看到,Drupal为输入的所有术语添加了一个抽象层次,在内部是通过数字ID来引用它们的,而不是通过名字。例如,如果你输入了前面的那些术语,但是你的经理觉得“差”字儿比“劣”字儿更好一些,没关系。你简单的编辑这个号码为1的术语,将它从“劣”改为“差”,这就可以了。因为Drupal在内部看到的是术语的号码,所以在Drupa内部一切将继续正常工作。
 
同义词
    当你定义一个术语时,你可以输入该术语的同义词;同义词是具有同样语义的又一术语。Drupal中的分类功能允许你输入同义词,并提供了用来存储的数据库表,以及提供了一些实用函数比如taxonomy_get_synonyms($tid)和taxonomy_get_synonym_root($synonym),但是这些功能的用户界面的实现则留给了第3方模块,比如术语表模块(http://drupal.org/project/glossary)。
 
词汇表
    一个词汇表包含了一组术语。Drupal允许你将一个词汇表与一个或多个节点类型关联起来。这一松散的关联,在跨节点类型归类时非常有用。例如,如果你有一个站点,允许用户可以提交旅游相关的游记和图片,那么你可以使用一个词汇表,在里面使用国家名字作为术语;这样你就可以方便的查看带有“比利时”标签的所有游记和图片。词汇表编辑界面如图14-1所示。
 
必须的词汇表
    词汇表可以是必须的,也可以不是必须的。如果一个词汇表是必须的,那么用户在提交节点表单以前必须为节点选择一个术语。如果不是必须的,那么用户提交表单时,可以使用默认术语“没有选择任何选项”。
 
受控的词汇表
    当词汇表中的术语数量被限定时(也就是说,用户不能添加新的术语),此时它被称为受控的词汇表。对于一个受控的词汇表,其中的术语一般都通过下拉选择框显示给用户。当然,管理员,或者拥有管理分类权限的用户可以添加,删除,或者修改术语。
 
标签
    一个标签实际上就是一个术语。然而,“标签化”这个词儿,一般隐含了由网站用户来创建标签这层含义。这与受控的词汇表刚好相反。当用户提交一个节点时,可以输入他们自己的术语,而不是从一个词汇表中选择一个术语。如果术语还不是词汇表的一部分,那么它将被添加进来。在词汇表的编辑界面,如果标签复选框被选中了,那么词汇表的用户界面将显示为一个文本输入框(启用了JavaScript自动完成),而不是受控的词汇表所使用的下拉选择框。
14-1 添加词汇表所用的表单
 
单个VS多个术语
    在词汇表编辑界面,通过使用“多重选择”复选框,Drupal允许你声明,对于一个给定的节点,是使用单个术语还是使用多个术语。如果声明了多个术语,那么节点提交表单中的用户界面也将改变,从简单的下拉选择框变成了多选下拉字段。
 
提示:“多重选择”选项仅仅适用于受控的词汇表,对于启用了标签的词汇表不适用。
 
上层
    当添加或编辑一个术语时,在表单的“高级选项”部分(如图14-2所示),可以选择一个上一层术语。这个在术语之间定义了一个层级关系。
 
14-2. 用来添加术语的表单
 
相关术语
    如果一个词汇表允许使用相关术语,那么当你定义一个新术语或者编辑一个已有术语时,将会显示一个多选字段,这样你就可以从已有术语中选出与该术语相关的了。该字段出现在表单的“高级选项”部分(如图14-2所示)。
 
重量
    每个词汇表都有一个重量,从-10到10(如图14-1所示)。这用来控制词汇表在节点提交表单中的位置。词汇表的重量越小,在词汇表字段集(Vocabularies fieldset)中的位置越靠前;重量越大,位置越靠后。
    每个术语也都有一个重量。术语的重量,是用来决定它在用户下拉选择框中的位置。这一顺序与“管理➤内容管理➤分类➤列出术语”中的一致。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第14章 分类的种类

 

这有几个不同类型的分类。最简单的仅有一列术语,而最复杂的则有多层级关系。另外,术语可以有同义词或者与其它术语相关。让我们从最简单的开始。
 
单层的
一个仅包含了一列术语的词汇表是非常简单的。表14-1展示了我们如何在一个简单的单层词汇表中对编程语言进行分类,我们将把这个词汇表叫做“编程语言”。
 
表14-1词汇表中的简单术语
术语 ID                   术语名字
1                       C
2                       C++
3                        Cobol
 
分层级的
现在,让我们引入一个概念“层级”,在这里每个术语都可能与其它术语存在一种关系。如表14-2所示。
 
表14-2 词汇表中分层级的术语(子术语位于它们的父术语的下面并缩进)
术语 ID                术语名字
1                   Object-Oriented
2                      C++
3                       Smalltalk
4                  Procedural
5                       C
6                       Cobol
 
14-3明确的展示了这种分层级的关系。在这个例子中,Procedural是一个父亲而Cobol是一个孩子。注意,每个术语都有它自己的ID,这与它是父亲还是孩子没有关系。
14-3 一个分层级的词汇表,其术语之间具有父子关系
   
    当创建术语时,在“添加术语”表单的“高级选项”部分中的上层字段里面,你可以为其选择一个上一层术语,或者你也可以使用拖放来调整术语的位置,这样你就可以将术语安排到层级里面。当添加了多个术语以后,导航到“管理➤内容管理➤分类”,点击该词汇表的“列出术语”连接,你就可以看到托放界面了。托放界面如图14-4所示。
 
14-4.使用托放界面,将术语安排到一个层级中。
 
多层级的
    一个词汇表可以有多个层级而不仅仅是单个层级。这也就是意味着一个术语可以有多个父亲。例如,假定你向你的编程语言词汇表中添加PHP。PHP可以使用过程化的方式编码,但是在最近的版本中,也引入了面向对象的编程能力。我们是把它放到面向对象的下面还是过程化的下面?在多层级关系中,你可以把它放在两者的下面,如图14-5所示
14-5 一个多层级的词汇表中,术语可以有多个父亲
 
       在网站建设的计划阶段,你值得花费大量的时间,用来认真的考虑一下分类的使用情况,以决定你需要使用哪类词汇表。
    因为多层级词汇表无法轻易的显示在用户界面,当你为一个术语选择多个父亲时,Drupal将警告你托放界面将被禁用。警告如图14-6所示。
14-6.为一个术语选择多个父亲将禁用托放界面
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第14章 根据术语查看内容

 

通过访问一个术语的URL,你可以来查看与该术语相关的所有节点,除非模块覆写了这个视图。例如,在URLhttp://example.com/?q=taxonomy/term/5中, 5就是你想要查看的术语的术语ID。结果将是一列标题和摘要,其中的每个节点都使用了该术语作为标签。
 
在URL中使用AND和OR
    分别使用逗号“,”和加号“+”,就可以让构建的分类URL支持AND和OR操作。下面是一些例子。
    为了显示分配给术语ID为5和6的所有节点,可以使用下面的URL:
 
 
    使用下面的URL,来显示分配给术语ID为1或2,或3的所有节点:
 
 
    taxonomy.module对AND和OR的混合使用,目前还没有提供支持。
 
提示:使用路径模块,为你用的这些分类URL设置友好的URL别名,这样它们后面就不会带有那些枯燥的数字了。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第14章 为分层级的词汇表声明深度

 

在前面的例子中,我们使用了一个隐含的参数。例如,URL
 
    实际上就是
 
    在准备将要显示的结果集时,尾数0就是所要搜索的层级的层次数量;如果参数为all,这意味着将包括所有的层次。假定你有一个分层级的词汇表,如表14-3所示。
 
14-3.一个地理的分层级的词汇表(子术语位于它们父亲的下面,同时缩进)
术语 ID        名字
1           加拿大
2               不列颠哥伦比亚省
3                   温哥华
4               安大略省
5                   多伦多
 
层级的第一层次是国家,加拿大;它有两个孩子,不列颠哥伦比亚省和安大略省。每一个省都包含一个孩子,一个主要的加拿大城市,在那里Drupal开发异常活跃。修改URL中的深度参数,所带来的影响如下所示。
以温哥华为标签的所有节点共用下面的URL:
 
 
为了显示标签为“不列颠哥伦比亚省”(但是没有标签“温哥华”)的所有节点,可以使用下面的URL:
 
 
为了显示标签为“不列颠哥伦比亚省”(包含标签“温哥华”)的所有节点(注意我们这里将深度设为了1),使用下面的URL:
 
 
标签为“加拿大”或者标签为任意一个加拿大省份或者城市的所有节点,都可使用下面的URL来显示:
 
 
注意 结果集是作为一个普通的节点列表来显示的。如果你想将节点的标题和(或)摘要分层级的显示出来,那么你需要编写一个自定义的主题函数,或者使用views模块(http://drupal.org/project/views)。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第14章 自动的RSS种子

 

每个术语都有一个自动的RSS种子,用来显示标签该术语的最新节点。例如,术语ID 3的种子位于
 
 
    注意深度参数(这里为0)是必须的。和期望的一样,你可以使用AND或者OR来联合术语以创建一个联合的种子,下面是术语2或4的种子,包含了所有的亲儿子(不包括孙子):
 
 
    下面是包含所有子孙术语的种子:
 
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第14章 存储分类

老葛的Drupal培训班 Think in Drupal

如果你不想局限于内置的分类能力,那么这将驱使着你去理解分类在数据库中是如何存储的。在一个典型的非Drupal数据库中,你可能通过向数据库表中简单的添加一列来创建一个单层分类。你已经看到了,Drupal使用普通的数据库表来添加分类。图14-7给出了表结构。
 
14-7 Drupal的分类数据库表:主键使用了粗体;索引字段使用了斜体。term_node表中的*vid,引用的是node_revisions表的版本ID,而不是词汇表ID。
 
     下列表组成了Drupal的分类存储系统:
 
• vocabulary:这个表存储了词汇表的相关信息,通过Drupal的分类界面可以对其进行修改。
 
• vocabulary_node_types: 这个表用来追踪词汇表与节点类型的对应关系。该类型是Drupal内部的节点类型名字(例如,blog),它对应于node表的type列。
 
• term_data: 这个表包含了术语的真实名字,它所在的词汇表,它的可选的描述,以及它的重量,后者用来决定它在术语的下拉选择框中的相对位置(例如,在节点提交表单上)。
 
• term_synonym: 这个表包含了给定术语ID的同义词。
 
• term_relation:当定义一个术语时,这个表用来追踪相关术语的术语ID。
 
• term_hierarchy:这个表包含了术语的术语ID和它父亲的术语ID。如果一个术语位于根部(也就是说,它没有父亲),那么它父亲的ID为0.
 
• term_node: 这个表是用来匹配术语与节点的,节点以该术语为标签。
 

Drupal版本:

Drupal专业开发指南 第14章 基于模块的词汇表

 

除了使用“管理➤内容管理➤分类”创建词汇表以外,模块也可以使用分类数据库表来存储它们自己的词汇表。例如,论坛模块使用分类数据库表来保存一个关于容器和论坛的词汇表。图片模块使用分类数据库表来管理相册。任何时候,当你发现自己需要实现分层级的术语时,你都需要考虑一下,如果使用分类模块和一个基于模块的词汇表,是否会更好一些?
vocabulary表中的module列用来标识哪个模块拥有这一词汇表。一般情况下,这一列将包含taxonomy(分类),这是因为分类模块管理着大多数词汇表。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第14章 创建一个基于模块的词汇表

 

让我们看一个基于模块的词汇表的例子。第3方的相册模块(包含在图片模块;参看http://drupal.org/project/image)使用分类来管理不同的相册。它通过代码创建了它的词汇表,如下面的例子所示,并且通过将$vocabulary数组的module键设为该模块的名字(不带.module)来锁定该词汇表的所有权。
 
/**
 * Returns (and possibly creates) a new vocabulary for Image galleries.
 */
function _image_gallery_get_vid() {
    $vid = variable_get('image_gallery_nav_vocabulary', '');
    if (empty($vid) || is_null(taxonomy_vocabulary_load($vid))) {
        // Check to see if an image gallery vocabulary exists.
        $vid = db_result(db_query("SELECT vid FROM {vocabulary} WHERE
            module='image_gallery'"));
        if (!$vid) {
            $vocabulary = array(
                'name' => t('Image Galleries'),
                'multiple' => '0',
                'required' => '0',
                'hierarchy' => '1',
                'relations' => '0',
                'module' => 'image_gallery',
                'nodes' => array(
                    'image' => 1
                )
            );
            taxonomy_save_vocabulary($vocabulary);
            $vid = $vocabulary['vid'];
        }
        variable_set('image_gallery_nav_vocabulary', $vid);
    }
 
    return $vid;
}
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第14章 为术语提供自定义路径

老葛的Drupal培训班 Think in Drupal

如果你的模块负责维护一个词汇表,你可能想为该模块控制的术语提供自定义路径,来代替由taxonomy.module提供的默认路径taxonomy/term/[term id]。当为一个术语生成一个链接时,调用taxonomy.module中的taxonomy_term_path()函数(你应该调用这个函数,而不是自己为分类术语创建链接;不要假定分类模块维护了该分类)。注意,在下面的代码中,它是如何检查哪个模块拥有该词汇表的:
 
/**
 * For vocabularies not maintained by taxonomy.module, give the maintaining
 * module a chance to provide a path for terms in that vocabulary.
 *
 * @param $term
 * A term object.
 * @return
 * An internal Drupal path.
 */
function taxonomy_term_path($term) {
    $vocabulary = taxonomy_get_vocabulary($term->vid);
    if ($vocabulary->module != 'taxonomy' &&
        $path = module_invoke($vocabulary->module, 'term_path', $term)) {
            return $path;
    }
    return 'taxonomy/term/'. $term->tid;
}
 
    例如,image_gallery.module将路径重定向为了image/tid/[term id]:
 
function image_gallery_term_path($term) {
    return 'image/tid/'. $term->tid;
}

Drupal版本:

Drupal专业开发指南 第14章 使用hook_taxonomy()来获悉词汇表的变更

老葛的Drupal培训班 Think in Drupal

如果你为你自己的模块保留了一个词汇表,当使用标准的分类用户界面修改你的词汇表时,那么你将想收到词汇表的相关修改信息。对于由taxonomy.module维护的已有的一个词汇表,当它被修改时,你可能也想收到相关的修改信息。在任何一种情况下,通过实现hook_taxonomy(),当词汇表变更时你都会收到相应的通知。下面的模块实现了hook_taxonomy(),当词汇表变更时,你将会收到电子邮件通知。下面是taxonomymonitor.info文件:
 
; $Id$
name = Taxonomy Monitor
description = Sends email to notify of changes to taxonomy vocabularies.
package = Pro Drupal Development
dependencies[] = taxonomy
core = 6.x
 
    下面是taxonomymonitor.module:
 
<?php
// $Id$
 
/**
 * Implementation of hook_taxonomy().
 *
 * Sends email when changes to vocabularies or terms occur.
 */
function taxonomymonitor_taxonomy($op, $type, $array = array()) {
    $to = 'me@example.com';
    $name = check_plain($array['name']);
 
    // $type is either 'vocabulary' or 'term'.
    switch ($type) {
        case 'vocabulary':
            switch($op) {
                case 'insert':
                    $subject = t('Vocabulary @voc was added.', array('@voc' => $name));
                    break;
                case 'update':
                    $subject = t('Vocabulary @voc was changed.', array('@voc' => $name));
                    break;
                case 'delete':
                    $subject = t('Vocabulary @voc was deleted.', array('@voc' => $name));
                    break;
            }
            break;
        case 'term':
            switch($op) {
                case 'insert':
                    $subject = t('Term @term was added.', array('@term' => $name));
                    break;
                case 'update':
                    $subject = t('Term @term was changed.', array('@term' => $name));
                    break;
                case 'delete':
                    $subject = t('Term @term was deleted.', array('@term' => $name));
                    break;
            }
    }
 
    // Dump the vocabulary or term information out and send it along.
    $body = print_r($array, TRUE);
 
    // Send the email.
    watchdog('taxonomymonitor', 'Sending email for @type @op',
        array('@type' => $type, '@op' => $op));
    drupal_mail('taxonomymonitor-notify', $to, $subject, $body);
}
 
    对于一些额外的功能,比如将变更者的名字也包含进来,你可以修改这一模块来实现自己的需求。

Drupal版本:

Drupal专业开发指南 第14章 常见任务

老葛的Drupal培训班 Think in Drupal

下面是一些常见任务, 在你使用分类时可能会碰到。

 
在一个节点对象中查找分类术语
通过在taxonomy.module中实现hook_nodeapi(),在node_load()期间分类术语被加载到节点对象中。实际负责从数据库中取回术语的是taxonomy_node_get_terms()函数。这将生成一个包含术语对象的数组,它放在节点的taxonomy键中:
 
print_r($node->taxonomy);
 
Array (
[3] => stdClass Object (
[tid] => 3
[vid] => 1
[name] => Vancouver
[description] => By Land, Sea, and Air we Prosper.
[weight] => 0 )
)
 
构建你自己的分类查询
如果你需要生成一个某种类别的节点列表,你可能希望一切都简单一些;你可能希望Drupal将分类术语保存在node表中,那样你就可以使用下面的SQL了:
 
SELECT * FROM node WHERE vocabulary = 1 and term = 'cheeseburger'
 
灵活性是有代价的,Drupal的分类很灵活,但是Drupal开发者在使用时需要多做一些工作。在Drupal中你不能使用这么简单的查询,你必须学习使用JOIN来查询分类数据库表。
 
使用taxonomy_select_nodes()
 
在你开始编写一个查询以前,你需要考虑一下,使用一个已有的函数是否也能够得到你想要的。例如,如果你想得到标签为术语ID 5和6的节点的标题,你可以使用taxonomy_select_nodes():
 
$tids = array(5, 6);
$result = taxonomy_select_nodes($tids, 'and');
$titles = array();
while ($data = db_fetch_object($result)) {
    $titles[] = $data->title;
}

Drupal版本:

Drupal专业开发指南 第14章 分类函数

老葛的Drupal培训班 Think in Drupal

下面的部分将解释在你的模块可能用到的分类常用函数。
 
取回词汇表的相关信息
下面所列的内置函数用来取回词汇表的相关信息,返回的可以是词汇表数据对象或者是包含这样对象的数组。
 
taxonomy_ vocabulary_load($vid)
这个函数取回单个词汇表(参数$vid是该词汇表的ID),返回的是一个词汇表对象。它还在内部缓存了词汇表对象,这样多次调用同一个词汇表并不费事。这个加载函数在Drupal的菜单系统中也非常有用(详细可参看第4章)。
 
taxonomy_get_vocabularies($type)
taxonomy_get_vocabularies($type)函数取回所有的词汇表对象。参数$type将取回的词汇表限定在给定的节点类型上;比如,blog。这个函数返回一个词汇表对象数组。
 

Drupal版本:

Drupal专业开发指南 第14章 添加、修改、和删除词汇表

老葛的Drupal培训班 Think in Drupal

下面的函数用于创建、修改、和删除词汇表。它们返回一个状态代码,代码为Drupal常量SAVED_UPDATED、SAVED_NEW、或SAVED_DELETED中的一个。
 
taxonomy_save_vocabulary(&$vocabulary)
这个函数用来创建一个新的词汇表或者更新一个已有的词汇表。参数$vocabulary是一个关联数组(注意它不是一个词汇表对象!),包含以下键:
 
• name: 词汇表的名字。
 
• description: 词汇表的描述。
 
• help: 一个帮助文本,在节点创建表单中,它显示在这个词汇表字段的下面。
 
• nodes: 一个数组,包含了这个词汇表适用的所有节点类型。
 
• hierarchy: 0表示没有层级,1表示单个层级,2表示多个层级。
 
• relations: 0表示禁用相关术语, 1表示允许相关术语。
 
• tags: 0表示禁用自由标签化, 1表示启用自由标签化。
 
• multiple: 0表示禁用术语的多选,1表示启用多选。
 
• required: 0表示节点被提交前术语是可选的(这将引入一个默认的“没有选择任何选项”术语), 1表示节点提交前术语是必选的。
 
• weight: 词汇表的重量;它影响它在节点提交表单的词汇表字段集里面的位置。
 
• module: 负责这个词汇表的模块的名字。如果没有传递这个键,那么它的值默认为taxonomy
 
• vid: 词汇表ID。如果没有传递这个键,那么将创建一个新的词汇表。
 
taxonomy_save_vocabulary(&$vocabulary)函数返回SAVED_NEW 或者 SAVED_UPDATED。
 
taxonomy_del_vocabulary($vid)
这个函数的参数$vid是词汇表的ID。删除一个词汇表的同时,会删除词汇表中的所有术语,通过为每个术语调用taxonomy_del_term()来删除术语。taxonomy_del_vocabulary($vid)函数返回SAVED_DELETED。

Drupal版本:

Drupal专业开发指南 第14章 取回术语的相关信息

 

下面所列的内置函数用来取回术语的相关信息,返回的一般是对象或者是包含对象的数组。
 
taxonomy_get_term($tid)
这个函数取回一个术语(参数$tid是词语的ID),返回的是一个术语对象。它在内部缓存了术语对象,所以对同一术语的多次调用并不费事。术语对象的结构如下所示:
 
$term = taxonomy_get_term(5);
var_dump($term);
object(stdClass)#6 (5) {
    ["tid"] => string(1) "3"
    ["vid"] => string(1) "1"
    ["name"]=> string(9) "Vancouver"
    ["description"]=> string(32) "By Land, Sea, and Air we Prosper"
    ["weight"]=> string(1) "0"
}
 
taxonomy_get_term_by_name($text)
taxonomy_get_term_by_name($text)函数搜索匹配一个字符串(参数$text是一个字符串)的所有术语。$text中的空格将被除去,而匹配操作使用的查询为WHERE LOWER(t.name) = LOWER($text)。这个函数返回一个包含术语对象的数组。
 
taxonomy_node_get_terms($nid, $key)
这个函数查找与一个节点相关的所有术语。参数$nid是节点的ID,就是为该节点取回术语的;而参数$key默认为tid,它有点难以理解,它影响结果返回的方式。taxonomy_node_get_terms($nid, $key)返回一个数组的数组,以$key为键。因此,结果数组在默认情况下是以术语ID为键的,但是你也可以替换为term_data表(tid, vid, name, description, weight)中的任意一列。这个函数会为每个节点在内部缓存结果。
 
提示 对于传递进来的节点,这里只用到了它的属性$node->vid。如果你知道节点(为其取回术语)的版本ID,那么你可以使用一个伪造的带有$vid属性的节点对象,这样就避免调用费事的node_load()函数了,例如,$fake_node = new stdClass(); $fake_node->$vid = 12;
$terms = taxonomy_node_get_terms($fake_node);。
 
taxonomy_node_get_terms_by_vocabulary($nid, $vid, $key)
这个函数将在一个词汇表($vid)中查找与一个节点($nid)相关的所有术语。更多信息,可参看taxonomy_node_get_terms($nid, $key)中参数$key的描述。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第14章 添加,修改,和删除术语

老葛的Drupal培训班 Think in Drupal

下面的函数用于创建、修改、和删除术语。它们返回一个状态代码,代码为Drupal常量SAVED_UPDATED、SAVED_NEW、或SAVED_DELETED中的一个。
 
taxonomy_save_term(&$term)
    这个函数创建一个新的术语或者更新一个已有的术语。参数$term是一个关联数组(注意它不是一个术语对象!),包含以下键:
 
• name: 术语的名字。
 
• description: 术语的描述。Drupal的默认用户界面中用不到该值,但是可能在你的模块中或者其它的第3方模块中用到它。
 
• vid: 术语所在词汇表的ID。
 
• weight: 术语的重量。它影响术语下拉选择框中各个术语的相对位置。
 
• relations: 一个可选的数组,包含了相关术语的ID。
 
• parent: 既可以是一个字符串,用来代表父术语的术语ID;也可以是一个数组,其元素既可以是字符串,也可以是子数组,其中字符串为用来代表父术语的术语ID,子数组则是包含这样字符串的数组。
 
• synonyms: 一个可选的字符串,包含了以换行字符(\n)分隔的同义词。
 
• tid: 术语的ID。如果没有传递这个键,那么将创建一个新的术语。
 
    这个函数返回SAVED_NEW 或者 SAVED_UPDATED。
 
taxonomy_del_term($tid)
taxonomy_del_term($tid)函数用来删除一个术语;参数$tid是术语ID。如果一个术语是在一个分层级的词汇表中,并且具有子术语,那么子术语也将被删除,除非子术语拥有多个父术语。

Drupal版本:

Drupal专业开发指南 第14章 取回术语层级的相关信息

老葛的Drupal培训班 Think in Drupal

当使用分层级的词汇表时,下面的函数会非常有用。
 
taxonomy_get_parents($tid, $key)
这个函数查找一个术语的上一层父亲;参数$tid是术语ID。参数$key默认为tid,它是term_data表(tid, vid, name,description, weight)中的一列。taxonomy_get_parents($tid, $key)返回一个包含术语对象的关联数组,以$key为键。
 
taxonomy_get_parents_all($tid)
这个函数找出一个术语的所有祖先;参数$tid是术语ID。这个函数返回一个包含术语对象的数组。
 
taxonomy_get_children($tid, $vid, $key)
taxonomy_get_children($tid, $vid, $key)函数查找一个术语的所有孩子。参数$tid是术语ID。参数$vid是可选的;如果一个词汇表的ID传了进来,那么该术语的孩子将被限制在这个词汇表中(注意,只有当术语有多个父亲,而父术语又位于不同的词汇表中时,这才有用,但是这种情况很少见)。参数$key默认为tid,它是term_data表(tid, vid, name,description, weight)中的一列。这个函数返回一个包含术语对象的关联数组,其中以$key为键。
 
taxonomy_get_tree($vid, $parent, $depth, $max_depth)
这个函数生成一个词汇表的分层级的表示。参数$vid就是词汇表ID,就是为词汇表生成术语树的。如果你不想为一个词汇表生成一个完整的术语树,而只想生成一个以$parent术语ID为根的子树,那么你可以声明参数$parent。参数$depth是内部使用的参数,默认为-1。参数$max_depth是一个整数,用来指出返回的树的层次数,它默认为NULL,这意味着所有的层次。这个函数返回了一个包含术语对象的数组,这里向术语对象添加了depthparents键。depth键是一个整数,用来指示术语在树中所处的层次;而parents键是一个数组,包含了术语父亲的术语ID。让我们以表14-3所列的词汇表为例,它的词汇表ID为2,让我们获得它的结果:
 
$vid = 2;
print_r($taxonomy_get_tree($vid));
 
结果如下:
Array (
    [0] => stdClass Object (
        [tid] => 1
        [vid] => 2
        [name] => Canada
        [description] => A mari usque ad mare.
        [weight] => 0
        [depth] => 0
        [parents] => Array (
            [0] => 0 )
    )
    [1] => stdClass Object (
        [tid] => 4
        [vid] => 2
        [name] => Ontario
        [description] => Ut incepit fidelis sic permanet.
        [weight] => 0
        [depth] => 1
        [parents] => Array (
            [0] => 1 )
    )
    [2] => stdClass Object (
        [tid] => 5
        [vid] => 2
        [name] => Toronto
        [description] => Diversity Our Strength.
        [weight] => 0
        [depth] => 2
        [parents] => Array (
            [0] => 4 )
    )
    [3] => stdClass Object (
        [tid] => 2
        [vid] => 2
        [name] => British Columbia
        [description] => Splendor sine occasu.
        [weight] => 0
        [depth] => 1
        [parents] => Array (
            [0] => 1 )
    )
    [4] => stdClass Object (
        [tid] => 3
        [vid] => 2
        [name] => Vancouver
        [description] => By Land, Sea and Air We Prosper.
        [weight] => 0
        [depth] => 2
        [parents] => Array (
            [0] => 2 )
    )
)
 

Drupal版本:

Drupal专业开发指南 第14章 取回术语同义词的相关信息

 

如果你的模块需要实现对同义词的支持,那么下面的函数对你将会有所帮助。
 
taxonomy_get_synonyms($tid)
使用这个函数为一个给定的术语取回包含其同义词的数组。参数$tid是术语ID。函数返回一个字符串数组;每个字符串就是术语的一个同义词。
 
taxonomy_get_synonym_root($synonym)
给参数$synonym一个字符串,这个函数将执行一个针对term_synonym表的精确匹配查询。它返回单个术语对象,表示使用该同义词找到的第一个术语。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第14章 查找带有特定术语的节点

老葛的Drupal培训班 Think in Drupal

有时,你想有一种简单的方式,来查询具有特定术语的节点,或者输出这样查询的结果。下面的函数可以帮你实现这一点。
 
taxonomy_select_nodes($tids, $operator, $depth, $pager, $order)
这个函数,将基于给定参数构建一个数据库查询,并执行该查询,来查找匹配特定条件的节点。它返回一个指向查询结果的资源标识符。参数$tids是一个包含术语ID的数组。参数$operator是“or”或者“and”,它用来声明如何解释$tids数组。参数$depth指示在分类树中要遍历的层次深度,默认为0,意味着“不要对$tids中术语的任何孩子进行查找”。将$depth设为1,意味着对$tids中的术语以及它们的第一代子术语进行查找。将$depth设为all,意味着对$tids中术语以及它们的所有子术语进行查找。参数$pager是一个布尔值,用来指示是否要对生成的节点列表使用分页器,它默认为TRUE。如果你要生成一个XML种子,那么你可以将$pager设置为FALSE。参数$order包含了字面上的order语句,用在查询的SQL中,默认为“n.sticky DESC, n.created DESC”。
如果你需要对许多术语进行搜索,那么这个函数是非常耗费数据库资源的。
 
taxonomy_render_nodes($result)
如果你使用taxonomy_select_nodes()来查询匹配特定分类条件的节点,那么我们在这里就可以把taxonomy_render_nodes()作为一个起点,来为你的查询创建一个简单的输出。
 

Drupal版本:

Drupal专业开发指南 第14章 额外资源

老葛的Drupal培训班 Think in Drupal

许多模块在许多地方都使用了分类,比如添加访问控制(http://drupal.org/project/taxonomy_access),动态类别浏览((http://drupal.org/project/taxonomy_browser),在一个区块中根据分类术语显示相关的节点(http://drupal.org/project/similarterms)。Drupal参考手册包含了关于分类的更多信息,http://drupal.org/handbook/modules/taxonomy。分类相关模块的列表,可参看http://drupal.org/project/Modules/category/71
 
    我们鼓励你使用views模块,特别是在主题化分类列表时(http://drupal.org/project/views
 

Drupal版本:

Drupal专业开发指南 第14章 总结

老葛的Drupal培训班 Think in Drupal

读完本章后,你应该能够
• 理解什么是分类
• 理解术语,词汇表,以及它们不同的选项
• 单层的,分层级的,和多层级的词汇表之间的区别
• 构建带有AND 和 OR 操作的URL,来对分类术语进行搜索
• 为分类术语和术语联合体的RSS种子构建URL
• 理解分类是如何存储的
• 知道如何在你自己的模块中使用词汇表
• 创建你的模块来接收分类变更的通知

Drupal版本:

Drupal专业开发指南 第14章 中英文对照

Taxonomy: 分类,分类系统, 分类法

Category: 类别
Categories: 不同的类别
Term:术语
Review:评估
Bad:劣
OK:合格
Excellent:优
Tag:标签
Tagging:标签化。
Level:【计算机科学】 层次
Poor:差
Synonym:同义词
contributed module3方模块
glossary:术语表
vocabulary:词汇表
Controlled Vocabularies:受控的词汇表
administer taxonomy:管理分类
“None selected”: “没有选择任何选项”
Multiple select:多重选择
Parents:上层,父亲
parent term:上一层术语
Related Terms:相关术语
Flat:单层
Hierarchical:分层级的

Hierarchy:层级

 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第15章 缓存

老葛的Drupal培训班 Think in Drupal

在为动态网站构建页面时,需要对数据库进行大量的查询操作,以获取相关信息比如保存的内容、站点设置、当前用户等等。将这些耗费资源的操作的结果保存起来以备后用,这是从应用层提升站点性能的最佳途径之一。这里被保存的不仅仅是数据库调用,在PHP中对取回的信息所作的处理也被保存了起来。Drupal内置的缓存API对大部分核心数据进行了自动缓存,并为Drupal开发者提供了一组工具以进行量身定做。例如,memcache模块(http://drupal.org/project/memcache)就是一个基于内存缓存的例子,它就使用了Drupal的缓存API。
 
注意 本章所讲的是Drupal应用层中的缓存。其它层的缓存,比如数据库的内部缓存(例如,MySQL的查询缓存),也能极大地提升性能。在第22章中提到了这些方面。
 

Drupal版本:

Drupal专业开发指南 第15章 什么时候使用缓存

老葛的Drupal培训班 Think in Drupal

有一点需要记住,那就是使用缓存时要进行权衡。对大量的数据进行缓存,可对性能有很大提升,这是不假,但这是有前提的,前提就是被缓存的数据在接下来能被重复使用。这就是为什么Drupal内置的页面缓存仅用于匿名用户----注册用户通常需要的是页面的定制版本,这样缓存的效果就不太明显。对小量的数据进行缓存(比如今天的流行文章列表),尽管对你网站性能的提升不是很大,但是也会有所改善。
     另外要讲的是,对不经常变动的数据使用缓存效果会更好。比如,对于每周热门故事列表,就比较适用。如果对于一个繁忙的论坛,缓存最新的5条评论,那么效果就不太明显,因为被缓存的数据很快就会过期,还没有多少用户使用到它呢,它就需要更新了。在最坏的情况下,一个坏的缓存策略(比如,对变动过于频繁的数据进行缓存)可能会增加网站的负担,而不是提升性能。
 

Drupal版本:

Drupal专业开发指南 第15章 缓存是如何工作的

     模块常常需要进行昂贵的数据查询或者调用远程web服务。对于这些耗费资源的操作,不需要每次都重复进行一次,模块可以将它们的数据缓存到Drupal中为缓存保留的数据库表中,或者模块也可以创建一个新的数据库表,并将缓存数据存储在那里。当下次用到这些数据时,通过一个简单的查询就可以快速的将其取回。你将在本章的后面看到,Drupal的缓存后端是可插拔的,所以尽管这里所指的是数据库表,实际的后端也可以采用其它的存储,比如直接使用文件或者一个基于内存的缓存。

     在你的模块中,你可以存储缓存信息的默认数据库表是cache。当缓存信息的数据量不大时,最好使用该表。如果你想为每个节点、菜单、用户缓存信息,那么你将需要为你的模块创建一个它自己专有的缓存表。这样就可以减小Drupal的cache表中的记录数量,以及减少写入冲突,从而提升性能。当要为你的模块创建一个新的缓存表时,该表的数据结构一定要与默认的cache表的结构完全相同,这里只有表的名字不同。为了保持一致性,最好在表的名字前面加上前缀cache_。让我们看一下cache表的数据库结构;参看表格15-1.
 
注意 当为你的模块创建一个新的缓存表时,该表的数据结构一定要与默认的cache表的结构完全相同。
 
表15-1 cache表的模式
字段*          类型           Null       默认值
cid            varchar(255)    NO          —
data            longblob        YES         —
expire        int             NO          0
created         int             NO          0
headers         text            YES         NULL
serialized     smallint        NO          0
*粗体表示主键;斜体表示索引字段
 
       cid列,存储的是主缓存ID,用于快速取回缓存信息。在Drupal核心中使用缓存ID的例子有,对于页面缓存使用的是页面的URL(比如,http://example.com/?q=node/1),对于主题注册表缓存使用的是一个字符串和一个主题名字(例如,theme_registry:garland),或者甚至可以使用规则的字符串(比如, variables表中内容的缓存,其缓存ID设置为了variables)。这里的重点是,对于正被缓存的项目来说,缓存ID必须是一个唯一的标识符。
       data列,存储的是你想要缓存的信息。对于复杂的数据类型比如数组或者对象,需要使用PHP的serialize()函数进行序列化,从而将其数据结构也保存到数据库中。(Drupal自动帮你实现了这一点)。
       expire列,使用下面的3个值中的一个:
 
       CACHE_PERMANENT:该项目永远不会被删除,只有当调用cahe_clear_all()并明确给出持久化项目的缓存ID时,才会被删除。
 
       CACHE_TEMPORARY:这意味着当下一次不带参数的调用cahe_clear_all()时,该项目就会被删除,而没有最小时间限制。标记为CACHE_PERMANENT的项目此时不会被删除。
 
       一个Unix时间戳:指的是该项目的最小存在时间,在这个最小时间内,该项目就不会被删除,当过了这个时间,它就和标记为CACHE_TEMPORARY的项目一样了。
 
       created列,是一个Unix时间戳,指的是缓存条目创建的日期。
       headers列,用来存储HTTP的响应头部,在缓存数据是一个完整的Drupal页面请求时使用。大多数时候,你不使用这一字段,这是因为你要缓存的是页面的一部分而不是整个页面,也就是说要缓存的数据不依赖于头部。记住,尽管如此,你自定义的缓存表的结构仍然需要和默认的cache表的结构完全相同,所以尽管没有用到它,也要保留它。
    serialized列,指的是data列中的数据是否处于序列化的形态。0表示未序列化的数据,1表示序列化的数据。如果数据被序列化,并且serialized列的值为1,那么缓存系统在将数据返回给调用者以前,会对其进行反序列化。对于对象、数组这种类型的数据,在被缓存时,缓存系统能将其自动的序列化并将serialized列设置为1。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第15章 缓存在Drupal核心中的应用

 

Drupal默认自带了6个缓存表:cache存储了variables表和数据库模式和主题注册表的一个拷贝;cache_block存储了区块的缓存拷贝;cache_menu存储了导航菜单的缓存拷贝;每个节点的内容在被过滤器系统解析后,其缓存拷贝将被存储在cache_filter表中;cache_form用在表单API中,在可能的情况下,用于避免表单的重新构建;cache_page为匿名用户存储了页面的缓存拷贝。我们在接下来的部分中,将会逐一的讲解这些缓存。需要注意的是,在管理界面“管理➤站点配置 ➤性能”中的“页面缓存”和“区块缓存”设置,它们仅仅影响页面缓存和区块缓存表,对于Drupal中的其它缓存部分不起作用。换句话说,过滤器,菜单,和模块设置总是被缓存的。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第15章 菜单系统

 

菜单系统缓存了连接Drupal路径与回调的路由信息。无论是否启用了Drupal的页面缓存,由菜单模块创建的菜单都会被缓存。所以为了清除菜单缓存,可以使用“管理➤站点配置 ➤性能”页面的“清除缓存数据”按钮,或者调用menu_cache_clear_all()。如果你对菜单的修改影响到了区块,你可能需要调用更厉害的menu_rebuild()函数;当菜单被重新构建时,菜单缓存也会被清除。菜单的例子有,Drupal的一级和二级链接,以及用户导航区块。菜单基于单用户、单地域进行缓存。关于菜单系统的更多信息,可参看第4章。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第15章 过滤的输入格式

老葛的Drupal培训班 Think in Drupal

当创建或者编辑一个节点时,节点的内容将经过与其输入格式相关的各种过滤器的处理。例如,HTML过滤器格式将换行转换为了HTML<p>和<br>标签,同时还过滤掉恶意的HTML。如果每次查看一个节点时都进行过滤,那么这会浪费很多资源。因此,只有在刚刚创建或编辑完节点以后才对其应用过滤器(其它时候不用),并将过滤后的内容缓存到cache_filter数据库表中,这一缓存不受Drupal页面缓存是否启用的影响。关于输入格式的更多信息,可参看第11章。
 
提示 当你使用管理界面修改节点摘要的默认长度时,只有当你重新保存每个节点以后才会生效,其原因就在于过滤器缓存。解决该问题的简便方法就是清空cache_filter表,这样所有的节点内容将被重新解析,摘要被重新构建。或者,你可能想清除所有的缓存(包括过滤器缓存),那么可以使用“管理➤站点配置 ➤性能”页面的“清除缓存数据”按钮。
 

Drupal版本:

Drupal专业开发指南 第15章 管理变量和模块设置

Drupal将大多数的管理设置存储在variables表中,并将该表的所有数据缓存到cache表中,以加快查找配置数据的速度。这类变量的例子,包括你站点的名称、评论和用户的设置、以及files目录的位置。所有的这些变量都被缓存到cache表的一行记录当中了,这样在需要每个变量时,就可以快速的将其取回,而不是每次都进行一次数据库查询。它们被存储为一个PHP数组,所以缓存值将被序列化从而保留它的结构。如果一个变量使用variable_set()和variable_get()作为设置器(setter)和读取器(getter)函数,那么将以这种方式来存储和缓存它。

 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第15章 页面

 

我们在前面讨论了很多,不过都是对站点的耗费资源的构件进行缓存,但是Drupal最大的优化是缓存整个页面视图。对于匿名用户,很容易做到这一点,这是因为所有页面对于所有匿名用户都是相同的。然而,对于登录用户,每个页面都是针对用户量身定做的。因此需要采用不同的缓存策略来处理这种情况。
    对于匿名用户,Drupal可以使用单个查询来取回缓存的页面内容,当然它还需要一些其它查询以加载Drupal本身。对于匿名用户的页面缓存,有两种缓存策略供你选择:普通模式和激进模式。当然你也可以禁用页面缓存。通过设置最小缓存有效期,你可以进一步的修改普通模式和激进模式策略。这些设置可以在Drupal管理界面“管理➤站点配置 ➤性能”中找到。该界面如图15-1所示。在接下来的部分中,让我们学习一下每种设置。
15-1.控制页面缓存行为的管理界面
 
禁用
    这将完全禁用页面缓存。通常在开发网站时使用。一般情况下,你都需要启用页面缓存。
 
注意 即使禁用了页面缓存,Drupal仍然缓存用户菜单、过滤后的内容、主题注册表、数据库模式、系统变量。这些构件级别的缓存不会被禁用。
 
普通模式
    与完全不使用缓存相比,普通模式对性能会有巨大提升,因此它是提升一个缓慢的Drupal站点性能的最简单方式之一。让我们仔细的看一下,当启用了普通模式缓存系统时,请求的生命周期。
    为了理解普通模式的页面缓存,你首先需要了解Drupal的引导指令流程。引导指令流程是由各个阶段组成的,这里的阶段就是一些更小的独立的步骤。Drupal利用了这个阶段性的引导指令系统,在提供一个缓存页面时,只需要加载和解析所需的代码,对于无关的代码,则不需要对其进行加载和解析,这样就将数据库查询降到了最小。
    图15-2详细给出了为匿名用户的请求提供一个缓存页面的流程。
15-2 本图展示了当处于Drupal的普通模式缓存设置下,为匿名用户提供缓存页面的请求生命周期。引导指令流程的前面5个阶段与缓存无关,在这里将其加进来是为了保持完整性。*表示一个数据库查询;**表示在该点的数据库查询次数是未知的。
 
    首先,请求进来以后,Web服务器就得执行index.php。在index.php中的第一行PHP代码,就是用来包含文件includes/bootstrap.inc的,该文件包含了加载引导指令的核心函数。接着,index.php会调用drupal_bootstrap()。
    drupal_bootstrap()负责执行每一个引导指令阶段。对于普通模式缓存,我们只需要关心DRUPAL_BOOTSTRAP_LATE_PAGE_CACHE引导指令阶段就可以了。在该阶段,首先会从数据库中取回系统变量。假定缓存策略是普通模式,接下来的一步就是包含文件includes/module.inc。module.inc内部的函数是用来允许Drupal激活模块系统的。接着,Drupal将初始化实现了hook_boot()或者hook_exit()的模块。通过分别调用bootstrap_invoke_all(‘boot’)和bootstrap_invoke_all(‘exit’)来激活这些钩子。例如,统计模块,使用statistics_ exit()函数来追踪页面访问。节流阀模块使用throttle_exit()函数,根据当前的访问量来修改节流阀级别。
 
注意 在一个模块中使用hook_ boot ()或者hook_exit()函数,会为整个站点的性能带来负担,这是由于在处于普通缓存模式下,为访问者提供每个缓存页面时,都需要加载你的模块。当你实现这些钩子时,可用的函数也会受到限制,因为此时还没有加载includes/common.inc。常用函数比如t(),l(),和pager_query()此时都不可用。
 
    Drupal_page_cache_header()通过设置HTTP头部来准备缓存数据。Drupal将把Etag和304头部设置为适当的,这样浏览器就可以使用它们自己的内部缓存机制了,在适当的时候可以避免不必要的HTTP循环请求。如果浏览器发送的头部信息已经请求过它,那么缓存的数据将被发送给浏览器。
 
激进模式
    激进模式完全绕开了对所有模块的加载(如图15-3所示)。这意味着,此时不再为缓存页面调用boot(引导)和exit(退出)钩子了。因为不需要加载模块,所以需要解析的PHP代码少了。而需要执行的数据库查询也少了。如果你启用的模块使用了这些钩子(比如统计模块和节流阀模块),那么在激进模式下,它们可能不会正常工作。在管理界面“管理➤站点配置 ➤性能”,Drupal会给出警告,指出哪些模块可能会受到影响。
 
15-3 当处于Drupal的激进模式缓存设置下,为匿名用户提供缓存页面的请求生命周期,*表示一个数据库查询

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第15章 最小缓存有效期

 

这个设置用来控制你站点上的缓存内容的有效期。当一个用户提交了新的内容时,他/她将能立即看到变化;然而,其他用户只有在过了最小的缓存有效期以后,才能看到新的内容。当然,如果将最小缓存有效期设置为“无”,那么每个人都能立即看到新的内容。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第15章 Fastpath:隐藏的缓存设置

 

fastpath缓存设置不能通过Drupal管理界面进行配置,这是因为它的特性非常高级;fastpath使得程序员能够绕开Drupal,来实现一个高度定制化的缓存方案,比如内存或者基于文件的缓存(参看图15-4).
15-4 当处于Drupal的fastpath模式缓存设置下,为匿名用户提供缓存页面的请求生命周期
 
    第3方模块cacherouter (http://drupal.org/project/cacherouter)就是一个利用fastpath模式的模块。假定你把这个模块安装在了sites/all/modules/contrib中。
    由于fastpath在默认情况下不会创建数据库连接,所以要将所有的配置选项放在你的settings.php文件中:
 
$conf = array(
    'page_cache_fastpath' => TRUE,
    'cache_inc' => './sites/all/modules/contrib/cacherouter/cacherouter.inc',
    ... // More settings here.
);
 
    数组中的第一项,通过将page_cache_fastpath设置为TRUE来启用fastpath模式。这样就启用了fastpath模式,非常简单!第2项用来声明Drupal将要加载的文件,将会使用这个文件来代替includes/cache.inc。在这种情况下,声明的文件就是cacherouter模块将要使用的自定义缓存库。cacherouter模块还需要更多一点的配置;详细可参看http://drupal.org/project/cacherouter
    如果你要加载自己的自定义缓存库,来代替Drupal默认使用的includes/cache.inc库,那么你需要编写你自己的cache_set(),cache_get()和cache_clear_all()函数。
 
注意 一旦启用了fastpath缓存,那么它将覆写在Drupal管理界面中设置的任意缓存选项。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第15章 区块

老葛的Drupal培训班 Think in Drupal

根据它们的内容,区块也可以被缓存。使用管理界面“管理➤站点配置 ➤性能”,可以启用或者禁用Drupal的区块缓存(参看图15-5)。
 
15-5. 用来控制区块缓存行为的管理界面
 
    当一个模块实现hook_block()时,可以在list操作下,声明区块的缓存能力,这样就能实现区块缓存了。例如,下面是modules/user/user.module的hook_block()实现中的部分代码:
 
function user_block($op = 'list', $delta = 0, $edit = array()) {
    global $user;
 
    if ($op == 'list') {
        $blocks[0]['info'] = t('User login');
        // Not worth caching.
        $blocks[0]['cache'] = BLOCK_NO_CACHE;
 
        $blocks[1]['info'] = t('Navigation');
        // Menu blocks can't be cached because each menu item can have
        // a custom access callback. menu.inc manages its own caching.
        $blocks[1]['cache'] = BLOCK_NO_CACHE;
 
        $blocks[2]['info'] = t('Who\'s new');
 
        // Too dynamic to cache.
        $blocks[3]['info'] = t('Who\'s online');
        $blocks[3]['cache'] = BLOCK_NO_CACHE;
        return $blocks;
    }
    ...
}
 
    在前面的例子中,除了一个区块以外,用户模块提供的所有区块都声明了它们不应该被缓存。“新进用户”区块没有声明缓存选项,这意味着如果管理员启用了区块缓存,接着又启用“新进用户”区块,那么该区块将使用默认缓存设置BLOCK_CACHE_PER_ROLE。这意味着为每一个角色存储一个区块的单独的缓存版本。更精确的说,将为角色的每个联合体存储一个单独的缓存版本;通过把当前用户的角色ID连接起来(参看modules/block/block.module中的_block_get_cache_id()),来创建缓存ID。可以用于区块缓存的常量,参看表15-2
 
15-2.区块缓存的常量
常量                 值     含义
BLOCK_NO_CACHE          -1      这个区块不被缓存。
BLOCK_CACHE_PER_ROLE    1       每个角色看到一个单独的缓存区块。*
BLOCK_CACHE_PER_USER    2       每个用户看到一个单独的缓存区块。
BLOCK_CACHE_PER_PAGE    4       每个页面都有它自己的缓存区块。
BLOCK_CACHE_GLOBAL      8       为所有用户只缓存一次区块。
* 没有为区块声明缓存设置时的默认值
 
    所有被缓存的区块,都是基于单个主题和单个语言进行缓存的。这意味着,当启用了多个主题时,在用户当前使用的主题下,不会出现为其它主题缓存的区块;当启用了多个语言时,在用户当前使用的语言下,不会出现为其它语言缓存的区块。
 
注意 对于超级用户(用户1),区块永远不被缓存。
 
    通过使用PHP的位逻辑运算符,可以联合使用多个区块常量(类似于菜单常量)。例如,书籍模块的hook_block()提供的“书籍导航”区块,就同时使用了BLOCK_CACHE_PER_ROLE和BLOCK_CACHE_PER_PAGE:
 
function book_block($op = 'list', $delta = 0, $edit = array()) {
    $block = array();
    switch ($op) {
        case 'list':
            $block[0]['info'] = t('Book navigation');
            $block[0]['cache'] = BLOCK_CACHE_PER_PAGE | BLOCK_CACHE_PER_ROLE;
            return $block;
...
}
    不能为BLOCK_CACHE_PER_ROLE和BLOCK_CACHE_PER_USER常量使用位逻辑运算符(|),这是因为这两种缓存模式是相互排斥的。

Drupal版本:

Drupal专业开发指南 第15章 静态变量缓存

许多Drupal函数使用一个静态变量来缓存数据。在一个HTTP请求的生命周期内,在第2次调用它时,将会立即返回数据。下面是一个来自于节点模块的例子:

 
function node_get_types($op = 'types', $node = NULL, $reset = FALSE) {
    static $_node_types, $_node_names;
 
    if ($reset || !isset($_node_types)) {
        list($_node_types, $_node_names) = _node_types_build();
    }
    ...
}
    缓存总是有代价的。静态变量缓存的代价就是内存。幸运的是,与数据库CPU循环相比,内存一般还是很富裕的。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第15章 使用缓存API

 

对于模块开发者来说,如果他们想使用缓存API的话,那么就需要掌握两个函数:cache_set()和cache_get()。
 
使用cache_set()缓存数据
    cache_set()用来将数据写入到缓存中。函数签名如下:
 
cache_set($cid, $table = 'cache', $data, $expire = CACHE_PERMANENT, $headers = NULL)
 
函数参数有:
 
    $cid:唯一的缓存ID字符串,用作缓存数据的键。使用冒号来定界可能的层级。
 
$table:用来存储数据的表的名字。你可以创建你自己的表,或者使用cache,cache_block, cache_filter,cache_form,cache_menu, cache_page。默认使用cache表。
 
$data:存储在缓存中的数据。PHP对象和数组将被自动序列化。
 
$expire:缓存数据的有效期的时长。可能值有CACHE_PERMANENT,CACHE_TEMPORARY,或者一个Unix时间戳。如果给定了一个Unix时间戳,并且当前时间超过了该时间戳,那么该数据的处理方式将和标记为CACHE_TEMPORARY的是一样的。
 
$headers:用于缓存的页面,传递给浏览器的HTTP头部字符串。
 
modules/filter/filter.module中,有一个cache_set()示例:
 
// Store in cache with a minimum expiration time of 1 day.
if ($cache) {
    cache_set($cid, 'cache_filter', $text, time() + (60 * 60 * 24));
}
 
使用cache_get()取回缓存的数据
    cache_get()用来取回缓存的数据。函数签名如下:
 
cache_get($cid, $table = 'cache')
而函数参数有:
 
$cid:要取回的数据的缓存ID。
$table:要取回的数据所在的表的名字。你可以创建你自己的表,或者使用cache,cache_block,cache_filter,cache_form,cache_menu,cache_page。默认使用cache表。
 
modules/filter/filter.module中,有一个cache_get()的示例:
 
// Check for a cached version of this piece of text.
if ($cached = cache_get($cid, 'cache_filter')) {
    return $cached->data;
}
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第15章 清除缓存

 

如果你的模块最了解其数据的失效时间,那么它需要负责在一个适当的时间清除缓存。对于缓存的清除,有两个指导原则:
 
• 尽可能的清除特定的缓存。不要因为特定模块数据的一点修改,来清除所有的Drupal缓存!打个比方来说,如果你家厨房的地板脏了,只需要对其进行清扫就可以了,而不需要把家里的所有地毯都掀了并替换掉。
 
• 要尽可能多的重复利用缓存的数据。尽管缓存的要点是,通过减少所需的工作总量,来增加响应能力,但是在清除缓存数据的时候,也是需要花费很多功夫的,特别是有很多缓存数据的时候。
 
    下面的子部分描述了清除缓存数据的一些方式。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第15章 使用$reset参数

 

许多使用静态变量实现内部缓存的Drupal函数,都有一个可选的$reset参数,用来指示该函数清除它的内部缓存。例如,下面是我们熟知的node_load():
 
function node_load($param = array(), $revision = NULL, $reset = NULL) {
    static $nodes = array();
 
    if ($reset) {
        $nodes = array();
    }
    ...
}

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第15章 使用cache_clear_all()

老葛的Drupal培训班 Think in Drupal

用来清除缓存数据的主函数就是includes/cache.inc中的cache_clear_all()。函数的签名如下:
function cache_clear_all($cid = NULL, $table = NULL, $wildcard = FALSE) {...}
 
    参数$cid和$table的含义,与cache_set()和cache_get()中的一样。而参数$wildcard是用来指示是否对$cid采用通配符的形式进行处理,也就是说,把$cid作为一个子字符串,任何从左边匹配的条目将被清除。下面是一些例子。
 
    清除cache表中的特定条目foo:bar:
 
$cid = 'foo:bar';
cache_clear_all($cid, 'cache');
 
    清除cache表中的由foo模块设置的(假定,$cid都采用foo:前缀)任何过期的条目:
 
$cid = 'foo:'; // Will match cache keys foo:bar, foo:baz, etc.
cache_clear_all($cid, 'cache', TRUE);
 
    在前面的例子中,实际运行的数据库查询如下:
 
db_query("DELETE FROM {". $table ."} WHERE cid LIKE '%s%%'", $cid);
 
    如果foo模块将它的数据保存在了它自己的cache_foo表中,那么需要对该表进行声明,这样cache_clear_all()就知道清除哪个表了:
 
$cid = 'foo:bar';
cache_clear_all($cid, 'cache_foo');
 
    如果你想完全清空一个缓存表,那么可以把$cid设为*,把$wildcard设为TRUE。这个例子将清空整个cache_foo表:
 
cache_clear_all('*', 'cache_foo', TRUE);
 
    清除页面和区块缓存(例如,cache_page和cache_block表)的任何过期条目:
 
cache_clear_all();

Drupal版本:

Drupal专业开发指南 第15章 使用hook_flush_caches()

 

Drupal有一个中心函数,用来清空所有的缓存,包括JavaScript和CSS缓存。这就是来自于includes/common.inc的drupal_flush_all_caches()函数:
 
/**
 * Flush all cached data on the site.
 *
 * Empties cache tables, rebuilds the menu cache and theme registries, and
 * exposes a hook for other modules to clear their own cache data as well.
 */
function drupal_flush_all_caches() {
    // Change query-strings on css/js files to enforce reload for all users.
    _drupal_flush_css_js();
    drupal_clear_css_cache();
    drupal_clear_js_cache();
    system_theme_data();
    drupal_rebuild_theme_registry();
    menu_rebuild();
    node_types_rebuild();
    // Don't clear cache_form - in-progress form submissions may break.
    // Ordered so clearing the page cache will always be the last action.
    $core = array('cache', 'cache_block', 'cache_filter', 'cache_page');
    $cache_tables = array_merge(module_invoke_all('flush_caches'), $core);
    foreach ($cache_tables as $table) {
        cache_clear_all('*', $table, TRUE);
    }
}
 
    注意包含module_invoke_all('flush_caches')的那行代码。这将触发hook_flush_caches()。如果你使用了自己的缓存表,那么当点击“管理➤站点配置 ➤性能”页面的“清除缓存数据”按钮时,通过实现钩子hook_flush_caches(),你的模块就可以清除自己的缓存了。该按钮的提交处理器调用的就是drupal_flush_all_caches()。hook_flush_caches()的实现非常简单;你的模块只需要简单得返回一个数组,里面包含要被清空的缓存表就可以了。下面是来自更新状态模块的例子:
 
/**
 * Implementation of hook_flush_caches().
 */
function update_flush_caches() {
    return array('cache_update');
}

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第15章 总结

 

在本章,你学到:
    Drupal提供的各种缓存类型:页面、区块、菜单、变量、过滤器缓存
    页面缓存系统的工作原理
    普通模式,激进模式和fastpath模式之间的不同之处
    区块缓存系统的工作原理
缓存API函数
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第16章 会话

 

HTTP是一个无状态协议,这意味着浏览器和服务器之间的每次交互都是相互独立的。当一个用户访问一个网站的一系列页面时,你该如何对其进行追踪呢?你可以使用会话。从PHP4开始,通过会话家族函数,PHP提供了对会话的内置支持。在本章,你将看到Drupal是如何使用PHP会话的。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第16章 什么是会话?

老葛的Drupal培训班 Think in Drupal

当一个浏览器第一次请求一个Drupal站点的页面时,PHP会向浏览器发送一个cookie,里面包含了一个随机生成的32位字符串ID,默认称为PHPSESSID。在浏览器第一次访问一个站点时,在返回的HTTP响应头部中包含了一行信息,它就是cookie,如下所示:
 
HTTP/1.1 200 OK
Date: Thu, 17 Apr 2008 20:24:58 GMT
Server: Apache
Set-Cookie: PHPSESSID=3sulj1mainvme55r8udcc6j2a4; expires=Sat, 10 May 200823:58:19
GMT; path=/
Last-Modified: Thu, 17 Apr 2008 20:24:59 GMT
Cache-Control: store, no-cache, must-revalidate
Cache-Control: post-check=0, pre-check=0
Content-Type: text/html; charset=utf-8
 
    在接下来的访问中,浏览器将cookie包含在每个HTTP请求中,用来向服务器出示cookie:
 
GET / HTTP/1.1
User-Agent=Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.14)
Gecko/20080404 Firefox/2.0.0.14
Cookie: PHPSESSID=3sulj1mainvme55r8udcc6j2a4
 

    这样当单个浏览器请求网站页面时,PHP就可以对其进行追踪了。32位字符串ID,也就是会话ID,它有两种功能,首先它可以作为键来读取Drupal存储的会话信息,另外它允许Drupal将会话与单独的用户关联起来。

Drupal版本:

Drupal专业开发指南 第16章 用法

老葛的Drupal培训班 Think in Drupal

Drupal在多个重要的内部函数中,使用会话来存储临时信息,比如单个用户的状态或偏爱。例如,drupal_set_message()需要为用户传递状态消息或者错误消息,将消息从错误发生的页面传递到下一个页面中。在这里,我们将消息存储在用户会话中的一个名为messages的数组里面,从而完成传递。
 
/**
 * Set a message which reflects the status of the performed operation.
 *
 * If the function is called with no arguments, this function returns all set
 * messages without clearing them.
 *
 * @param $message
 * The message should begin with a capital letter and always ends with a
 * period '.'.
 * @param $type
 * The type of the message. One of the following values are possible:
 * 'status', 'warning', 'error'
 * @param $repeat
 * If this is FALSE and the message is already set, then the message won't
 * be repeated.
 */
function drupal_set_message($message = NULL, $type = 'status', $repeat = TRUE) {
    if ($message) {
        if (!isset($_SESSION['messages'])) {
            $_SESSION['messages'] = array();
        }
        if (!isset($_SESSION['messages'][$type])) {
            $_SESSION['messages'][$type] = array();
        }
        if ($repeat || !in_array($message, $_SESSION['messages'][$type])) {
            $_SESSION['messages'][$type][] = $message;
        }
    }
    // Messages not set when DB connection fails.
    return isset($_SESSION['messages']) ? $_SESSION['messages'] : NULL;
}
 
 另一个例子来自于comment.module,在这里使用会话为匿名用户存储查看偏爱:
 
$_SESSION['comment_mode'] = $mode;
$_SESSION['comment_sort'] = $order;
$_SESSION['comment_comments_per_page'] = $comments_per_page;
 
  在预览节点时,Drupal使用会话来处理文件上传。在“管理➤内容管理➤内容”中过滤站点内容列表时,Drupal用会话记住用户浏览偏爱,还有在“管理➤报告➤最新的日志条目”中过滤最新日志条目列表时,用的也是会话。而在安装和更新系统中,也使用了会话(install.php和update.php)。
    对于登录用户和匿名用户,Drupal都为其都创建了会话。在表sessions中有一行表示匿名用户的记录,其中uid列被设置为了0.由于会话是特定于浏览器的(它们绑定在了浏览器的cookie上),这样,在单个计算机上打开多个浏览器就会有多个会话。
 
 
警告 在匿名用户初次访问一个站点时,Drupal并不为其存储会话信息。这样当恶意的网络爬虫和机器人访问站点时,就不会向sessions表中写入大量数据了。对于开发者来说,这意味着你不能为匿名用户的初次访问,存储会话信息。
 
    存储在会话中的实际数据,是作为序列化数据存储在sessions表的session列中的。表16-1给出来一个典型的sessions表,它包含了3条记录。对于超级用户(uid 1),认证用户(uid 3),匿名用户(uid 0),都各有一条记录。对于超级用户,在其会话中,也就是session列,存储了看门狗过滤设置(用在dblog模块)。
 
16-1.来自sessions表的示例记录
uid    sid                      hostname  timestamp cache session
1 f5268d678333a1a7cce27e7e42b0c2e1 1.2.3.4      1208464106 0 dblog_overview_
                                                                filter|a:0:{}
3 be312e7b35562322f3ee98ccb9ce8490 5.6.7.8      1208460845 0  --
0 5718d73975456111b268ed06233d36de 127.0.0.1    1208461007 0  --
 
    当PHP的会话垃圾收集器周期性运行时,就会清理sessions表。记录在表中的保留时限由settings.php中的session.gc.maxlifetime决定。如果用户退出,那么与之相关的会话记录将被立即从数据库中清除。注意,如果一个用户通过多个浏览器(而不是浏览窗口)或者多个IP地址同时登录,那么每个浏览器都有一个会话;因此,从一个浏览器退出不会影响用户在其它浏览器的登录状态。

Drupal版本:

Drupal专业开发指南 第16章 会话相关设置

 

3个地方,可以修改Drupal的会话设置:.htaccess文件, settings.php文件, includes/bootstrap.inc文件中的引导指令代码。
 
在.htaccess文件中
    在Drupal安装的默认文件.htaccess中,使用下面一行代码关闭PHP的session.auto_start功能,这样在会话启动后,Drupal就确保拥有完全的控制能力了。代码如下:
 
php_value session.auto_start                0
 
    session.auto_start是一个配置选项,由于PHP在运行时不能改变它,所以将其放置在.htaccess文件中而不是settings.php中。
 
在settings.php文件中
    在settings.php文件中,你可以设置大部分的会话设置,该文件位于sites/default/
settings.php或者sites/example.com/settings.php。
 
ini_set('session.cache_expire',     200000); // 138.9 days
ini_set('session.cache_limiter',    'none');
ini_set('session.cookie_lifetime', 2000000); // 23.1 days
ini_set('session.gc_maxlifetime',   200000); // 55 hours
ini_set('session.save_handler',    'user'); // Use user-defined session handling.
ini_set('session.use_only_cookies',     1); // Require cookies.
ini_set('session.use_trans_sid',        0); // Don't use URL-based sessions.
 
    将这些设置放在settings.php中而不是.htaccess文件中,这就可以使子站点可以拥有不同的设置,并且在PHP作为CGI运行时(.htaccess中的PHP指令不能在这样的配置中工作),允许Drupal修改会话设置。
    Drupal使用函数ini_set('session.save_handler', 'user');来覆写由PHP提供的默认会话处理器,并实现它自己的会话管理。user-defined在这里的意思是“由Drupal定义的”(参看http://www.php.net/manual/en/function.session-set-save-handler.php)。
 
在bootstrap.inc文件中
    PHP提供了内置的会话处理函数,如果你想实现自己的会话处理器,那么PHP允许你覆写这些函数。这样PHP仍然负责cookie管理,而Drupal的实现则负责后台的会话存储。
    在引导指令中的DRUPAL_BOOTSTRAP_SESSION阶段,下面的代码将处理器设置为了includs/session.inc里面的函数,并启动会话处理:
 
require_once variable_get('session_inc', './includes/session.inc');
session_set_save_handler('sess_open', 'sess_close', 'sess_read', 'sess_write',
'sess_destroy_sid', 'sess_gc');
session_start();
 
    在个别的时候,一个文件内部的函数名可以不使用文件名作为前缀,这里就是这样。你可能会认为前面的函数应该为session_open,session_close等等。然而,由于PHP在该命名空间下已经有了这些函数,所以这里使用了更简短的前缀sess_。
    注意,正被包含进来的文件是通过一个Drupal变量定义的。这意味着你完全可以实现自己的会话处理,并将其插入进来以代替Drupal的默认会话处理。例如,memcache模块(drupal.org/project/memcache)实现了函数'sess_open'、 'sess_close'、 'sess_read'、 'sess_write'、'sess_destroy_sid'、 'sess_gc'。将Drupal变量session_inc设置为这个会话代码,来代替使用默认的会话处理:
 
<?php
    variable_set('session_inc',         './sites/all/modules/memcache/memcache-session.inc');                  
?>
 
    通过在你的settings.php文件中设置该变量,你也可以实现对它的覆写:
 
$conf = array(
    'session_inc' => './sites/all/modules/memcache/memcache-session.inc,
    ...
);

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第16章 cookie的必要性

老葛的Drupal培训班 Think in Drupal

如果浏览器不接受cookie的话,那么就建立不了会话。这是因为PHP指令sessions_use_only_cookies已被设置为1;而sessions.use_trans_sid也被设置为0,这样其替代选项(在URL的查询字符串中传递PHPSESSID)也被禁用。这种方式是Zend所推荐的:
 
    基于URL的会话管理与基于cookie的相比,安全性更差一些。例如,用户可能向朋友发送一封email,其中带有了一个URL,它包含了处于激活状态的会话ID,或者用户可能将一个包含了会话ID的URL,保存在他们的书签里,之后再使用同一会话ID来访问你的站点。
 
    当PHPSESSID出现在一个站点的查询字符串中时,它标志着主机提供商已经锁住了PHP,并且不允许通过ini_set()函数在运行时设置PHP指令。可选的一种替代方式是将设置移到.htaccess文件中(如果在主机上,PHP是作为Apache的一个模块运行的话)或者一个本地的php.ini文件中(如果在主机上,PHP是作为一个可执行的CGI运行时)。
  为了防止会话欺诈,当用户登录时会重新生成会话ID(参看modules/user/user.module里的user_authenticate_finalize()函数)。当用户修改密码时,也会重新生成会话。

Drupal版本:

Drupal专业开发指南 第16章 存储

 

会话信息存储在表sessions中,在引导指令的DRUPAL_BOOTSTRAP_SESSION阶段,它将会话ID和Drupal用户ID关联了起来(关于Drupal引导指令处理流程的更多详细,请参看第15章)。事实上,在Drupal中广泛使用的$user对象,就是在本阶段使用includes/session.inc中的sess_read()函数初步建立的。($user对象是如何建立的,请参看第6章)。
 
    表16-2给出了存储会话的数据库表结构:
 
16-2. Sessions表的结构
字段       类型  长度  描述
uid         int             认证用户的用户ID(匿名用户为0)
sid         int     64      由PHP生成的会话ID
hostname    varchar 128     这个会话ID最后使用的IP地址
timestamp  int             最后页面请求的Unix时间戳
cache       int             用户最后发布的时间,用于执行最小缓存生命周期
session     text    big     存储在$_SESSION中的序列化数据
 
    当Drupal提供一个页面时,最后一项工作就是将会话写入表sessions中(参看includes/session.inc里面的sess_write())。只有当浏览器提供了一个有效的cookie时,才这样做,这样就阻止了网络爬虫抓取页面时向表sessions中写入大量数据。

老葛的Drupal培训班  Think in Drupal

Drupal版本:

Drupal专业开发指南 第16章 会话生命周期

 

16-1展示了会话生命周期。当一个浏览器向服务器发送一个请求时,会话开始。在Drupal的引导指令流程(参看includes/bootstrap.inc)的DRUPAL_BOOTSTRAP_SESSION阶段,会话代码开始运行。如果浏览器没有出示一个已有的cookie,那么PHP的会话管理系统将为浏览器分配一个新cookie,里面包含了一个新的PHP会话ID。这个ID通常是一个32位的唯一的MD5哈希字符串。当然,PHP5允许你将配置指令session.hash_function设置为1,这样你就可以使用SHA-1哈希,来得到40位的字符串作为会话ID。
 
注意 MD5是一个计算文本字符串哈希值的算法,也是Drupal中选用的哈希算法。关于MD5和其它哈希算法的更多信息,请参看http://en.wikipedia.org/wiki/Cryptographic_hash_functions
 
    Drupal接着以该会话ID为键,来检查在表sessions中是否存在相应的记录。如果存在的话,那么includes/sessions.inc中的函数sess_read()将取回会话数据,并进行一次SQL JOIN操作,对sessions表中的记录与users表中的对应记录进行联合。这一联合的结果是得到了一个对象,它包含了两条记录的所有字段和值。这就是$user对象,在Drupal中经常使用的一个全局变量(参看第6章)。那么,通过查看$user对象,具体一点就是$user->session, $user->sid, $user->hostname, $user->timestamp,和$user->cache,也可以访问会话数据。在sess_read()中,当前用户的角色也被查找出来并分配给$user->roles。
    对于会话中的用户ID,如果在users表中,没有一个对应的用户ID与之匹配,那么会发生什么呢?这个问题具有欺骗性。由于Drupal的安装器在users表中创建了一个用户ID为0的记录,而且匿名用户在sessions表中的uid被指定为0,所以联合总是成功的。
 
警告 对于你的Drupal安装,永远不要删除users表中的所有记录。因为,为了让Drupal正常工作,该表中必须包含一条用户ID为0的记录。
 
    如果你想找出用户访问一个页面的最后时间,你可以使用$user->timestamp(记住,它来自于sessions表),或者使用$user->access,后者保存在users表中。对于这两者来说,$user->timestamp如果存在的话,那么它会更精确一点,这是因为$user->access的更新受限于节流,默认情况下180秒才更新一次。通过设置Drupal变量session_write_interval,可以修改这个值。这里是includes/session.inc中sess_write()函数的代码片断:
 
// Last access time is updated no more frequently than once every 180 seconds.
// This reduces contention in the users table.
$session_write_interval = variable_get('session_write_interval', 180);
if ($user->uid && time() - $user->access > $session_write_interval) {
    db_query("UPDATE {users} SET access = %d WHERE uid = %d", time(), $user->uid);
}
 
    当然,对于初次访问的用户,因为还没有保存时间戳,所以$user->timestamp和$user->access都不可用。
    当页面已被发送给浏览器时,最后一步工作就是关闭会话。PHP触发includes/session.inc里面的sess_write()函数,该函数将$_SESSION(请求期间的)中存放的所有东西都写入到sessions表中。只有当你确实需要时,还有最好能够确定该用户已被认证,这样才将数据存储在$_SESSION中。这样在网络爬虫抓取页面时,就不会向sessions表中写入数据了,而该表的大小能够直接影响到性能。
 
16-1 Drupal是如何使用会话来实例化$user对象的

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第16章 会话对话模仿

 

下面是一些虚拟的例子,模拟了在你的浏览器访问Drupal站点时,从会话的角度来看,都发生了什么。
 
初次访问
浏览器:嗨,请给我一个页面。
Drupal:我可以看一下你的cookie么?
浏览器:对不起,我没有;我这是第一次来这里。
Drupal:好吧,给你一个。
 
第二次访问
浏览器:请再给我一个页面。
Drupal:我可以看一下你的cookie么?
浏览器:好的,它在这里。它的会话号码为6tc47s8jd6rls9cugkdrrjm8h5。
Drupal:嗯,不过在我的记录中找不到它。但是还是给你页面。我将会把你的会话ID记录下来,以备你再次访问。
 
 
拥有帐号的用户
(当用户创建了一个帐号并点击了登录按钮时。)
浏览器:嗨!我需要一个页面。
Drupal:我可以看一下你的cookie么?
浏览器:好的,它在这里。它的会话号码为31bfa29408ebb23239042ca8f0f77652
Drupal:你好,乔!(喃喃而语)你的用户ID为384,你喜欢将你的评论嵌套起来,你的咖啡黑色的。给你一个新的cookie,这样你的会话就不会被盗用。我将记录你的这次访问。玩的愉快。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第16章 常见任务

 

下面是一些你使用会话或者修改会话设置的常见方式。
 
修改cookie的存活周期
    包含会话ID的cookie的存活周期,是由settings.php中的session.cookie_lifetime控制的,其默认值为2,000,000秒(大约23天)。如果将该值设为0,那么当用户关闭浏览器时,cookie将被立即销毁。
 
修改会话名字
    当将网站部署在多个子域名上面时,就会遇到一个关于会话的常见问题。由于每个站点为session.cookie_domain使用相同的默认值,默认情况下PHPSESSID的session.name也相同,用户发现他们在任何给定时间只能登录到一个站点上。通过为每个站点创建一个唯一的会话名字,Drupal就可以解决这个问题。这里作了一些修改,会话名字是基于网站基URL的MD5哈希。
 
    通过删除settings.php中的一行代码前面的注释符,并声明$cookie_domain变量的值,就可以绕过自动生成会话名字了。该变量的值只能包含字母数字字符。下面是settings.php中对应的部分:
 
/**
 * Drupal automatically generates a unique session cookie name for each site
 * based on on its full domain name. If you have multiple domains pointing at
 * the same Drupal site, you can either redirect them all to a single domain
 * (see comment in .htaccess), or uncomment the line below and specify their
 * shared base domain. Doing so assures that users remain logged in as they
 * cross between your various domains.
 */
# $cookie_domain = 'example.com';
 
注意Drupal中,Perl风格的注释符号(#)只用于settings.php,.htaccess, robots.txt文件中,而实际的Perl和shell脚本都放在scripts目录中。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第16章 将数据存储在会话中

 

由于会话系统能将数据自动存储起来,所以将数据存储在用户的会话中,会非常方便。无论是什么时候,你想在用户访问(或者多次访问,使用session.cookie_lifetime)期间为其存储一些数据时,都可以使用全局变量$_SESSION:
 
$_SESSION['favorite_color'] = $favorite_color;
 
    接着,在接下来的请求中,使用下面代码取回该值:
 
$favorite_color = $_SESSION['favorite_color'];
 
    如果你知道用户的uid,并且你想为用户存储一些数据,那么有一个更实用的方法,就是将数据作为一个唯一的属性存放在$user对象中,比如通过调用user_save($user, array('foo' => $bar))就实现了$user->foo =$bar,该方法将数据序列化并保存到users表的data列中。如果信息是暂时的,并且你不介意它的丢失,或者你想为匿名用户存储一些短期数据,那么你也可以将它存放在会话中。如果你想将用户的偏爱与用户永远的绑定在一起,那么可将它存储在$user对象中。
 
警告$user对象不能用来为匿名用户存储信息。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第16章 总结

 

读完本章后,你应该能够:
    理解Drupal是如何修改PHP的会话处理的。
    理解哪些文件包含了会话配置信息。
    理解会话生命周期以及在一个请求期间是如何创建Drupal的$user对象的。
    在用户的会话中存储数据和取回数据

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第17章 使用jQuery

老葛的Drupal培训班 Think in Drupal

 JavaScript无处不在。每个主流的Web浏览器都带有一个JavaScript解释器。Apple的控制面板小部件就是用JavaScript写的。Mozilla Firefox的用户界面就是使用JavaScript实现的。Adobe Photoshop里面也可以使用JavaScript。JavaScript存在于web技术的每个角落。
     不久以前, JavaScript枯燥无味,很容易让人头痛。如果你曾经有过这样痛苦的经历,现在是时候换种方式了,过去的就让它过去吧,现在我们开始使用jQuery。jQuery使得JavaScript的代码编写更加直观有趣,它还被内置到了Drupal中!在本章,我们将学习什么是jQuery以及在Drupal中如何使用它。最后我们将深入的学习一个实际的例子。
 

Drupal版本:

Drupal专业开发指南 第17章 什么是jQuery?

 

jQuery,是由John Resig创建的,主要用于解决开发者在使用JavaScript时遇到的常见问题和限制。编写JavaScript代码是件麻烦和繁琐的事情,而查找你想操作的特定的HTML或CSS元素有时也非常困难。jQuery为你提供了一种简单快捷的方式,用来在你的文档中查找这些元素。
 定位一个对象的技术名字叫做DOM遍历(traversal)。DOM 是文档对象模型的简称。该模型提供了一种树状方式,通过它们的标签来访问页面元素,以及通过JavaScript访问其它元素,如图17-1所示。
 
注意 你可以从jQuery的官方网站http://jquery.comhttp://www.visualjquery.com 学到更多的jQuery相关知识。
 
    当编写JavaScript代码时,你常常花费大量时间来处理浏览器和操作系统的不兼容性。jQuery为你处理了这一工作。还有,JavaScript中没有太多的高级函数。常见的任务,比如页面特定部分的动画特效,四处拖曳东西,或者可排序的元素,这些在JavaScript中都不存在。而jQuery则克服了这些限制。
 
       和Drupal一样,jQuery的核心代码非常精炼并且高效,仅有30 kb。在jQuery的中心,是一个可扩展的框架,JavaScript程序员可以对其进行扩展,在http://plugins.jquery.com/中已有数百个jQuery插件了。
 
图17-1 http://jquery.com页面的DOM表示,这里使用了Firefox浏览器中的Mozilla DOM检查工具

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第17章 老方式

 

首先,让我们快速的回顾一下纯JavaScript方式的DOM遍历。下面的代码展示了,在引入jQuery以前,Drupal是如何查找页面元素的(在这里,就是所有的可伸缩的字段集内部的legend元素):
 
var fieldsets = document.getElementsByTagName('fieldset');
var legend, fieldset;
for (var i = 0; fieldset = fieldsets[i]; i++) {
    if (!hasClass(fieldset, 'collapsible')) {
        continue;
    }
    legend = fieldset.getElementsByTagName('legend');
    if (legend.length == 0) {
        continue;
    }
    legend = legend[0];
    ...
}
 
         而下面则是在Drupal中引入jQuery以后,升级了的代码:
 
$('fieldset.collapsible > legend:not(.collapse-processed)', context).each(
    function() { ... });
 
    正如你看到的一样, jQuery的口头禅是“写得少,做得多”。对于使用JavaScript操作DOM的常见的重复性任务,jQuery使用一种简洁直观的语法对其作了封装。最终的结果是,代码简短,灵巧,易读。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第17章 jQuery是如何工作的

 

       jQuery是一个工具,用来在结构化文档中查找东西。可以使用CSS选择器或者jQuery自定义选择器(一个jQuery插件,支持使用XPath选择器),来从文档中选择元素。为DOM遍历使用CSS选择器对于开发者来说很有帮助,因为大多数开发者都已经熟悉了CSS语法。jQuery完整的支持了CSS 1,CSS 2,CSS 3。在我们深入学习如何在Drupal中使用jQuery以前,让我们先看几个基本的例子,来学习一下jQuery语法。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第17章 使用一个CSS ID选择器

 

让我们快速的回顾一下基本的CSS语法。假定你要操作的HTML如下所示:
 
<p id="intro">Welcome to the World of Widgets.</p>
 
    如果你想将段落的背景颜色设置为蓝色,那么你使用CSS在你的样式表中定位这个特定的段落,这里使用了 #intro ID选择器。根据HTML规范,在一个给定的文档内,ID必须是唯一的,所以我们可以确定只有这个元素拥有这一ID。在应用于你文档的样式表内部,添加以下条目,让你的段落变为蓝色:
 
#intro {
    background-color: blue;
}
 
    注意,这里主要有两个任务:查找具有#intro ID的元素,将该元素的背景颜色设为蓝色。
    使用jQuery,你也可以完成同样的工作。但是首先,我们先简单介绍一下jQuery的语法:为了保持代码的简洁性,在jQuery的JavaScript代码中,使用下面的一行将jQuery命名空间映射为了美元符号($):
 
var jQuery = window.jQuery = function( selector, context ) {...};
...
// Map the jQuery namespace to the '$' one
window.$ = jQuery;
 
注意 如果你对jQuery引擎的工作原理感兴趣的话,你可以下载完整的未压缩的jQuery JavaScript文件,下载地址为http://jquery.com。Drupal中包含的是一个压缩的版本,这样在使用浏览器访问你的站点时,需要下载的数据总量就会小一些。
 
       下面使用jQuery,来选出你的段落并将其背景颜色转变为蓝色:
 
$("#intro").css("background-color", "blue");
 
    你甚至可还以加点jQuery特效,来慢慢的渐进显示段落文本:
 
$("#intro").css("background-color", "blue").fadeIn("slow");
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第17章 使用一个CSS类选择器

 

下面是一个类似的例子,与上节中我们所用的CSS ID选择器不同,这里使用了CSS类选择器。HTML如下所示:
 
<p class="intro">Welcome to the World of Widgets.</p>
<p class="intro">Widgets are available in many sizes.</p>
 
    我们的CSS如下所:
 
.intro {
    background-color: blue;
}
 
    下面的也可以工作,它是一个更加具体的规则:
 
p.intro {
    background-color: blue;
}
 
    下面是对应的jQuery代码:
 
$(".intro").css("background-color", "blue").fadeIn("slow");
$("p.intro").css("background-color", "blue").fadeIn("slow");
 
    在上面的第一个例子中,你让jQuery查找具有info类的任何HTML元素,而第二个例子则有点细微的不同。这里你在所有的段落标签中,查找具有info类的元素。注意后者的速度稍微快了一点,这是因为使用p.intro将查找的范围限制在了段落标签中,从而查找的HTML就少了许多。
 
提示CSS中,“.”是一个类选择器,在同一文档中可以重复出现;而“#”是一个唯一的ID选择器,在同一页面只能出现一次。
 
 现在你对jQuery的工作原理应该有些了解了,让我们实际的看一下如何在Drupal中使用它。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第17章 Drupal中的jQuery

 

Drupal中使用jQuery很简单,这是因为Drupal内置了jQuery,并且在添加JavaScript时会自动加载jQuery。在Drupal中,通过drupal_add_js()函数来添加JavaScript文件。在本节中,我们将研究Drupal中的一些基本的jQuery功能。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第17章 你的第一段jQuery代码

 

在使用jQuery以前,让我们先做些准备工作.
 
1. 以用户1的身份(管理帐号)登录到你的Drupal站点。
 
2. 导航到“管理➤站点构建➤模块”,启用PHP过滤器模块。
 
3. 创建一个新的Page类型的节点,在节点创建表单上,在“输入格式”下面一定要选择“PHP代码”,如图17-2所示。在标题中输入Testing jQuery,在表单的正文中输入以下内容:
 
<?php
    drupal_add_js(
        '$(document).ready(function(){
            // Hide all the paragraphs.
            $("p").hide();
            // Fade them into visibility.
            $("p").fadeIn("slow");
        });',
        'inline'
    );
?>
 
<p id="one">Paragraph one</p>
<p>Paragraph two</p>
<p>Paragraph three</p>
 
         点击提交,这样就重新加载了页面。你创建的3个段落将慢慢的渐显出来。酷吧?刷新页面,再看一遍,让我们进一步学习一下这个例子。
 
17-2.使用PHP过滤器来做jQuery实验
 
    jQuery代码包含在文件misc/jquery.js中。并不是为Drupal中的每个页面都加载这个文件。只有在调用了方法drupal_add_js()时,才加载它。这里向drupal_add_js()传递了两个参数。第一个参数是一段你希望执行的JavaScript代码,第二个参数“inline”则用来告诉Drupal将代码写入到文档的<head>元素中的<script></script>标签里面。
 
注意 我们这里只是很简单的使用了drupal_add_js(),它还有许多高级的用法,你可以参看http://api.drupal.org/api/function/drupal_add_js/6
 
    让我们更仔细的看看这段JavaScript jQuery代码。
 
$(document).ready(function(){
    // Hide all the paragraphs.
    $("p").hide();
    // Fade them into visibility.
    $("p").fadeIn("slow");
});
 
    第一行代码需要详细的解释一下。当浏览器显示一个页面时,它会到达一个点----它接收到了HTML并完全的解析了页面的DOM结构。接下来的步骤就是显示DOM,这包括加载附加的本地文件(可能还有远程文件)。如果在生成DOM以前,你想尝试执行JavaScript代码,由于代码想要操作的对象还不存在,那么它可能就会抛出错误并停止运行。JavaScript程序员一般使用下面的代码(或其变体)来处理这种情况:
 
window.onload = function(){ ... }
 
         使用window.onload的缺点是,它需要完全加载所有的附加文件,这可能需要等待很长的时间。另外这种方式的局限性还有,在这里只能使用一个函数。为了解决这两个问题,jQuery有一个简单的语句供你使用:
 
$(document).ready(function(){
// Your code here.
});
 
         在生成了DOM以后,就立即执行$(document).ready()。由于前面所列的原因,你通常需要把你的jQuery代码封装在前面的语句中。function()调用在JavaScript中定义了一个匿名函数----在这里,包含了你想要执行的代码。
 
    好了,让我们看一下代码的中心内容,此时应该就不言自明了:
 
// Hide all the paragraphs.
$("p").hide();
// Fade them into visibility.
$("p").fadeIn("slow");
 
       前面的代码找出了所有的段落标签,隐藏它们,并在页面内部慢慢的把它们显示出来。用jQuery的行话来说,fadeIn()部分就是一个方法
 
注意 我们修改的是所有的段落标签,所以如果你访问一个节点列表页面,比如http://example.com/?q=node,你将发现所有的段落标签都受到了影响,而不仅仅是来自于你测试页面的摘要中的段落。在我们的例子中,可以通过修改我们的node.tpl.php模板文件,当节点单独显示在一个页面时,使用<div class='standalone'>对其内容进行包装,这样就可以使用$(".standalone > p")来限制段落标签集了。这个查询仅选择位于类.standalone内部的p元素。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第17章 通过ID定位一个元素

 

让我们重复我们的实验,不过这次只定位第一个段落,我们使用ID one对它进行了标识:
 
<?php
    drupal_add_js(
        '$(document).ready(function(){
            // Hide paragraph with ID "one".
            $("#one").hide();
            // Fade it into visibility.
            $("#one").fadeIn("slow");
        });',
        'inline'
    );
?>
 
<p id="one">Paragraph one</p>
<p>Paragraph two</p>
<p>Paragraph three</p>
 
注意 jQuery里面,通过ID访问一个元素是最快的选择器方法之一,这是因为它会被翻译为本地的JavaScript代码:document.getElementById("one ")。替代方案$("p#one")就会慢一些,这是因为jQuery首先需要查找所有的段落标签,然后再从中选择ID为one的元素。在jQuery中最慢的选择器方法就是$(".foo"),因为它需要在所有的元素中查找选择器类为foo的元素(在该情况下,使用$("p.foo")会快一些)。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第17章 方法链

 

       我们可以连接一系列的jQuery方法的原因是, jQuery内部的大多数方法都返回一个jQuery对象。让我们在单个jQuery命令中将多个方法串连起来:
 
// Hide all the p tags, fade them in to visibility, then slide them up and down.
$("p").hide().fadeIn("slow").slideUp("slow").slideDown("slow");
 
    jQuery的调用是从左向右进行的。前面的代码片段找到所有的段落标签,并渐进的显示它们,然后使用一个幻灯效果把段落移上去再移下来。由于其中的每个方法返回的jQuery包装器对象都包含了相同的元素集(所有的p元素),所以我们可以对同一个元素集进行一次又一次的操作,直到达到最终的效果。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第17章 添加或删除一个类

 

jQuery可以动态的修改一个元素的CSS类。这里,我们通过ID选中我们例子中的第一个段落,然后为它分配一个Drupal的错误类,这样就将它转变为红色了:
 
$("#one").addClass("error");
 
    与addClass()方法对应的是removeClass()方法。下面的代码片段将删除我们刚刚添加的错误类:
 
$("#one").removeClass("error");
 
    还有一个toggleClass()方法,每次调用它时都会添加或删除一个类:
 
$("#one").toggleClass("error"); // Adds class "error".
$("#one").toggleClass("error"); // Removes class "error".
$("#one").toggleClass("error"); // Adds class "error" again.
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第17章 包装已有元素

 

 
    我们前面介绍了向<p id="one">元素添加一个错误类,现在换种方式,让我们把该元素包装在一个div标签中,这样就能够更好的显示红色了。下面的jQuery片段将实现这一点:
 
<?php
    drupal_add_js(
        '$(document).ready(function(){
           $("#one").wrap("<div class=\'error\'></div>");
        });',
        'inline'
    );
?>
 
<p id="one">Paragraph one</p>
<p>Paragraph two</p>
<p>Paragraph three</p>
 
    注意单引号的转义,由于我们在drupal_add_js()内部已经使用了一对单引号,所以需要对双引号内部的单引号进行转义。div包装的结果如图17-3所示。
 
17-3. ID为“one”的段落被包装在了一个类为“error”的div标签中。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第17章 修改CSS元素的值

 

可以使用jQuery为CSS元素分配(或重新分配)值。让我们将第一个段落外面的边框设为实线(参看图17-4):
 
$("#one").wrap("<div class=\'error\'></div>").css("border", "solid");
 
    注意,css方法作用的仍然是p元素,而不是div元素,这是因为wrap方法在完成了包装以后返回的仍然是原有的p元素。
 
17-4.目标元素的边框属性已被修改
 
    前面的这些例子说明了jQuery可以实现的一些基本任务,这里仅仅涉及到了jQuery的一点皮毛。如果你需要这方面的更多知识,那么可以访问http://jquery.com/,或者找一本好的jQuery书籍来好好的学习一下。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第17章 JavaScript放在哪里

 

在前面的例子中,我们启用了PHP过滤器模块,通过直接在节点中编写JavaScript来测试jQuery。这种方式用于测试还是比较方便的,但是如果放在实际的站点上,就不能用了,最佳实践表明要尽可能的不用PHP过滤器。有多种不同的选择,可以用来在你的Drupal站点上包含JavaScript文件。例如,你可以把它们添加到你的主题中,可以从模块中包含它们,甚至可以在包含它们的同时允许其它模块修改或覆写你的代码。
 

老葛的Drupal培训班  Think in Drupal

Drupal版本:

Drupal专业开发指南 第17章 通过一个主题.info文件添加JavaScript

 

包含JavaScript文件的最方便的方式,就是在你的主题的.info文件中添加一行代码,不过这种方式也有一个缺点,那就是缺乏灵活性。让我们为你的站点添加一个效果,用来强调你站点的标识语,在一个页面被加载时,先把它淡出,接着再把它渐显出来。把下面的JavaScript代码放在你主题下的一个名为logofade.js的文件中。例如,如果你使用的主题为Garland,那么它就位于themes/garland/logofade.js。
 
// $Id$
// Selects the theme element with the id "logo", fades it out,
// then fades it in slowly.
if (Drupal.jsEnabled) {
    $(document).ready(function(){
        $("#logo").fadeOut("fast").fadeIn("slow");
    });
}
 
    JavaScript文件已经有了;现在我们只需要告诉Drupal加载它就可以了。向你的当前主题的.info文件中添加下面一行代码:
 
scripts[] = logofade.js
 
    最后一步就是让Drupal重读.info文件,这样它就会看到它需要加载logofade.js了。为了实现这一点,导航到“管理➤站点构建➤主题”,临时的转换到一个不同的主题上,然后再转换回来。
    如果对于你网站的每个页面,都需要为其加载某一JavaScript文件的话,那么这个方法还是非常有用的。在接下来的一节中,你将看到如何实现,只有当使用JavaScript的模块被启用时,才加载它。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第17章 一个使用jQuery的模块

 

让我们构建一个小模块,用来在一个JavaScript文件中包含一些jQuery函数。首先,我们需要一个用例。嗯,使用JavaScript代码来控制区块,如何?区块在Drupal中是很有用的:它们可以为你显示你的登录状态,告诉你站点的新进用户或者在线用户,提供有用的导航。但是有时候,你只想关注页面的内容。如果在默认的情况下把区块隐藏,只有当你想要查看它们的时候才将其显示出来,这样不也不错么?下面的模块实现了这一点,它使用jQuery来对左右边栏区域中的区块进行标识和隐藏,并提供了一个有用的按钮用来将区块重新显示出来。
    下面是sites/all/modules/custom/blockaway.info:
 
; $Id$
name = Block-Away
description = Uses jQuery to hide blocks until a button is clicked.
package = Pro Drupal Development
core = 6.x
 
    而下面是sites/all/modules/custom/blockaway.module:
 
<?php
// $Id$
 
/**
 * @file
 * Use this module to learn about jQuery.
 */
 
/**
 * Implementation of hook_init().
 */
function blockaway_init() {
    drupal_add_js(drupal_get_path('module', 'blockaway') .'/blockaway.js');
}
 
    这个模块的全部工作就是包含以下的JavaScript文件,我们可以将它放在sites/all/modules/custom/blockaway/blockaway.js:
 
// $Id$
 
/**
 * Hide blocks in sidebars, then make them visible at the click of a button.
 */
if (Drupal.jsEnabled) {
    $(document).ready(function() {
        // Get all div elements of class 'block' inside the left sidebar.
        // Add to that all div elements of class 'block' inside the
        // right sidebar.
        var blocks = $('#sidebar-left div.block, #sidebar-right div.block');
 
        // Hide them.
        blocks.hide();
 
        // Add a button that, when clicked, will make them reappear.
        $('#sidebar-left').prepend('<div id="collapsibutton">Show Blocks</div>');
        $('#collapsibutton').css({
            'width': '90px',
            'border': 'solid',
            'border-width': '1px',
            'padding': '5px',
            'background-color': '#fff'
        });
        // Add a handler that runs once when the button is clicked.
        $('#collapsibutton').one('click', function() {
            // Button clicked! Get rid of the button.
            $('#collapsibutton').remove();
            // Display all our hidden blocks using an effect.
            blocks.slideDown("slow");
        });
    });
}
 
    导航到“管理➤站点构建➤模块”,启用该模块,你以前可见的所有区块都消失了,被替换为了一个没有格式的按钮,如图17-5所示。
 
17-5. blockaway.module启用后,一个节点的显示
 
    点击了这个按钮以后,区块将会使用一个幻灯效果显示出来,变成可见的,如图17-6所示。
 
17-6.点击了显示区块按钮以后,区块变成了可见的。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第17章 可覆写的JavaScript

老葛的Drupal培训班 Think in Drupal

    blockaway.module中的代码非常简单并易于理解。它只负责包含blockaway.js文件。然而,如果模块更加复杂一点的话,那么将drupal_add_js()函数调用放在一个主题函数中,来取代放在hook_init()中,这对其他开发者来说会更加友好。这样,对于那些想使用你的模块,同时又想用一些方式来定制JavaScript代码的用户,他们无需修改你模块的源代码就可以完成定制工作(关于主题系统的更多详细,可参看第8章)。下面的代码是blockaway.module的修订版本,它使用hook_theme()声明了一个主题函数,并将drupal_add_js()调用移到了该主题函数中,而在hook_init()中调用这个主题函数。功能是一样的,但是现在聪明的开发者就可以对blockaway.js文件进行覆写了。
 
<?php
// $Id$
 
/**
 * @file
 * Use this module to learn about jQuery.
 */
 
/**
 * Implementation of hook_init().
 */
function blockaway_init() {
    theme('blockaway_javascript');
}
 
/**
 * Implementation of hook_theme().
 * Register our theme function.
 */
function blockaway_theme() {
    return array(
        'blockaway_javascript' => array(
            'arguments' => array(),
        ),
    );
}
 
/**
 * Theme function that just makes sure our JavaScript file
 * gets included.
 */
function theme_blockaway_javascript() {
    drupal_add_js(drupal_get_path('module', 'blockaway') .'/blockaway.js');
}
 
    让我们继续前进,来看看这种方法是否可以工作。我们将使用主题提供的JavaScript,来覆写模块提供的JavaScript。把sites/all/modules/custom/blockaway/blockaway.js拷贝到你的当前主题下----例如,themes/garland/blockaway.js。让我们稍微的修改一下JavaScript文件,这样我们就知道正在使用的是哪个JavaScript文件。将特效从slideDown("slow")改为fadeIn(5000);这将使用5秒钟的时间来渐进的显示区块。下面是新文件:
 
// $Id$
/**
 * Hide blocks in sidebars, then make them visible at the click of a button.
 */
if (Drupal.jsEnabled) {
    $(document).ready(function() {
        // Get all div elements of class 'block' inside the left sidebar.
        // Add to that all div elements of class 'block' inside the
        // right sidebar.
        var blocks = $('#sidebar-left div.block, #sidebar-right div.block');
 
        // Hide them.
        blocks.hide();
 
        // Add a button that, when clicked, will make them reappear.
        // Translate strings with Drupal.t(), just like t() in PHP code.
        var text = Drupal.t('Show Blocks');
        $('#sidebar-left').prepend('<div id="collapsibutton">' + text + '</div>');
        $('#collapsibutton').css({
            'width': '90px',
            'border': 'solid',
            'border-width': '1px',
            'padding': '5px',
            'background-color': '#fff'
        });
 
        // Add a handler that runs once when the button is clicked.
        $('#collapsibutton').one('click', function() {
            // Button clicked! Get rid of the button.
            $('#collapsibutton').remove();
            // Display all our hidden blocks using an effect.
           blocks.fadeIn(5000);
        });
    });
}
 
    我们最后要做的修改就是告诉Drupal加载这个新文件,从而取代sites/all/modules/custom/blockaway中的文件。我们通过覆写主题函数来实现这一点。向你主题的template.php文件中(如果你的主题还没有一个template.php文件,那么你需要创建一个),添加下面所给的函数:
 
<?php
// $Id$
 
/**
 * Override theme_blockaway_javascript() with the
 * following function.
 */
function phptemplate_blockaway_javascript() {
    drupal_add_js(path_to_theme() . '/blockaway.js');
}
 
    现在,当你使用浏览器来访问一个页面时,你应该能够看到“显示区块”按钮了,点击这个按钮,将会使用一个渐进的淡入效果来显示区块,而不是我们前面所用的幻灯效果。恭喜恭喜!你已经学会了如何在你的模块中使用jQuery,如何使用友好的方式编写jQuery以方便主题制作者和其他的开发者,同时,你还学会了如何覆写或增强其它模块中的JavaScript文件,这里假定这些模块中的JavaScript文件可被覆写。
    在我们结束这个例子的学习以前,让我演示一下如何使用模板文件进行覆写。首先,删除你添加到template.php文件中的phptemplate_blockaway_javascript()函数。接着,在你的当前主题中,创建一个空文件blockawayjavascript.tpl.php。例如,如果你使用的是Garland主题,那么创建的就是themes/garland/blockaway-javascript.tpl.php。不要在这个文件中放置任何东西。现在导航到“管理➤站点构建➤模块”,访问这个页面的作用就是重新构建主题注册表。Drupal将找到该模板文件,并使用它来替代你模块中的主题函数。最终的结果就是永远也不会加载blockaway.js了;通过创建一个空的模板文件,你实质上就是注释掉了该主题函数(回想一下第8章所讲的,当构建主题注册表的时候,Drupal将首先查找一个模板文件,然后才是主题函数)。
    现在,向你的blockaway-javascript.tpl.php文件中添加以下代码:
 
<?php drupal_add_js(path_to_theme() . '/blockaway.js'); ?>
 
    当你重新加载页面时,你应该可以看到JavaScript文件现在加载进来了。如果你想将第3方模块中的JavaScript文件替换为你的增强版本,或者想阻止加载一些JavaScript文件时,这里所讲的这些技术还是很有用处的。
 
注意 你不能在page.tpl.php中调用drupal_add_js(),对于其它的一些主题函数,如果是在它的预处理阶段调用的话(比如区块),那么也不能使用drupal_add_js()。为什么呢?这是因为它们在页面构建流程中执行的顺序过于靠后。核心模板文件是如何添加JavaScript的,可参看modules/block/block-admin-display-form.tpl.php。
 

Drupal版本:

Drupal专业开发指南 第17章 构建一个jQuery投票小部件(Widget)

老葛的Drupal培训班 Think in Drupal

让我们编写一个稍微复杂一点的启用jQuery的Drupal模块。我们将构建一个AJAX投票小部件,如图17-7所示,它可以让用户为他们喜欢的文章加一分。我们将使用jQuery来进行投票和修改总的投票分数,而不用重新加载整个页面。我们还添加一个基于角色的权限,这样只有具有“rate content”权限的用户才允许投票。由于用户的每次投票只能增加一分,所以让我们将模块命名为“plusone”。
 
17-7.投票小部件
 
    在我们开始为plusone模块编写实际的jQuery部分以前,首先需要构建模块所需的一些基本代码。如果你以前从来没有构建过模块,请参看第2章。否则,那么就让我们现在开始吧!
    在sites/all/modules/custom中创建一个名为plusone的目录(你可能首先需要创建sites/all/modules/custom目录)。在plusone目录中,创建文件plusone.info,向里面添加以下内容:
 
; $Id$
name = Plus One
description = "A +1 voting widget for nodes. "
package = Pro Drupal Development
core = 6.x
 
    这个文件将该模块注册到了Drupal中,这样可以通过管理界面启用或者禁用模块了。
    接着,我们将创建plusone.install文件。当启用、禁用、安装、卸载这个模块时,就会调用这个PHP文件中的函数;它一般用来创建或者删除数据库表。在这里,我们想用来追踪谁在哪个节点上投了票:
 
<?php
// $Id$
 
/**
 * Implementation of hook_install().
 */
function plusone_install() {
    // Create tables.
    drupal_install_schema('plusone');
}
 
/**
 * Implementation of hook_uninstall().
 */
function plusone_uninstall() {
    // Remove tables.
    drupal_uninstall_schema('plusone');
}
 
/**
 * Implementation of hook_schema().
 */
function plusone_schema() {
    $schema['plusone_votes'] = array(
        'description' => t('Stores votes from the plusone module.'),
        'fields' => array(
            'uid' => array(
                'type' => 'int',
                'not null' => TRUE,
                'default' => 0,
                'description' => t('The {user}.uid of the user casting the vote.'),
            ),
            'nid' => array(
                'type' => 'int',
                'not null' => TRUE,
                'default' => 0,
                'description' => t('The {node}.nid of the node being voted on.'),
            ),
            'vote_count' => array(
                'type' => 'int',
                'not null' => TRUE,
                'default' => 0,
                'description' => t('The number of votes cast.'),
            ),
        ),
        'primary key' => array('uid', 'nid'),
        'indexes' => array(
            'nid' => array('nid'),
            'uid' => array('uid'),
        ),
    );
    return $schema;
}
 
    还有,添加文件sites/all/modules/custom/plusone/plusone.css。这个文件不是必需的,但它可以使投票小部件的外观更漂亮一些,如图17-8所示。
 
17-8. 对比带有CSS投票小部件和不带有CSS的投票小部件
 
    向plusone.css添加以下内容:
 
div.plusone-widget {
    width: 100px;
    margin-bottom: 5px;
    text-align: center;
}
div.plusone-widget .score {
    padding: 10px;
    border: 1px solid #999;
    background-color: #eee;
    font-size: 175%;
}
div.plusone-widget .vote {
    padding: 1px 5px;
    margin-top: 2px;
    border: 1px solid #666;
    background-color: #ddd;
}
 
    我们已经创建了外围支持文件,现在让我们关注模块文件本身和jQuery JavaScript文件。创建两个空文件,sites/all/modules/custom/plusone/plusone.js和sites/all/modules/custom/plusone/plusone.module,在接下来的几步中,我们将逐步的向这两个文件添加代码。总结一下,我们已经创建了以下文件:
 
sites/
    all/
        modules/
            custom/
                plusone/
                    plusone.js
                    plusone.css
                    plusone.info
                    plusone.install
                    plusone.module

Drupal版本:

Drupal专业开发指南 第17章 创建模块

老葛的Drupal培训班 Think in Drupal

在一个文本编辑器中打开空的plusone.module,并添加标准的Drupal头部文件:
 
<?php
// $Id$
/**
 * @file
 * A simple +1 voting widget.
 */
 
    接下来我们将逐个的添加我们用到的Drupal钩子。一个比较简单的就是hook_perm(),它让你向Drupal的基于角色的访问控制页面添加一个“rate content”权限。使用这一权限,你就可以阻止那些没有账号或者没有登录的匿名用户进行投票了。
 
/**
 * Implementation of hook_perm().
 */
function plusone_perm() {
    return array('rate content');
}
 
    现在,我们开始实现一些AJAX功能。jQuery的一个重要特性,就是它能够提交自己的HTTP GET 或 POST请求,使用这一特性你就可以将投票提交给Drupal,而不用刷新整个页面了。jQuery将拦截投票链接上的点击事件,并向Drupal发送一个请求,让其保存投票并返回更新后的总分数。jQuery将使用这个新值来更新页面上的分数。图17-9展示了整个流程的概览。
    一旦jQuery拦截了对投票链接的点击,它需要通过一个URL来调用一个Drupal函数。我们使用hook_menu(),将由jQuery提交的投票URL映射到一个Drupal PHP函数上。这个PHP函数将投票保存到数据库中,并以JSON (JavaScript Object Notation)的形式为jQuery返回一个新分数(噢,这样我们就没有使用XML,因此这也就不是严格意义上的AJAX了)。
 
/**
 * Implementation of hook_menu().
 */
function plusone_menu() {
    $items['plusone/vote'] = array(
        'page callback' => 'plusone_vote',
        'access arguments' => array('rate content'),
        'type' => MENU_CALLBACK,
    );
    return $items;
}
 
    在前面的函数中,当路径为plusone/vote的请求进来以后,如果请求该路径的用户拥有“rate content”权限,那么函数plusone_vote()就会处理这个请求。
   
17-9.投票更新流程概览
 
注意 如果发送请求的用户没有“rate content”权限,Drupal将返回一个拒绝访问页面。然而,我们将确保动态的构建我们的投票小部件,这样那些无权投票的用户就看不到投票链接了。对于那些恶意的用户,他们可能会绕过我们的小部件,直接访问http://example.com/?q=plusone/vote,此时Drupal的权限系统仍然会保护我们免受他们的攻击。
 
    路径plusone/vote/3翻译成了PHP函数调用plusone_vote(3)(关于Drupal的菜单/回调系统的更多详细,参看第4章)。
 
/**
 * Called by jQuery, or by browser if JavaScript is disabled.
 * Submits the vote request. If called by jQuery, returns JSON.
 * If called by the browser, returns page with updated vote total.
 */
function plusone_vote($nid) {
    global $user;
    $nid = (int)$nid;
 
    // Authors may not vote on their own posts. We check the node table
    // to see if this user is the author of the post.
    $is_author = db_result(db_query('SELECT uid FROM {node} WHERE nid = %d AND
        uid = %d', $nid, $user->uid));
 
    if ($nid > 0 && !$is_author) {
        // Get current vote count for this user.
        $vote_count = plusone_get_vote($nid, $user->uid);
        if (!$vote_count) {
        // Delete existing vote count for this user.
        db_query('DELETE FROM {plusone_votes} WHERE uid = %d AND nid = %d',
            $user->uid, $nid);
        db_query('INSERT INTO {plusone_votes} (uid, nid, vote_count) VALUES
            (%d, %d, %d)', $user->uid, $nid, $vote_count + 1);
        watchdog('plusone', 'Vote by @user on node @nid.', array(
            '@user' => $user->name, '@nid' => $nid));
        }
    }
    // Get new total to display in the widget.
    $total_votes = plusone_get_total($nid);
    // Check to see if jQuery made the call. The AJAX call used
    // the POST method and passed in the key/value pair js = 1.
    if (!empty($_POST['js'])) {
        // jQuery made the call.
        // This will return results to jQuery's request.
        drupal_json(array(
            'total_votes' => $total_votes,
            'voted' => t('You voted')
        )
    );
    exit();
    }
 
    // It was a non-JavaScript call. Redisplay the entire page
    // with the updated vote total by redirecting to node/$nid
    // (or any URL alias that has been set for node/$nid).
    $path = drupal_get_path_alias('node/'. $nid);
    drupal_goto($path);
}
 
    前面的plusone_vote()函数,保存了当前的投票,并向jQuery返回信息;信息的形式是一个关联数组,里面包含了新总分和字符串You voted(你已投票),这个字符串用于替换投票小部件下面的Vote(投票文本。这个数组将被传给drupal_json(),这样就可以将PHP变量转化为它们的JavaScript等价物,在这里,就是将一个PHP关联数组转化为了一个JavaScript对象,并将HTTP头部设置为Content-type: text/javascript。关于JSON的更多详细,参看http://en.wikipedia.org/wiki/JSON
    注意,在前面的函数中,我们是如何处理浏览器禁用了JavaScript这种情况的。当我们编写jQuery代码时,我们将确保,来自jQuery的AJAX调用会传递一个名为js的参数并使用POST方法。如果没有js参数的话,我们就知道此时是用户点击了投票链接,浏览器本身请求了该路径----例如,plusone/vote/3。在这种情况下,因为浏览器期望的是一个普通的HTML页面,所以我们不会返回JSON。相反,我们更新了投票总分来反映用户投票的事实,接着,我们将浏览器重定向到最初的页面,Drupal将负责重新构建该页面并显示新的投票总分。
    在前面的代码中,我们调用了plusone_get_vote()和plusone_get_total(),现在让我们创建这两个函数:
 
/**
 * Return the number of votes for a given node ID/user ID pair.
 */
function plusone_get_vote($nid, $uid) {
    return (int)db_result(db_query('SELECT vote_count FROM {plusone_votes} WHERE
        nid = %d AND uid = %d', $nid, $uid));
}
 
/**
 * Return the total vote count for a node.
 */
function plusone_get_total($nid) {
    return (int)db_result(db_query('SELECT SUM(vote_count) FROM {plusone_votes}
        WHERE nid = %d', $nid));
}
 
    现在,让我们集中精力将投票小部件显示在文章旁边。这包括两部分。首先,我们在plusone_widget()函数内部定义一些变量。接着我们将这些变量传递给一个主题函数。下面是第一部分:
 
/**
 * Create voting widget to display on the web page.
 */
function plusone_widget($nid) {
    global $user;
 
    $total = plusone_get_total($nid);
    $is_author = db_result(db_query('SELECT uid FROM {node} WHERE nid = %d
        AND uid = %d', $nid, $user->uid));
    $voted = plusone_get_vote($nid, $user->uid);
 
    return theme('plusone_widget', $nid, $total, $is_author, $voted);
}
 
    还记不记得,当我们需要一个可主题化的项目时,我们需要使用hook_theme()来向Drupal声明它,这样就将其包含在了主题注册表中。下面就是这个钩子函数:
 
/**
 * Implementation of hook_theme().
 * Let Drupal know about our theme function.
 */
function plusone_theme() {
    return array(
        'plusone_widget' => array(
            'arguments' => array('nid', 'total', 'is_author', 'voted'),
        ),
    );
}
 
    接着,我们就需要实际的主题函数了。注意,在这里我们加载了我们的JavaScript和CSS文件。
 
/**
 * Theme for the voting widget.
 */
function theme_plusone_widget($nid, $total, $is_author, $voted) {
    // Load the JavaScript and CSS files.
    drupal_add_js(drupal_get_path('module', 'plusone') .'/plusone.js');
    drupal_add_css(drupal_get_path('module', 'plusone') .'/plusone.css');
 
    $output = '<div class="plusone-widget">';
    $output .= '<div class="score">'. $total .'</div>';
 
    $output .= '<div class="vote">';
    if ($is_author) {
        // User is author; not allowed to vote.
        $output .= t('Votes');
    }
    elseif ($voted) {
        // User already voted; not allowed to vote again.
        $output .= t('You voted');
    }
    else {
        // User is eligible to vote.
        $output .= l(t('Vote'), "plusone/vote/$nid", array(
            'attributes' => array('class' => 'plusone-link')
            ));
    }
    $output .= '</div>'; // Close div with class "vote".
    $output .= '</div>'; // Close div with class "plusone-widget".
 
    return $output;
}
 
    在前面代码的plusone_widget()函数中,我们先设置了一些变量,接着将小部件的主题化委托给了我们创建的自定义主题函数theme_plusone_widget()。记住theme('plusone_widget')实际上调用的就是theme_plusone_widget()(更多详细,可参看第8章)。创建一个单独的主题函数,而不是在plusone_widget()函数内部构建HTML,这样设计者在想要修改外观时就能覆写这个函数了。
    在我们的主题函数theme_plusone_widget()中,一定要为关键的HTML元素添加CSS类属性,这样在jQuery中就可以非常方便的定位这些元素了。还有,看一下链接的URL。它指向了plusone/vote/$nid,其中$nid是文章的当前节点ID。当用户点击这个链接时,由于我们使用jQuery监听该链接上的onClick事件,所以jQuery将代替Drupal来拦截并处理这个事件。看到没有,我们在构建链接时,是如何定义CSS选择器plusone-link的?在我们后面的Javascript中,找到该选择器出现的地方,这就是a.plus1-link。它就是说,一个带有css类plusone-link的HTML元素<a>。
    显示在页面http://example.com/?q=node/4中的小部件的HTML,应该是这样的:
 
<div class="plusone-widget">
    <div class="score">0</div>
    <div class="vote">
        <a class="plusone-link" href="/plusone/vote/4">Vote</a>
    </div>
</div>
 
    theme_plusone_widget()函数用来生成发送给浏览器的小部件。我们想让这个小部件显示在节点视图中,这样当用户查看节点时,就可以进行投票了。你能猜一下,我们将使用哪个Drupal钩子呢?这就是我们的老朋友hook_nodeapi(),它允许我们修改正被构建的任意节点。
 
/**
 * Implementation of hook_nodeapi().
 */
function plusone_nodeapi(&$node, $op, $teaser, $page) {
    switch ($op) {
        case 'view':
        // Show the widget, but only if the full node is being displayed.
        if (!$teaser) {
            $node->content['plusone_widget'] = array(
                '#value' => plusone_widget($node->nid),
                '#weight' => 100,
            );
        }
        break;
 
        case 'delete':
        // Node is being deleted; delete associated vote data.
        db_query('DELETE FROM {plusone_vote} WHERE nid = %d', $node->nid);
        break;
    }
}
 
    我们将weight元素的值设置为一个大的(或者“重的”)数字,这样就确保了小部件显示在文章的底部,而不是顶部。我们还偷偷的加了一个delete情况,这样当节点被删除时,该节点的投票记录也将被一同删除。
这就是plusone.module的全部内容了。我们的模块马上就要完工了,现在就剩下填写plusone.js了,在里面填写我们的jQuery代码,用于执行AJAX调用,更新投票总分,并将字符串Vote修改为You voted
 
// $Id$
 
// Only run if we are in a supported browser.
if (Drupal.jsEnabled) {
    // Run the following code when the DOM has been fully loaded.
    $(document).ready(function () {
        // Attach some code to the click event for the
        // link with class "plusone-link".
        $('a.plusone-link').click(function () {
            // When clicked, first define an anonymous function
            // to the variable voteSaved.
            var voteSaved = function (data) {
                // Update the number of votes.
                $('div.score').html(data.total_votes);
                // Update the "Vote" string to "You voted".
                $('div.vote').html(data.voted);
            }
            // Make the AJAX call; if successful the
            // anonymous function in voteSaved is run.
            $.ajax({
                type: 'POST', // Use the POST method.
                url: this.href,
                dataType: 'json',
                success: voteSaved,
                data: 'js=1' // Pass a key/value pair.
            });
            // Prevent the browser from handling the click.
            return false;
        });
    });
}
 
    你应该把你所有的jQuery代码都包装在一个Drupal.jsEnabled测试中。这个测试将确保当前的浏览器支持特定的DOM方法(如果不支持的话,那么将不会执行我们的JavaScript)。
    这个JavaScript向a.plusone-link添加了一个事件侦听器(还记不记得我们将plusone-link定义为了CSS类选择器?),这样当用户点击链接时,它将触发一个HTTP POST请求,来请求它指向的URL。前面的代码还演示了,jQuery是如何处理从Drupal中传递回来的数据的。当AJAX请求完成以后,返回值(从Drupal中返回)将作为data参数传递到匿名函数中,我们把这个函数赋值给了变量voteSaved。关联数组中的键所引用的数组,最初是在Drupal内部的plusone_vote()函数中构建的。最后,Javascript更新了分数,并将文本“Vote”修改为了“You Voted”。
    为了阻止加载整个页面(因为JavaScript负责处理点击事件),在JavaScript jQuery函数中,我们把返回值设置为了false

Drupal版本:

Drupal专业开发指南 第17章 使用Drupal.behaviors

 

JavaScript的交互,是通过向DOM中的元素绑定一些行为实现的(例如,使用一个事件,比如鼠标点击,来触发动作)。对DOM的一个修改,可能就会导致这一绑定的失效。所以,尽管我们前面所用的plusone.js文件,在一个简单的Drupal站点上可以很好的工作,但是在一个复杂的Drupal站点上,如果其它JavaScript文件也操作这个DOM的话,那么就可能会遇到问题。Drupal提供了一个中心变量Drupal.behaviors,使用它就可以注册JavaScript函数了,这样就确保了绑定的行为在需要的时候能正确的执行。下面的plusone.js版本,和前面的版本在功能上是一样的,都允许通过AJAX进行投票,但是这里的这个,通过使用Drupal.behaviors进行了注册,从而保护了我们的绑定:
 
// $Id$
 
Drupal.behaviors.plusone = function (context) {
    $('a.plusone-link:not(.plusone-processed)', context)
    .click(function () {
        var voteSaved = function (data) {
            $('div.score').html(data.total_votes);
            $('div.vote').html(data.voted);
        }
        $.ajax({
            type: 'POST',
            url: this.href,
            dataType: 'json',
            success: voteSaved,
            data: 'js=1'
        });
        return false;
    })
    .addClass('plusone-processed');
}
 
    注意,这里我们没有进行Drupal.jsEnabled测试,这是因为现在Drupal能自动的帮我们实现了这一点。关于Drupal.behaviors的更多详细,可参看misc/drupal.js。
 老葛的Drupal培训班 http://www.thinkindrupal.com

Drupal版本:

Drupal专业开发指南 第17章 扩展这个模块的方式

 

    对于这个模块,我们可以进行很多好的扩展,比如允许站点管理员仅对特定节点类型启用投票小部件。实现方式和我们在第2章构建注释模块时所用的方式一样。接着,在hook_nodeapi('view')内部,在添加小部件以前,你应该需要检查一下是否为给定节点类型启用了投票功能。还可以从许多其它的方面来扩展这个模块,比如基于不同的角色为投票分配不同的权数,或者限定一个用户在一天内的总投票数量。我们这里的目的是让这个模块尽可能的简单一点,以强调Drupal与jQuery之间的交互。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第17章 兼容性

 

jQuery的兼容性,还有jQuery的其它重要信息,都可在http://docs.jquery.com找到。总之,jQuery支持以下浏览器:
 
    • IE6.0及更高版本
    • Mozilla Firefox 1.5及更高版本
    • Apple Safari 2.0.2及更高版本
    • Opera 9.0及更高版本
 
    关于浏览器兼容性的更多详细,可参看http://docs.jquery.com/Browser_Compatibility
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第17章 后续步骤

 

    为了更好的学习如何在Drupal中使用jQuery,可以看一下你的Drupal安装中的misc目录。在这里你可以找到各种JavaScript文件,比如,表单字段的自动完成(autocomplete.js),批处理(batch.js),字段集的伸缩性(collapse.js),进度条的创建(progress.js),可拖拽的表格(tabledrag.js),以及更多。还可参看Drupal的JavaScript小组,位于http://groups.drupal.org/javascript
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第17章 总结

 

在本章中,你学到了:
    • 什么是jQuery
    • jQuery工作原理的一般概念
    • 如何在你的模块中加载JavaScript文件
    • jQuery和Drupal是如何交互的,如何在前后台之间传递请求和数值
    • 如何构建一个简单的投票小部件
 
 
 
 
 
 
 
 
   
 
提示: 如果你在计算机上自己动手实践,但是小部件却不能正常工作。你需要检查一下,你是不是以内容创建者的身份登录了(这是因为用户不能对自己创建的内容投票),并检查一下登录用户是否拥有权限“rate content”(“评论内容”)。一个好用的Ajax请求测试工具是名为Firbug的FireFox插件,你可以从http://getfirebug.com/下载到它。
 
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 本地化和翻译

老葛的Drupal培训班 Think in Drupal

本地化就是将用户界面的字符串替换为适合用户本地的翻译了的字符串。Drupal的开发和使用都是由一个国际化社区完成的。因此,它默认支持本地化,还对从右向左的语言,比如阿拉伯语、以色列语,提供了主题化支持。在本章中,你将看到如何启用本地化,如何使用用户界面翻译将Drupal内置的字符串替换为你自己的。接着,我们将看一个完整的翻译,并学习如何创建、导入、导出它们。最后,我们将检查一下Drupal在多个语言下显示相同内容的能力(比如,一个加拿大的网站,它使用英语和法语来显示内容),并学习Drupal是如何为页面的显示选择合适的语言的。
 

Drupal版本:

Drupal专业开发指南 第18章 启用本地模块(Locale Module)

 

    本地模块,为Drupal提供了语言处理功能和用户界面翻译,在安装Drupal时并未启用它。这与Drupal的哲理是一致的,那就是仅在需要的时候启用相应的功能。你可以在“管理➤站点构建 ➤模块”中启用本地模块。如果安装Drupal时,选用的语言不是英语,而是其它的一个语言翻译,那么本地模块将作为安装流程的一部分被启用。本章中的例子都假定启用了本地模块。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 用户界面翻译

 

Drupal的界面是由字儿、短语、和句子组成的,用来向用户传递信息。在接下来的部分中,你将看到如何修改它们。我们的例子将集中于字符串替换,同时我们需要理解----翻译就是建立在字符串替换的基础之上的。
 
字符串
    从一个编程的观点来看,一个字符串就是一系列的字符,比如5位字符的字符串Hello。在Drupal中,字符串的翻译构成了用户界面翻译的基础。当Drupal准备一个用来输出的字符串时,它将检查这个字符串是否需要被翻译一下,如果启用了英语,那么将显示“Hello”;如果启用了法语,那么将显示“Bonjour”。让我们看一下这是怎么实现的。
 
使用t()翻译字符串
    在Drupal中显示给终端用户的所有字符串,都须经过t()函数的处理;这个就是Drupal的翻译函数,由于经常使用,为了方便起见,所以将它简写为“t”。
 
注意Drupal中,有些地方隐含的使用了t(),比如传递给watchdog()的字符串,或者菜单钩子中的标题和描述。复数形式使用format_plural()来翻译,该函数负责调用t()(参看http://api.drupal.org/api/function/format_plural/6)。
 
    t()函数中特定于本地的部分,如下所示:
 
function t($string, $args = array(), $langcode = NULL) {
    global $language;
    static $custom_strings;
 
    $langcode = isset($langcode) ? $langcode : $language->language;
 
    // First, check for an array of customized strings. If present, use the array
    // *instead of* database lookups. This is a high performance way to provide a
    // handful of string replacements. See settings.php for examples.
    // Cache the $custom_strings variable to improve performance.
    if (!isset($custom_strings[$langcode])) {
        $custom_strings[$langcode] = variable_get('locale_custom_strings_'.
            $langcode, array());
    }
    // Custom strings work for English too, even if locale module is disabled.
    if (isset($custom_strings[$langcode][$string])) {
        $string = $custom_strings[$langcode][$string];
    }
    // Translate with locale module if enabled.
    elseif (function_exists('locale') && $langcode != 'en') {
        $string = locale($string, $langcode);
    }
    if (empty($args)) {
        return $string;
    }
    ...
}
 
    除了负责翻译以外,t()函数还负责向字符串中的占位符插入相应的值。这些值通常是用户提供的输入,它们在显示以前,必须经过文本转换处理。
 
t('Hello, my name is %name.', array('%name' => 'John');
Hello, my name is John.
 
    文本中待插入的位置是由占位符指示的,而待插入的文本是一个键值数组。这个文本转换处理对Drupal的安全性非常重要(更多信息,可参看第20章)。图18-1向你展示了t()函数是如何处理翻译的;图20-1向你展示了t()函数是如何处理占位符的。
18-1. t()函数是如何处理翻译和占位符插入的,这里假定当前语言为法语。
 
使用自定义字符串替换内置的字符串
    用户界面的翻译,主要就是将一个字符串替换为另一个。让我们从简单的开始,仅仅选择修改几个字符串。对于翻译问题,有多种解决方案。我们将从最简单的入手,然后逐步深入。第一种方案涉及到编辑你的设置文件,而第2种方案则涉及到本地模块。让我们首先在面包屑中实现一个简单的字符串替换,然后再将Blog 替换为Journal
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 在settings.php中覆写字符串

老葛的Drupal培训班 Think in Drupal

找到你的settings.php文件(一般位于sites/default/settings.php)。在进行修改以前,你可能首先需要修改这个文件的权限,将其改为可写的,因为Drupal总是尽可能的将其保持为只读的。滚读到settings.php的结尾处。我们将添加下面的自定义字符串数组:
 
/**
 * String overrides:
 *
 * To override specific strings on your site with or without enabling locale
 * module, add an entry to this list. This functionality allows you to change
 * a small number of your site's default English language interface strings.
 *
 * Remove the leading hash signs to enable.
 */
# $conf['locale_custom_strings_en'] = array(
#  forum' => 'Discussion board',
#  @count min' => '@count minutes',
# );
 
$conf['locale_custom_strings_en'] = array(
    Home' => 'Sweet Home',
);
 
    如果你访问你的站点,你将会注意到,在面包屑中,“Home”(首页)已被修改为了“Sweet Home”(美丽家园),如图18-2所示。
    现在你应该知道如何覆写字符串了,让我们继续前进,把单词“Blog”替换为“Journal”:
 
$conf['locale_custom_strings_en'] = array(
    Blog' => 'Journal',
);
 
    接着,导航到“管理➤站点构建➤模块”,启用博客模块。然后访问“创建内容➤博客条目”,你将看到如图18-3所示的一幕。
 
18-2.面包屑中的字符串“Home”被替换为了“Sweet Home”。
 
18-3.字符串“Blog entry”未被替换为“Journal entry”。
 
    哪里出错了?为什么你自定义的字符串替换数组不起作用呢?这是因为字符串“Blog entry”与字符串“Blog”不完全相同。你不能只挑选子字符串进行替换;你必须匹配整个字符串。
    如何才能找到包含单词“Blog”的所有字符串?这样你就可以使用它的“Journal”等价字符串逐一进行替换了。本地模块可以帮你实现这一点。
 
提示 由于不需要调用数据库,所以在settings.php文件中覆写字符串是很高效的(仅用于小量的字符串集);而字符串的替换只需简单的查找一个数组就可以了。对于字符串的覆写,你不需要为其启用本地模块。另外还有一个第3方的字符串覆写模块,地址为http://drupal.org/project/stringoverrides
 

Drupal版本:

Drupal专业开发指南 第18章 使用本地模块替换字符串

 

我们在前面讲述了,通过在settings.php文件中定义一列自定义的字符串替换,来实现字符串的替换,除了这种方式以外,我们还可以使用本地模块,使用它来查找要被替换的字符串,并定义替换为什么。一个语言翻译就是Drupal的自定义字符串替换集。当Drupal准备显示一个字符串时,它将使用t()函数对字符串进行处理,正如前面所述的那样。如果它在当前语言翻译中找到了一个替换,那么它将使用该替换;如果未找到,那么它将简单的使用原始的字符串。图18-4以一种简单的形式给出了这一流程,这也正是locale()函数的所要做的。使用本地模块的方法,就是创建一个语言代码为en-US的语言,让其仅包含我们想要替换的字符串。
 
18-4.如果本地模块没有在当前语言翻译中找到一个替换,那么它将会退回来使用原始的字符串。
 
    好的,让我们现在开始,将任何包含“blog”的字符串修改为包含“journal”的字符串。因为,如果找不到翻译的话,那么Drupal将会退回来使用原始的字符串,所以我们只需要提供我们想要修改的字符串。我们可以把这些字符串放在一个自定义的语言中,对于我们没有提供的字符串,让Drupal回退到原始的字符串上去就可以了。首先,添加一个自定义语言,来保存我们的自定义字符串。添加语言的界面如图18-5所示。我们把它叫做“English-custom”,并使用“en-US”作为语言代码和路径前缀。
 
18-5.为字符串翻译添加一个自定义语言
 老葛的Drupal培训班 Think in Drupal
    现在,启用你的新语言,并将其设为默认语言,如图18-6所示。点击“保存配置”,取消对英语的选中,并再次点击“保存配置”,如图18-7所示。这样就只启用了一个语言,当用户编辑它们的帐号时,就看不到让人困惑的如图18-8所示的“语言设置”选项了。
18-6. 启用新语言,并将它设为默认语言
 
18-7.禁用英语,这样English-custom就是唯一启用了的语言。
 
18-8. “我的帐号”页面的用户界面,用户可以为站点发送的电子邮件选择一个偏爱语言。
 
    好的,你启用了名为English-custom的单个语言翻译。因为我们还没有添加任何字符串替换,所以它当前还是空的。对于每个字符串,Drupal都将使用如图18-4所示的流程对其进行处理,由于在English-custom中找不到字符串替换,所以最终会回退到原始的英文字符串上。让我们设立一些字符串替换。导航到管理界面“管理➤站点构建➤翻译界面”,如图18-9所示。
18-9. “翻译界面”的概览页面
 
    Drupal使用的是即时翻译。当一个页面被加载时,里面的每个字符串将经过t()函数和locale()函数的处理,在这里,如果发现字符串还没有出现在locales_source和locales_target数据库表中,那么它将被添加到这些表中。所以,在图18-9中“内置界面”一列的值,表示有301个字符串经过了t()的处理并可被翻译。继续前进,在Drupal中随便浏览一些其它页面,然后再回到刚才的页面。你会发现字符串的数量增加了,为什么呢?这是因为Drupal遇到了更多的需要翻译的界面。我们将使用本地模块的web界面来翻译一些字符串。
    在点击了“搜索”标签以后,我们将看到一个搜索界面,它允许我们查找需要翻译的字符串。让我们对目前可用的这301个或者更多的字符串进行搜索。搜索界面如图18-10所示。
 
18-10.用来显示可翻译字符串的搜索界面
 
    选择我们的语言(English-custom),对所有的字符串进行搜索,把搜索框置为空将显示所有的可翻译的字符串。在每个字符串的后面都紧挨着一个“编辑”链接。在字符串列表的下面,也就是页面的底部,将会再次显示搜索界面。由于字符串列表太长了,让我们限定一下它的范围,改为包含单词“Search”的字符串。在“字符串包括”字段中输入单词“Search”,并点击搜索按钮。结果将会是一列包含单词“Search”的字符串,如图18-11所示。现在让我们把字符串“Search”修改为“Search now”,点击“编辑”链接。
 
18-11.一列包含单词“search”的可翻译字符串,以及它们的状态。
 
    当你编辑了这个字符串以后,你返回到了搜索标签页面。但是,等一下!它现在变成了“Search now”标签!而搜索表单底部的按钮,也由“Search”改为了“Search now”,如图18-12所示。实际上,在单词“Search”单独出现的每个地方,它都被替换为了“Search now”。
18-12.字符串“Search”现在被替换为了字符串“Search now”
 
    继续前进,再次搜索字符串“Search”,在字符串的结果列表中,你将会看到,对于该条目,其语言列的删除线不见了,这表示该字符串已经被翻译了,如图18-13所示。
 
18-13.编辑完“Search”后的可翻译字符串列表
 
    注意,这里显示的是原始的字符串。如果你返回到概览标签,你将看到English-custom现在有了一个可用的替换字符串。
    现在你学会了如何修改字符串,我们将继续前进,把所有出现“blog”的地方改为“journal”。在启用了博客模块以后,访问博客相关的页面(比如/node/add/blog和blog/1),我们将看到等待我们翻译的字符串。在“管理➤站点构建➤翻译界面”中的搜索,是区分大小写的,所以我们需要搜索两次,一次“blog”,一次“Blog”,这样就得到所有的情况了;然后使用我们喜欢的单词“journal”和“Journal”,将它们分别修改为等价的替换字符串。
 
警告 我们这里介绍的方法,是用来对Drupal站点进行润色的,它仅针对特定界面元素的字符串替换,而不是全部的。例如,如果一个模块包含了单词“blog”,但是还没有启用,那么我们就没有翻译该模块的字符串。在本章的“开始一个新的翻译”一节中,将会介绍一个更彻底的方法。
 
    该改的都改了,看起来也不错,但是还有一点,那就是创建一个新的journal条目时所用的URL还是http://example.com/?q=node/add/blog,这一点还是有点麻烦的;能不能把它替换为http://example.com/?q=node/add/journal?当然可以。启用路径模块,添加一个别名,把node/add/blog作为已有的系统路径,把node/add/journal作为对应的别名,这样我们就可以快速的解决这个问题了。一转眼!所有的“blog”都消失了,这样在使用站点时,你再也看不到单词“blog”了,这样就顺心了。
 
提示  有个第3方模块,它使得字符串的翻译更加方便,这就是本地化客户端模块,下载地址为http://drupal.org/project/l10n_client。这个模块提供了一个在线的本地化编辑器界面,并且大量的使用了AJAX。
 

Drupal版本:

Drupal专业开发指南 第18章 导出你的翻译

 

在你选择了并翻译了你想要修改的字符串以后,当你设立一个新的Drupal站点时,如果还需要再次选择和翻译,这样不断的重复已有的工作,那么也就太丢人了。通过使用“管理➤站点构建➤翻译界面”中的导出标签,你可以把翻译保存在一个特定的PO文件中(便携式对象文件)。这个文件将包含Drupal传递给t()的所有字符串,还有你已经定义的任何替换字符串。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 便携式对象文件

 

导出我们的English-custom翻译所生成的文件,它的前面几行如下所示:
 
# English-custom translation of Drupal 6
# Copyright (c) 2007 drupalusername <me@example.com>
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"POT-Creation-Date: 2008-05-09 12:46-0500\n"
"PO-Revision-Date: 2008-05-09 12:46-0500\n"
"Last-Translator: drupalusername <me@example.com>\n"
"Language-Team: English-custom <me@example.com>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
 
#: /example.com/?q=admin/build/translate/search
msgid "Search"
msgstr "Search now"
 
#: /example.com/?q=node/add/blog
msgid "blog"
msgstr "journal"
 
#: /example.com/?q=admin/build/modules/list/confirm
msgid "Blog entry"
msgstr "Journal entry"
 
#: /example.com/?q=admin/build/translate/search
msgid ""
"A <em>blog entry</em> is a single post to an online journal, or "
"<em>blog</em>."
msgstr "A <em>journal entry</em> is a single post to an online journal."
...
 
    .po文件包含两部分,头部为一些元数据,后面跟着翻译了的字符串。每个字符串包含3部分:1个注释,说明了字符串第一次出现的位置;一个msgid,表示原始字符串;一个msgstr,表示翻译了的字符串。关于.po文件格式的更全面的描述,参看http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files
    使用“管理➤站点构建➤翻译界面”中的导入标签,现在就可以将en-US.po文件导入到另一个Drupal站点中了(该站点也启用了本地模块)。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 便携式对象模板

 

    一个翻译包含了一些元数据和大量的原始的和已翻译的字符串;而一个便携式对象模板文件(后缀为.pot)则包含了所有的待翻译的原始字符串,而不包含任何翻译了的字符串。.pot文件在一些场合是非常有用的,比如当你从头开始一门语言的翻译时,或者在修改你的站点以前,想判定从最终版本到现在为止,是否向Drupal中添加了一些新字符串时(找出这些字符串的另一种方式是,复制你的Drupal站点,并对其进行更新,搜索未翻译的字符串,如本章“使用自定义字符串替换内置的字符串”一节所讲)。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 开始一个新的翻译

 

Drupal的用户界面已被翻译成了许多语言。如果你想成为一名志愿者来帮助翻译的话,社区是非常欢迎你的。每个已有的语言翻译,都有一个项目页面用来追踪其发展。例如,法语翻译就位于http://drupal.org/project/fr。在翻译的论坛里面可以找到与翻译有关的一般帮助http://drupal.org/forum/30
 
注意 一些专业的翻译者,在使用英语以外的语言时,不会使用本章前面介绍的字符串替换方法。他们习惯使用.po和.pot文件,并通常使用一些特殊的软件来帮助他们管理翻译(参看http://drupal.org/node/11131)。还可参看另一个项目,一个基于web的翻译工具,地址为:http://drupal.org/project/l10n_server
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 用户界面翻译

 

Drupal的界面是由字儿、短语、和句子组成的,用来向用户传递信息。在接下来的部分中,你将看到如何修改它们。我们的例子将集中于字符串替换,同时我们需要理解----翻译就是建立在字符串替换的基础之上的。
 
字符串
    从一个编程的观点来看,一个字符串就是一系列的字符,比如5位字符的字符串Hello。在Drupal中,字符串的翻译构成了用户界面翻译的基础。当Drupal准备一个用来输出的字符串时,它将检查这个字符串是否需要被翻译一下,如果启用了英语,那么将显示“Hello”;如果启用了法语,那么将显示“Bonjour”。让我们看一下这是怎么实现的。
 
使用t()翻译字符串
    在Drupal中显示给终端用户的所有字符串,都须经过t()函数的处理;这个就是Drupal的翻译函数,由于经常使用,为了方便起见,所以将它简写为“t”。
 
注意Drupal中,有些地方隐含的使用了t(),比如传递给watchdog()的字符串,或者菜单钩子中的标题和描述。复数形式使用format_plural()来翻译,该函数负责调用t()(参看http://api.drupal.org/api/function/format_plural/6)。
 
    t()函数中特定于本地的部分,如下所示:
 
function t($string, $args = array(), $langcode = NULL) {
    global $language;
    static $custom_strings;
 
    $langcode = isset($langcode) ? $langcode : $language->language;
 
    // First, check for an array of customized strings. If present, use the array
    // *instead of* database lookups. This is a high performance way to provide a
    // handful of string replacements. See settings.php for examples.
    // Cache the $custom_strings variable to improve performance.
    if (!isset($custom_strings[$langcode])) {
        $custom_strings[$langcode] = variable_get('locale_custom_strings_'.
            $langcode, array());
    }
    // Custom strings work for English too, even if locale module is disabled.
    if (isset($custom_strings[$langcode][$string])) {
        $string = $custom_strings[$langcode][$string];
    }
    // Translate with locale module if enabled.
    elseif (function_exists('locale') && $langcode != 'en') {
        $string = locale($string, $langcode);
    }
    if (empty($args)) {
        return $string;
    }
    ...
}
 
    除了负责翻译以外,t()函数还负责向字符串中的占位符插入相应的值。这些值通常是用户提供的输入,它们在显示以前,必须经过文本转换处理。
 
t('Hello, my name is %name.', array('%name' => 'John');
Hello, my name is John.
 
    文本中待插入的位置是由占位符指示的,而待插入的文本是一个键值数组。这个文本转换处理对Drupal的安全性非常重要(更多信息,可参看第20章)。图18-1向你展示了t()函数是如何处理翻译的;图20-1向你展示了t()函数是如何处理占位符的。
18-1. t()函数是如何处理翻译和占位符插入的,这里假定当前语言为法语。
 
使用自定义字符串替换内置的字符串
    用户界面的翻译,主要就是将一个字符串替换为另一个。让我们从简单的开始,仅仅选择修改几个字符串。对于翻译问题,有多种解决方案。我们将从最简单的入手,然后逐步深入。第一种方案涉及到编辑你的设置文件,而第2种方案则涉及到本地模块。让我们首先在面包屑中实现一个简单的字符串替换,然后再将Blog 替换为Journal
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 获取Drupal的.pot文件

 

Drupal最终的.pot文件,可以在http://drupal.org/project/drupal-pot下载。下载你感兴趣的Drupal分支的.tar.gz文件,并将其进行解压缩,这样你将得到一个目录,里面包含了对应于Drupal文件的.pot文件。例如aggregator-module.pot包含了Drupal的聚合器模块中的可翻译的字符串。
 
$ gunzip drupal-pot-6.x-1.0.tar.gz
$ tar -xf drupal-pot-6.x-1.0.tar
$ ls drupal-pot
LICENSE.txt             modules-dblog.pot       modules-statistics.pot
README.txt              modules-filter.pot      modules-syslog.pot
general.pot             modules-forum.pot       modules-system.pot
includes.pot            modules-help.pot        modules-taxonomy.pot
installer.pot           modules-locale.pot      modules-throttle.pot
misc.pot                modules-menu.pot        modules-tracker.pot
modules-aggregator.pot modules-node.pot        modules-translation.pot
modules-block.pot       modules-openid.pot      modules-trigger.pot
modules-blog.pot        modules-path.pot        modules-update.pot
modules-blogapi.pot     modules-php.pot         modules-upload.pot
modules-book.pot        modules-ping.pot        modules-user.pot
modules-color.pot       modules-poll.pot        themes-chameleon.pot
modules-comment.pot     modules-profile.pot     themes-garland.pot
modules-contact.pot     modules-search.pot      themes-pushbutton.pot
 
    在这个目录中,你还将注意到一些其它文件。有一个用来提供信息的README.txt文件(读它一遍!),一个名为general.pot的文件,一个名为installer.pot的文件。general.pot文件包含了多次重复出现的字符串,翻译时一般从它开始。installer.pot包含的是安装器界面的字符串,如果你需要为安装器创建一个翻译,那么必须翻译该文件所包含的字符串。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 使用翻译模板提取器生成.pot文件

 

3方的翻译模板提取器模块(参看http://drupal.org/project/potx),可以用来为你生成.pot文件。如果你编写了一个自己的模块,或者下载了一个第3方的模块,当需要为其创建一个翻译时,该模块会非常有用。翻译模板提取器模块包含了两个提取器:一个是命令行的版本,一个是基于web的版本。如果你熟悉Unix下的xgettext程序,你可以把这个模块看成是该程序的Drupal版本。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 为你的模块创建一个.pot文件

 

我们在第2章中创建了一个注释模块,现在让我们为该模块生成一个.pot文件。首先,我们需要下载翻译模板提取器模块,下载地址http://drupal.org/project/potx,解压该模块的压缩包,将加解压后的文件放在sites/all/modules/potx。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 使用命令行

 

potx.inc和potx-cli.php拷贝到注释模块的目录sites/all/modules/custom/annotate下。接着,我们需要运行提取器,这样它就可以创建.pot文件了。
 
警告  你向你的Drupal站点添加了一个可执行的PHP脚本,它需要对它运行在的目录具有写权限(这样它就可以写入.pot文件了)。千万不要在一个在线的站点上执行模板提取,你需要把网站拷贝一份,放在你开发用的机器上,然后再执行模板提取。
 
    下面是运行提取器时返回的结果:
 
$ cd sites/all/modules/custom/annotate
$ php potx-cli.php
Processing annotate.admin.inc...
Processing annotate.module...
Processing annotate.install...
Processing annotate.info...
Done.
 
    让我们看一下所生成的文件:
annotate.admin.inc      general.pot
annotate.info           potx-cli.php
annotate.install        potx.inc
annotate.module
 
    运行提取器脚本后将生成一个新的文件general.pot,它包含了来自于annotate.module, annotate.info, 和annotate.install的字符串。该脚本在默认的情况下把所有的字符串都放在了general.pot中,但是,如果你喜欢的话,它也可以生成单独的文件。运行
 
$ php potx-cli.php –-help
 
    来查看提取器脚本提供的各种选项。在当前的这种情况下,把所有的字符串都放在一个文件中,这会比较方便。如果我们需要把这个翻译模板分享给他人,那么我们可以在annotate目录下创建一个translations子目录,把general.pot移到translations目录下,并将其重命名为annotate.pot。接着,我们可以打开这个合成的.pot文件,将其翻译成法语,然后将其保存为fr.po,这样我们模块的目录就变成了这样:
 
annotate.admin.inc
annotate.info
annotate.install
annotate.module
translations/
    annotate.pot
    fr.po
老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 使用基于web的提取器

 

除了前面介绍的命令行方式以外,翻译模板提取器模块还提供了一个基于web的用户界面,你可以使用它来从你的模块中提取字符串。确保你已经下载了该模块,并把它放在了sites/all/modules/potx下,这和前面所述的一样,接下来,导航到“管理➤站点构建➤模块”,启用注释模块和翻译模板提取器模块。接着,导航到“管理➤站点构建➤翻译界面”,你会看到新的提取标签。点击这个标签,在标签页面选择“语言独立模板”,并点击提取按钮,这样你就可以生成一个.pot文件了,如图18-14所示。通过你的web浏览器,将.pot文件下载到本地,接着你可以把它放在sites/all/custom/annotate/translations中,此时又和使用命令行提取器时一样了。
 
18-14.使用翻译模板提取器模块的基于web的用户界面,为注释模块提取一个.pot文件。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 为整个站点创建.pot文件

 

如果你想为你站点中的所有可翻译的字符串创建.pot文件,那么你需要把potx.inc和potx-cli.php文件放在你站点的根部,确保你对当前目录具有写权限,然后运行potx-cli.php。如果你想让生成的.pot文件,与http://drupal.org/project/Translations中.pot文件的布局保持一致,那么在使用命令行运行该脚本时,你需要将mode参数设置为core:
 
$ php potx-cli.php --mode=core
 
    脚本总是把.pot文件输出到它所在的目录中;例如, modules-aggregator.pot文件将会创建在你站点的根目录下,而不是在modules/aggregator/下面。.pot文件的名字反映了其对应模块所在的位置。所以在前面的例子中,将会生成一个sites-all-modules-custom-annotate.pot文件。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 安装一个语言翻译

 你可以在安装Drupal时选择一个英语以外的语言,也可以在安装后添加一个语言翻译。我们将讲解这两种可能。

 
在安装时设立一个翻译
    Drupal的安装器使用st()函数来识别安装器翻译,而不是使用t(),这是因为在安装器运行时,Drupal还没有完成安装,所以此时不能使用t()。安装器翻译是基于installer.pot文件的(参看“获取Drupal的.pot文件”一节),在安装期间,它将作为一个选择出现。
    为了实际的查看安装器的翻译能力,让我们从http://drupal.org/project/Translations下载Drupal的法语翻译。这将得到一个fr-6.x-1.x.tar.gz文件。根据这个文件名的后缀,我们可以看出,它是一个使用GZIP压缩了的.tar文件。提取该文件的一种方式就是使用Unix的tar命令:
 
$ tar -xzvf fr-6.x-1.x.tar.gz
 
警告 该文件包含的目录结构与Drupal的目录结构一致。当提取它时,如果你的提取方法是在提取的同时,完成与Drupal已有目录的合并,那么你需要小心了。在Mac OS X中,默认的提取器无法实现这一点。如果你在提取以后,得到了一个fr-6.x-1.x-dev文件夹,那么这就表示没有进行合并。参看http://www.lullabot.com/videocast/installing-drupal-translation,它使用图文的方式说明了执行提取的正确方式。译者注:分两步就可以了,解压缩该文件,然后拷贝到Drupal目录中。
 
    在成功的提取了翻译以后,在你的Drupal目录中,你会找到许多额外的名为translations的文件夹。例如,在profiles/default文件夹下(Drupal的默认安装轮廓所在的地方),就有了一个translations子文件夹,里面包含了一个fr.po文件。这就是安装器的法语翻译,当Drupal的安装器运行时,你可以看到一个新的选择出现了,如图18-15所示。
    如果你选择了法语,那么将使用法语进行安装,并且站点的默认语言也将被设置为法语。
 
18-15. 当在安装轮廓的translations子目录下存在一个.po文件时,Drupal的安装器将允许你为它选择一个语言。
 

老葛的Drupal培训班 Think in Drupal

 

Drupal版本:

Drupal专业开发指南 第18章 在已有站点上安装一个翻译

 

为了在一个已有的站点上安装一个语言翻译,你需要像前面一节所讲的那样,采用相同的提取步骤。当成功的提取了翻译文件以后(你可以检查translations管理➤站点配置➤语言”,点击“添加语言”标签,你就可以添加语言了。接着,找出对应于语言翻译文件的语言,简单的选择它,并点击“添加语言”按钮,如图18-16所示。如果你已经正确的提取了翻译文件,那么Drupal将会显示一个进度条,表示它正在安装翻译。导航到“管理➤站点配置➤语言”,你就可以看到新装的语言了。子目录是否已经存在了),导航到“
(译者注:这里的“提取”,指的是将语言包中的po文件提取到Drupal站点目录中)
 
18-16.安装一个语言。(在点击添加语言按钮时,必须已经正确提取了翻译文件。)
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 支持从右到左的语言

 

语言的方向性,显示在已添加到Drupal中的语言翻译的列表中,如图18-17所示。
18-17. 从右到左的语言可以用语言表格中的“读写方向”列标识出来。
 
    Drupal对从右到左的语言的支持,比如以色列语,是在主题层实现的。当Drupal为当前页面加载一个样式表时,如果当前语言的读写方向为从右到左,那么Drupal将检查是否存在一个以-rtl.css结尾的对应的样式表。如果该样式表也存在,那么它也将和被请求的样式表一起被加载进来。图18-18给出了这一逻辑。因此,对于支持从右到左的语言的主题,一般把样式定义在主样式表中,而把覆写的CSS定义在对应的从右到左的样式表中。
    例如,如果当前语言为以色列语,主题为Bluemarine,当Drupal添加样式表themes/bluemarine/style.css时,文件themes/bluemarine/style-rtl.css也将被包含进来。在Drupal的默认主题中,检查从右到左的样式表,来看看都有哪些CSS元素被覆写了。
    导航到“管理➤站点配置➤语言”,点击相应语言的“编辑”链接,在编辑页面,你可以修改语言的读写方向。
    如果想测试当前语言的方向性,那么可以使用下面的代码:
 
if (defined('LANGUAGE_RTL') && $language->direction == LANGUAGE_RTL) {
    // Do something.
}
 
    由于常量LANGUAGE_RTL是由本地模块定义的,所以如果没有加载本地模块,那么也就无法支持从右到左的语言,因此上面的代码是有效的。
18-18.如果当前语言的读写方向为从右到左,如果附加的样式表存在的话,那么它将会被包含进来。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 语言协定

 

Drupal实现了用来判定用户语言的大多数常见方式,这样当一个Drupal站点启用了多个语言时,那么将会使用用户首选的语言。在接下来的部分中,我们将假定安装了Drupal的法语翻译,正如前面所述的那样。导航到“管理➤站点配置➤语言”,点击配置标签,在这里就可以对Drupal判定语言设置的方式进行配置了。相应的用户界面如图18-19所示。让我们检查一下每个选项。
 
18-19.语言协定的可能设置
 
一个也没有
    这是默认选项,也是最简单的一个。在显示页面时,将为所有的用户使用站点的默认语言。指定默认语言的用户界面,可参看图18-17。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 用户首选语言

 

如果启用了多个语言,当用户编辑他们的“我的帐号”页面时,他们将看到如图18-20所示的字段集。
18-20.为电子邮件消息选择一个特定于用户的语言。
 
    可以按照下面的方式取回用户所选的语言:
 
// Retrieve user 3's preferred language.
$account = user_load(array('uid' => 3));
$language = user_preferred_language($account);
 
    如果用户还没有设置一个首选的语言,那么将会返回站点的默认语言。结果将会是一个语言对象(关于语言对象的更多详细,参看下一节)。如果“语言协定”设置被设置为了“一个也没有”,那么只有在站点发送电子邮件时,才会使用用户的首选语言。在其它情况下,用户的首选语言都不起作用,比如显示页面时就不会使用用户首选的语言。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 全局$language对象

 

通过查看全局变量$language(一个对象),你可以通过编程的方式判定当前的语言。在引导指令的DRUPAL_BOOTSTRAP_LANGUAGE部分,该变量被初始化。通过执行一个var_dump(),你可以查看该变量的详细:
 
global $language;
var_dump($language);
 
结果如下所示:
object(stdClass) (11) {
    ["language"]    => string(2) "fr"
    ["name"]        => string(6) "French"
    ["native"]      => string(9) "Français"
    ["direction"]  => string(1) "0"
    ["enabled"]     => int(1)
    ["plurals"]     => string(1) "2"
    ["formula"]     => string(6) "($n>1)"
    ["domain"]      => string(0) ""
    ["prefix"]      => string(2) "fr"
    ["weight"]      => string(1) "0"
    ["javascript"] => string(0) ""
}
 
    通过$language对象的language属性,可以取回RFC 4646语言标识符(比如,前面例子中的fr):
 
global $language;
$lang = $language->language;
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 仅使用路径前缀

 

当语言协定被设置为了“仅使用路径前缀”时,这里只有两种可能性。要么在路径中找到了语言路径前缀,要么使用默认语言。假定你创建了一个站点,同时能够支持英语和法语。英语是站点的默认语言,但是法语翻译也被安装和启用了。导航到“管理➤站点配置➤语言”,点击法语旁边的“编辑”链接,这样你将看到一个如图18-21所示的用户界面。注意,“路径前缀”字段被设置为了fr。这个值可被修改为任意的字符串。
    由于路径前缀设置为了fr,所以Drupal就可以通过查看被请求的URL来判定当前语言。流程如图18-22所示。
18-21.编辑语言的用户界面,显示了“路径前缀”字段
 
18-22.使用法语的路径前缀来判定语言
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 带有语言回退的路径前缀

 

当把“语言协定”设为这个设置时,Drupal将首先考虑路径前缀。如果一个匹配的也没有,那么将使用$user->language来检查用户首选的语言。如果用户没有选择首选语言,Drupal接着考虑浏览器的HTTP请求中Accept-language HTTP头部,来尝试着判定用户的首选语言。如果浏览器也没有指定一个首选语言,那么将使用站点的默认语言。假定英语是站点的默认语言,同时还启用了法语和以色列语,语言判定的流程如图18-23所示。
18-23.使用“带有语言回退的路径前缀”来判定语言
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 仅使用域名

 

当把“语言协定”设为这个设置时,Drupal将通过尝试对当前URL的域和语言域进行匹配,来判定当前语言,这里的语言域,指的是一个语言的“编辑语言”页面中“语言域”字段所指定的语言域(如图18-21所示)。例如,把英语作为默认语言,把http://fr.example.com指定为法语的语言域,那么当用户访问http://fr.example.com/?q=node/2时,当前语言将被设置为法语;当用户访问http://example.com/?q=node/2时,当前语言将被设置为英语。
 
注意 在“语言协定”被设置为“仅使用域名”时,来自“我的帐号”页面的用户首选语言和客户端浏览器的设置将被忽略。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 内容翻译

 到目前为止,我们所讲的主要是Drupal用户界面的翻译。但是,内容该怎么办呢?一旦判定了当前语言设置,那么用户极有可能想查看该语言下的站点内容!现在,让我们学习一下内容翻译的工作原理。

 
介绍内容翻译模块
    Drupal内置了一种方式,用来管理内容翻译,这就是内容翻译模块。这个模块向Drupal的内容类型添加了附加的多语言支持和翻译管理选项。
 
多语言支持
    导航到“管理➤站点构建➤模块”,启用本地和内容翻译模块,“多语言支持”选项将会出现在每个内容类型的“工作流设置”字段集中。为了查看这些设置,导航到“管理➤内容管理➤内容类型”,点击Page内容类型的“编辑”链接,展开“工作流设置”字段集,这样你将会看到“多语言支持”这一新设置,如图18-24所示。本地模块提供了“已禁用”和“已启用”设置,而内容翻译模块提供了“已启用,带有翻译”设置。
 
18-24.一个内容类型的多语言设置
 
    现在,如果你导航到“创建内容➤Page”,那么你将在内容创建表单上看到一个新的下拉选择框,它允许你选择内容所在的语言,或者内容是“语言中立的”。图18-25给出了该字段。
 
18-25.内容创建表单上的语言选择字段
 
    使用不同的语言创建了一些页面以后,在内容的管理界面“管理➤内容管理➤内容”,你可以看到新增了“语言”一列。另外,还多了一个语言选项,用来过滤内容,如图18-26所示。
 
18-26.启用了多语言支持的内容管理界面

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 带有翻译的多语言支持

 能够使用多个语言创建内容固然不错。然而,大多数的站点,都不会有一篇内容使用英语,另外一篇内容使用法语,两者没有任何关系。相反,法语内容通常是英语内容的一个翻译(或者反之亦然)。当把内容类型的“多语言支持”设为“已启用,带有翻译”(参看图18-24)时,那就变成可能的了。它涉及到以下步骤:

 
1. 使用一个语言创建一篇文章。这就是源文章。
2. 创建文章的翻译。
 
    让我们使用一个例子,来逐步的实现这些任务。首先,确保Page内容类型的“多语言支持”设置已被设为 “已启用,带有翻译”。接着,我们使用英语创建一个简单的页面。导航到“创建内容➤Page”,为标题输入Hello,为正文输入Have a nice day.。将语言设为英语,并点击保存按钮。除了通常的查看和编辑标签以外,你现在应该还会看到一个翻译标签(参看图18-27)。
 
18-27.节点现在有了一个翻译标签。
 
    点击翻译标签,将会显示该文章的翻译状态的总结。如图18-28所示,在英语中已有了一篇源文章,但是现在也只有这些了。通过点击“添加翻译”链接,让我们创建一个法语翻译。
18-28.点击翻译标签,显示了翻译状态的总结。
 
    点击“添加翻译”链接,将再次来到节点编辑表单页面,但是这次,语言被设置为了法语。为标题输入Bonjour,为正文输入Ayez un beau jour.。点击保存按钮,这样就新增了一个节点。Drupal将自动的在源节点和翻译之间创建链接,并使用语言名字作为链接的文本。图18-29给出了源节点的法语翻译的外观,在这里,源节点使用的是英语,同时还有一个使用以色列语的附加翻译。
18-29.源节点的法语翻译包含了指向英语和以色列语版本的链接。
 
    通过实现modules/translation/translation.module中的hook_link(),来构建这些链接:
 
/**
 * Implementation of hook_link().
 *
 * Display translation links with native language names, if this node
 * is part of a translation set.
 */
function translation_link($type, $node = NULL, $teaser = FALSE) {
    $links = array();
    if ($type == 'node' && ($node->tnid) &&
    $translations = translation_node_get_translations($node->tnid)) {
        // Do not show link to the same node.
        unset($translations[$node->language]);
        $languages = language_list();
        foreach ($translations as $language => $translation) {
            $links["node_translation_$language"] = array(
                'title'      => $languages[$language]->native,
                'href'       => "node/$translation->nid",
                'language'  => $languages[$language],
                'attributes' => array(
                    'title' => $translation->title,
                    'class' => 'translation-link'
                )
            );
        }
    }
    return $links;
}
 
    除了内容翻译模块生成的链接以外,本地模块还提供了一个语言转换区块,你可以在“管理➤站点构建➤区块”中启用它。只有当启用了多个语言,并且“语言协定”设置没有被设为“一个也没有”时,语言转换区块才会显示出来。语言转换区块如图18-30所示。
 
18-30. 语言转换区块
 
    让我们回到正题,继续讨论源节点和它们的翻译。如果一个节点是一个源节点,那么编辑它时将会在节点编辑表单中显示一个附加的名为“翻译设置”的字段集。这个字段集包含了单个复选框:“把翻译标记为过时的”,如图18-31所示。
 
18-31.一个源节点的节点编辑表单中的“翻译设置”字段集
 
    复选框是用来指示是否需要重新翻译的,如果你对源节点作了比较大的更改,也就是说原有的翻译已不再适用,那么就需要重新翻译了。选中复选框,将把翻译标记为过时的,这样在查看节点的翻译状态时,就会显示一个红色的提示“过时的”,对比图18-28和图18-32。
 
18-32.源节点已被编辑,而翻译的文章也被标记为过时的。
 
    源节点和它的翻译都有单独的节点ID,事实上,在数据库中它们就是完全独立的节点。它们通过使用node表的tnid列关联起来,该列包含的值,就是源节点的节点ID。假定英语版本就是源节点,并且是站点的第一个节点,而法语和以色列语翻译是紧接着添加的两个节点,那么node表将如图18-33所示。
 
18-33. tnid列用来追踪源节点和它们的翻译之间的关系
 
    注意translate列中的1,用来指示一个过时的翻译。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 本地化和翻译相关的文件

 有时,想知道是由Drupal中的哪一部分负责某一本地化或翻译功能的,这是非常困难的。表18-1给出了这些文件和它们的职责。

 
18-1. Drupal内部与本地化和翻译相关的文件

 

文件
职责
includes/bootstrap.inc
运行DRUPAL_BOOTSTRAP_LANGUAGE阶段,判定当前语言
includes/language.inc
如果启用了多语言,那么它将被引导指令包含进来。可用来选择语言和将内部URL重写为特定于语言的
includes/common.inc
t()所在的地方,另外还有drupal_add_css(),后者支持从右到左的语言。
includes/locale.inc
包含了用户界面和管理语言翻译的函数。
modules/locale/locale.module
当安装或启用模块/主题时,它用来提供字符串替换和翻译导入。它向路经、节点、节点类型的表单中添加语言设置界面。
modules/translation/translation.module
管理源节点和对应的翻译
modules/translation/translation.admin.inc
当点击翻译标签时,它提供翻译概览页面(参看图18-31)。

 

 
附加资源
    国际化支持对于Drupal项目非常重要。为了追踪国际化的最新进展或者想参与进来,可参看http://groups.drupal.org/i18n
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第18章 总结

 

在本章中,你学到了以下知识:
    • t()函数的工作原理
    • 如何定制内置的Drupal字符串
    • 如何导出你定制的翻译
    • 什么是便携式对象文件和便携式对象模板文件
    • 如何下载便携式对象模板文件和生成你自己的
    • 如何导入一个已有的Drupal翻译
    • 如何使用样式表来支持从右到左的语言
    • 语言协定设置是如何影响Drupal的
    • 内容翻译的工作原理

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 编写安全的代码

几乎每天我们都可以看到关于安全漏洞的头条消息,这种或者那种软件出现了漏洞。对于每个严谨的开发者来讲,将恶意用户拒之门外就是头等大事。

一个恶意用户可以使用多种方式来攻击你的Drupal站点。这些攻击方式包括,向你的系统中注入代码并让它执行,操纵你数据库中的数据,查看用户无权访问的资料,通过你的Drupal站点发送垃圾邮件。在本章,我们将学习如何编写安全的代码来阻止这些攻击。
幸运的是,Drupal提供了一些工具,使用它们你就可以很容易的消除这些常见的安全漏洞。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 处理用户输入

 

当用户与Drupal交互时,一般都是通过一系列的表单来完成的,比如节点提交表单、评论提交表单。用户也可能使用blogapi module来发布一个基于Drupal的日志。Drupal的用户输入方式可以总结为“存储原始的;过滤输出的”(store the original; filter on output)。这样,数据库中总会保存一份与用户输入内容完全一致的备份。当用户输入的内容准备用来生成web页面输出时,就应该进行过滤了,这样潜在的恶意代码就被中和了。
当用户输入的内容没有经过安全处理就在你的程序中执行时,就可能引起安全漏洞。当你编写你的程序时,如果你没有全面的考虑各种情况的话,就会留下安全隐患。你可能期望用户输入标准的字符,而事实上他们可以输入非标准的字符串,比如控制字符。你可能看到其中包含字符%20的URL;例如,http://example.com/my%20document.html。这是一个为了与URL规范(参看http://www.w3.org/Addressing/URL/url-spec.html)兼容,对空格字符进行编码后的结果。当有人保存了一个名为my document.html的文件,并且可从web服务器上请求它,那么就会对空格编码。%意味着编码了的字符,而20则因为这它是ASCII字符 32(这里的20是32的16位进制表示)。恶意用户可通过各种诡计,来使用编码字符给你的网站带来麻烦,你将会在本章的后面看到这一点。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 考虑数据类型

当在一个像Drupal这样的系统中处理文本时,由于用户输入将会作为站点的一部分展示出来,所以如果将用户输入看作一个带有类型的变量,那么就能帮我们很好的理解这个系统。如果你使用过强类型语言写过程序,比如JAVA,那么你将熟悉强类型变量。例如,在Java中,一个整数就是一个整数。在PHP(弱类型语言)中,使用PHP的自动类型转换,根据上下文,你既可以把整数看作字符串,也可以看作整数。但是优秀的PHP程序员都会仔细的考虑类型,并恰到好处的利用自动类型转换。同样,尽管是通过用户输入得到的,节点提交表单中的“Body”(主体)字段,也可以作为一个文本进行处理,如果我们把它看作具有特定类型的文本,那么就会更好的理解的它的本质。用户输入的是纯文本么?用户输入的文本中是否带有HTML标签,如果带有的话是否将它们也一同显示出来?如果带有HTML标签的话,这些标签中是否允许带有恶意的标签,比如JavaScript,它可以将你的页面替换成一个手机铃声的广告?在展示给用户的页面中,采用HTML格式;用户输入是各种文本格式类型的变体,在展示它们以前,必须安全的将其转化为HTML。如果我们使用这种方式来考虑用户输入的话,这能够帮助我们理解Drupal的文本转换功能的工作原理。文本输入的常见类型,还有将文本转化为另一种格式的函数,如表20-1所示。

 
20-1 将一种文本类型安全的转化为另一种类型
源格式      目标格式            Drupal 函数              功能
Plain text         HTML               check_plain()           将特定字符编码为HTML实体
HTML text         HTML               filter_xss()       使用一组标签,检查和清理HTML
Rich text          HTML               check_markup()     使用过滤器过滤
Plain text         URL                drupal_urlencode() 将特定字符编码为%0x
URL                HTML               check_url()        清除有害的协议,比如javascript,
Plain text         MIME               mime_header_encode() 编码非ASCII字符, UTF-8编码字符
 

老葛的Drupal培训班  Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 Plain text(纯文本)

Plain text(纯文本)

Plain text 是仅仅包含纯文本的文本。例如,如果你让一个用户在一个表单中键入他/她喜欢的颜色,你期望用户输入“green”(绿色)或者 “purple”(紫色),而不包含任何标识字体。如果在另一个网页中,包含这个输入框,但是却没有不进行任何检查来确保它真的是仅仅包含纯文本,那么就会留下安全漏洞。例如用户没有输入一个颜色,而输入了一下内容:
 
 
因此,我们可以使用check_plain()来确保通过将HTML标签转义为HTML实体来消除潜在的危害。从check_plain()返回的文本不包含任何HTML标签,因为将它们转换为相应的实体了。如果一个用户输入了前面的恶意JavaScript代码,那么check_plain()函数就会将其转化为以下文本,现在它就没有危害了:
 
&lt;img src=&quot;javascript:window.location =&#039;&lt;a
 
HTML text(HTML文本)
HTML text 可以包含HTML标识字体。然而,你永远不要盲目的相信用户,仅仅输入了安全的HTML;一般情况下,你想将用户能够使用标签限制在一个特定的可用的HTML标签子集中。例如,<script>一般都会被你禁用,因为它允许用户在你的站点上运行他们选择的脚本。同样,你也不想让用户使用<form>标签,以在你的站点上建立表单。
 
Rich text(富文本)
Rich text是比纯文本包含更多信息的文本,它不一定必须是HTML。它可以包含wiki标识字体,或者论坛代码(BBCode),或者其它的标识语言。在展示以前,必须使用一个过滤器来将这些文本转化为HTML。
 
注意 更过关于过滤器的信息,参看第11章。
 
URL
URL是由用户输入的,或者其它不可信的地方获取的URL。你可能希望用户输入http://example.com,但是用户却输入了javascript:runevilJS()。在将URL展现在HTML页面以前,你必须使用check_url()来对其进行检查,以确保它的格式良好并且不包含任何攻击。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 使用check_plain()和t()来清理输出

当你对你用到的文本不信任,并且你不想在文本中有任何markup(标识字体)时,那么就可以使用check_plain()。

下面是使用用户输入的原始方式,假定用户刚刚在一个文本输入框中输入了一个喜欢的颜色。
下面的代码不安全:
 
drupal_set_message("Your favorite color is $color!"); // No input checking!
 
下面的代码安全,但不是最佳实践:
 
drupal_set_message('Your favorite color is ' . check_plain($color));
 
这段代码不好,这是因为它没有把这个文本字符串(这里指的是check_plain()函数输出的结果)放到t()中,对于文本字符串总是需要调用该函数的。如果你像上面这样编写代码,那你就等着挨骂吧,翻译者将不能够翻译你的语句,因为它没有使用t()函数。
你不能将变量直接放到双引号中,并将它们传递给t()。下面的代码仍然不安全,因为它没有使用占位符:
 
drupal_set_message(t("Your favorite color is $color!")); // No input checking!
 
t()函数提供了一种内置的方式用来确保你的字符串的安全性,使用一个带有单字符前缀的占位符,如下所示。
下面的代码很安全并且格式良好:
 
drupal_set_message(t('Your favorite color is @color', array('@color' => $color));
 
    注意数组中的键(@color)与字符串中的占位符完全相同。消息的结果如同下面的这样:
 
Your favorite color is brown.
 
前缀@告诉t(),要对替换占位符的值调用check_plain()。
 
注意 当运行一个Drupal 的t()函数时,将对占位符的值调用check_plain(),对于其它的字符串则不调用check_plain()。所以你需要信任你的翻译者。
 
在这里,我们可能想通过改变颜色值的样式,来强调用户所选择的颜色。使用%前缀可以达成所愿,它意味着“对于该值执行theme('placeholder', $value)”。它间接的将值传递给了check_plain(),如图20-1所示。前缀%是最常用的前缀。
 
       下面的代码是安全的并且格式良好:
 
drupal_set_message(t('Your favorite color is %color', array('%color' => $color));
 
一个消息产生的结果如下所示。使用theme_placeholder()对该值进行了主题化,它简单的使用了<em></em>标签对值进行了包装。
 
Your favorite color is brown.
 
    如果你的文本在前面已被清理过了,你可以使用前缀!来禁用t()中的检查。例如,l()函数构建了一个链接,在构建链接时,它通过check_plain()处理了链接文本。所以在下面的例子中,可以安全的使用前缀!:
 
// l() function runs text through check_plain() and returns sanitized text
// so no need for us to do check_plain($link) or to have t() do it for us.
$link = l($user_supplied_text, $path);
drupal_set_message(t('Go to the website !website', array('!website' => $link));
 
注意 除非你通过在选项参数中将html设置为TRUE,以对l()指示链接文本已经是HTML格式了,否则l()函数仍然将链接文本通过check_plain()进行处理。参看http://api.drupal.org/api/function/l/6
 
t()函数中,用于字符串替换的@, %, 和 !占位符的作用,如图20-1所示。在该图中我们是用了一个简单的例子进行说明,记住你可以在字符串中使用多个占位符并将它们添加到数组中,例如:
 
drupal_set_message(t('Your favorite color is %color and you like %food',
array('%color' => $color, '%food' => $food)));
 
使用前缀!时要小心一点,因为该字符串没有经过check_plain()处理。
 
20-1 字符串替换时,不同占位符前缀的作用
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 使用filter_xss()阻止跨站点脚本攻击

跨站点脚本(XSS)是攻击网站的一种常用方式,攻击者可以向一个网页插入他/她自己的代码,然后使用这些代码进行各种破坏活动。

 
     注意:XSS攻击的例子,参看http://ha.ckers.org/xss.html
 
假定你允许用户向你的网站输入HTML,期望他们这么输入
 
<em>Hi!</em> My name is Sally, and I...
 
但是他们输入了
 
 
    哎哟!我们又学了一课:永远不要信任用户输入。下面是函数filter_xss()的签名:
filter_xss($string, $allowed_tags = array('a', 'em', 'strong', 'cite', 'code',
'ul', 'ol', 'li', 'dl', 'dt', 'dd'))
 
    函数filter_xss()对传递给它的文本字符串进行以下操作:
 
1.它通过检查确保被过滤的文本是有效的UTF-8,从而阻止IE6下面的bug。
 
2. 它删掉诡异的字符比如NULL和Netscape4 JavaScript实体。
 
3. 它确保HTML实体比如&amp; 形式正确。
 
4.它确保HTML标签和标签属性的形式正确。在本阶段,没有出现在允许列表中——也就是,filter_xss()中的第2个参数——的标签,将被删除。属性style也被删除,这是由于通过覆盖CSS,它能够影响页面的外观,或者通过将页面的背景颜色设为一个垃圾链接的颜色来隐藏内容。以on开头的任何属性都会被删除(比如,onclick 或者 onfocus)如果你熟悉正则表达式,并且能够记住HTML实体的字符编码的话,你可以使用一个调试器来一步一步的学习一下filter_xss()(位于modules/filter/filter.module)以及它的相关函数。
 
5. 它确保所有的HTML标签,都不包含不允许的协议。允许的协议有http,https, ftp, news, nntp, telnet, mailto, irc, ssh, sftp, 和 webcal。你可以通过设置filter_allowed_protocols变量来修改这一列表。通过将下面的代码添加到你的settings.php文件中(参看settings.php文件中的关于变量覆写的注释),可以将允许的协议限制为http 和 https:
$conf = array(
'filter_allowed_protocols' => array('http', 'https')
);
 
下面的filter_xss()例子来自于modules/aggregator/aggregator.pages.inc,聚合器模块处理存在潜在安全隐患的RSS 或者 Atom种子。在下面,该模块为显示种子项模板文件准备使用的变量:
 
/**
 * Process variables for aggregator-item.tpl.php.
 *
 * @see aggregator-item.tpl.php
 */
function template_preprocess_aggregator_item(&$variables) {
$item = $variables['item'];
 
$variables['feed_url'] = check_url($item->link);
$variables['feed_title'] = check_plain($item->title);
$variables['content'] = aggregator_filter_xss($item->description);
...
}
 
注意我们调用了aggregator_filter_xss(),它对filter_xss()进行了封装并提供了一组可接受的HTML标签。我们将这个函数稍微做了简化,如下所示:
 
/**
 * Safely render HTML content, as allowed.
 */
function aggregator_filter_xss($value) {
$tags = variable_get("aggregator_allowed_html_tags",
'<a> <b> <br> <dd> <dl> <dt> <em> <i> <li> <ol> <p> <strong> <u> <ul>');
// Turn tag list into an array so we can pass it as a parameter.
$allowed_tags = preg_split('/\s+|<|>/', $tags, -1, PREG_SPLIT_NO_EMPTY));
return filter_xss($value, $allowed_tags);
}
 
     注意 作为安全性的一个练习,你可以拿出你自己的一些定制模块,来追踪输入到系统中了的用户输入,一定要确保在进行逻辑处理之前,先对用户输入进行清理,以保证安全性。
老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 使用filter_xss_admin()

有时你想让你的模块为后台管理页面生成HTML。由于我们对后台管理页面进行了访问控制,所以我们可以假定这些可以访问后台管理页面的用户比普通用户更可信。你可以为后台管理页面建立一个特定的过滤器并使用过滤器系统,但是这有点麻烦。因此,Drupal提供了函数filter_xss_admin()。它使用一组更加自由的可用标签列表,简单的对filter_xss()做了封装,除了<script> 和 <style>标签以外,它包含了所有的其它标签。使用它的一个例子是在主题中展示站点的宗旨(mission):

 
if (drupal_is_front_page()) {
$mission = filter_xss_admin(theme_get_setting('mission'));
}
 
只有在管理设置页面Administer >> Site configuration >>“Site information”才可以设置站点的宗旨,而只有超级用户和具有“administer site configuration”(管理站点配置)权限的人才可以访问这一页面,所以在这里使用filter_xss_admin()就比较合适。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 安全的处理URLs

模块常常处理用户提交的URLs并显示它们。我们需要一些机制来确保用户提供的值确实是一个合法的URL。Drupal提供了函数check_url(),它实际上是仅仅对filter_xss_bad_protocol()做了封装。它通过检查来确保URL中的协议是该Drupal站点所允许的协议(参看“使用filter_xss()阻止跨站点脚本攻击”部分的第5步),并使用check_plain()来处理URL。

如果你想判断一个URL是否合法,你可以使用valid_url()。它将检查http, https, 和 ftp URL的语法,并检查非法字符;如果URL通过了测试,那么它返回TRUE。这是一个便捷的方式,用来确保提交的URLs不包含javascript协议。
 
警告 仅仅通过语法检查的URL并不一定安全!
 
如果你使用URL——例如,在一个查询字符串中——来传递一些信息的话,你可以使用drupal_urlencode()来传递转义了的字符。调用drupal_urlencode()对斜杠,反斜杠,符号&进行编码,以兼容Drupal的简洁URL,接着调用PHP的rawurlencode()函数。函数drupal_urlencode()并不比直接调用rawurlencode()更安全,但是它可以方便的对字符串进行编码,以适应Apache的mod_rewrite模块。
 
提示 drupal_urlencode()是对PHP函数封装的一个例子:你可以直接调用PHP的urlencode(),但这样你将失去由Drupal为你负责一个函数的好处。类似的字符串封装函数可参看unicode.inc;例如,使用drupal_strlen()来代替PHP函数strlen()。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 使用db_query()来确保查询语句的安全

攻击网站的一个常见方式称为SQL注入。让我们看一个没有考虑安全性的程序员编写的模块。他仅仅想用一种简单的方式,来列出特定类型节点的所有标题:

 
/*
 * Implementation of hook_menu().
 */
function insecure_menu() {
$items['insecure'] = array(
'title' => 'Insecure Module',
'description' => 'Example of how not to do things.',
'page callback' => 'insecure_code',
'access arguments' => array('access content'),
);
return $items;
}
 
/*
 * Menu callback, called when user goes to http://example.com/?q=insecure
 */
function insecure_code($type = 'story') {
// SQL statement where variable is embedded directly into the statement.
$sql = "SELECT title FROM {node} WHERE type = '$type'"; // Never do this!
$result = db_query($sql);
$titles = array();
while ($data = db_fetch_object($result)) {
$titles[] = $data->title;
}
// For debugging, output the SQL statement to the screen.
$output = $sql;
 
$output .= theme('item_list', $titles);
return $output;
}
 
    访问http://example.com/?q=insecure,一切正常。我们得到了SQL,接着是故事(story)列表,如图20-2所示。
 
20-2。 简单的一列故事节点标题
 
注意,看看这个程序员是如何在这里,聪明的将insecure_code()函数中的$type参数默认设为了'story'。这个程序员利用了Drupal的菜单系统中的一个知识点,路径中的额外变量将自动作为参数传递给回调函数,所以路径http://example.com/?q=insecure/page将为我们得到类型为'page'的节点的所有标题,如图20-3所示。
 
20-3 简单的一列页面节点标题
 
    然而,该程序员犯了一个潜在的致命错误。将变量$type直接放到SQL中,并且依赖于PHP的变量扩展,这样整个站点就完全暴露了。让我们访问http://example.com/?'%20OR%20type%20=%20'story(参看图20-4)。
 
20-4 db_query()中不使用占位符引起的SQL注入
 
哎哟!我们可以在URL中输入SQL并执行它!怎么成这样了?还记不记得,在前面我提到%20是空格的已编码版本?我们简单的输入了以下文本的编码版本:
 
page' OR type = 'story
 
还记不记得我们将不安全的SQL赋值给了$sql变量? 看,当我们输入的编码文本被解码并成为语句的一部分时,发生了什么。下面是以前的代码:
 
SELECT title FROM {node} WHERE type = '$type'
 
替换$type,现在将其设置为了page' OR type = 'story,现在就成了
 
SELECT title from {node} WHERE type = 'page' OR type = 'story'
 
一旦你让用户能够修改发送给数据库的SQL,那么你的站点很容易受到攻击了。下面是改进后的版本:
 
function insecure_code($type = 'story') {
// SQL now protected by using a quoted placeholder.
$sql = "SELECT title FROM {node} WHERE type = '%s'";
$result = db_query($sql, $type);
$titles = array();
while ($data = db_fetch_object($result)) {
$titles[] = $data->title;
}
// For debugging, output the SQL statement to the screen.
$output = $sql;
 
$output .= theme('item_list', $titles);
return $output;
}
 
现在当我们通过访问http://example.com/?q=insecure/page'%20OR%20type%20=%20'story试图操纵URL时,db_query()将通过对单引号进行转义从而清理输入的数值。查询语句将变成下面的样子:
SELECT title FROM node WHERE type = 'page\' OR type = \'story'
 
由于我们没有名为"page\' OR type = \'story"的节点类型,这个查询很明显会失败。然而,我们仍然可以对其进行改进,因为在这种情况下URL应该仅包含一个有限集中的成员;也就是,我们站点上的节点类型。我们知道都有哪些类型,所以我们需要确认用户提供的值是我们已知中的一个。例如,如果我们仅启用了page和story两种节点类型,只有当URL中提供了这些类型时,我们才继续处理。让我们添加一些代码,用来检查这一点:
 
function insecure_code($type = 'story') {
// Check to make sure $type is in our list of known content types.
$types = node_get_types();
if (!isset($types[$type])) {
watchdog('security', 'Possible SQL injection attempt!', array(),
WATCHDOG_ALERT);
return t('Unable to process request.');
}
 
// SQL now protected by using a placeholder.
$sql = "SELECT title FROM {node} WHERE type = '%s'";
$result = db_query($sql, $type);
$titles = array();
while ($data = db_fetch_object($result)) {
$titles[] = $data->title;
}
// For debugging, output the SQL statement to the screen.
$output = $sql;
 
$output .= theme('item_list', $titles);
return $output;
}
 
    这里我们添加了一个检查,用来确保$type是我们的已有节点类型中的一个,如果没有通过检查,那么我们就为系统管理员记录一个便捷的警告日志。然而,还有更多问题。这个SQL没有对发布的和未发布的节点进行区分,所以未发布的节点的标题也会显示出来。另外,节点标题是用户提交的数据,在输出以前需要对其进行安全清理。但是,现在的代码,仅仅将标题从数据库中取出并将其显示了出来。让我们修正这些问题。
 
function insecure_code($type = 'story') {
// Check to make sure $type is in our list of known content types.
$types = node_get_types();
if (!isset($types[$type])) {
watchdog('security', 'Possible SQL injection attempt!', array(),
WATCHDOG_ALERT);
return t('Unable to process request.');
}
 
// SQL now protected by using a placeholder.
$sql = "SELECT title FROM {node} WHERE type = '%s' AND status = 1";
$result = db_query($sql, $type);
$titles = array();
while ($data = db_fetch_object($result)) {
$titles[] = $data->title;
}
 
// Pass all array members through check_plain().
$titles = array_map('check_plain', $titles);
$output = theme('item_list', $titles);
return $output;
}
 
现在只显示已发布的节点了,而所有的标题在显示以前,都经过了check_plain()安全处理。我们还删除了调试模式。这个模块得到了极大的改进!但是还有一个安全漏洞。你能找到吗?如果不能的话,继续阅读本章吧。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 使用db_rewrite_sql()来保持私有数据的私有性

老葛的Drupal培训班 Think in Drupal

在前面的列出节点这一例子,对于第3方模块来说是个常见的任务(现在的用的少一些了,这是由于使用views模块,可以很方便的通过web定义节点列表)。你可能会问:如果站点启用了节点访问控制模块,在前面的例子中哪段代码是用来保证我们的用户仅看到允许他们看到的节点集?这个问题问得很好,确实没有相应的代码。前面的代码将显示给定节点类型的所有节点,即便是节点访问模块限制访问的节点,也被显示了出来。这段代码非常傲慢,它没有考虑其它模块的感受!让我们修改一下代码。
    修改前:
 
$sql = "SELECT title FROM {node} WHERE type = '%s' AND status = 1";
$result = db_query($sql, $type);
 
    修改后:
 
$sql = "SELECT n.nid, title FROM {node} n WHERE type = '%s' AND status = 1";
$result = db_query(db_rewrite_sql($sql), $type); // Respect node access rules.
 
我们使用db_rewrite_sql()来对传递给db_query()的SQL参数进行了包装,函数db_rewrite_sql()允许其它模块修改SQL。传递给db_rewrite_sql()的查询语句,需要生命它们的主键(n.nid)和表的别名(n),所以我们在这里加了进来。核心模块中的一个重要例子就是节点模块,它改写针对node表的查询语句的。它首先检查node_access中是否存在可能限制用户访问节点的记录,然后向SQL中插入查询语句片段,用来检查这些权限。在我们的这种情况下,节点模块将修改SQL,它向WHERE语句中插入一个AND查询片段,从而过滤用户无权访问的节点。如何实现这一点,以及更多关于db_rewrite_sql()的信息,请参看第5章。
 

Drupal版本:

Drupal专业开发指南 第20章 动态查询语句

如果在你的SQL中,有多个只有在运行时才能确定的变量,这并不妨碍让你使用占位符。你将需要使用占位符比如'%s' 或者 %d,通过编程来创建你的SQL,接着你需要传递一组值来填充这些占位符。如果你自己直接调用db_escape_string(),那么你就做错了。下面的例子展示了占位符的使用,这里假定我们想取出,匹配特定节点类型的已发布节点的ID和标题:

 
// $node_types is an array containing one or more node type names
// such as page, story, blog, etc.
$node_types = array('page', 'story', 'blog');
 
// Generate an appropriate number of placeholders of the appropriate type.
$placeholders = db_placeholders($node_types, 'text');
 
// $placeholders is now a string that looks like '%s', '%s', '%s'
$sql = "SELECT n.nid, n.title from {node} n WHERE n.type IN ($placeholders)
AND status = 1";
 
// Let db_query() fill in the placeholders with values.
$result = db_query(db_rewrite_sql($sql), $node_types);
 
    运行完db_rewrite_sql()以后,db_query()的调用看起来的样子如下所示:
 
db_query("SELECT DISTINCT(n.nid), n.title from {node} n WHERE n.type IN
('%s','%s','%s') AND status = 1", array('page', 'story', 'blog'));
 
现在当db_query()执行时,将对节点类型的名称进行清理。如果你想知道具体的缘由,可参看includes/database.inc中的db_query_callback()。
下面是另一个例子。有时你处在这样的一种情况,你想在一个查询的WHERE语句中添加一些AND限制条件来限制查询语句。此时,你也需要小心点使用占位符。在下面的例子中我们假定$uid 和 $type的值都是有效的(例如,3和page)。
 
$sql = "SELECT n.nid, n.title FROM {node} n WHERE status = 1";
$where = array();
$where_values = array();
 
$where[] = "AND n.uid = %d";
$where_values[] = $uid;
 
$where[] = "AND n.type = '%s'";
$where_values[] = $type;
 
$sql = $sql . ' ' . implode(' ', $where) ;
// $sql is now SELECT n.nid, n.title
// FROM {node} n
// WHERE status = 1 AND n.uid = %d AND n.type = '%s'
 
// The values will now be securely inserted into the placeholders.
$result = db_query(db_rewrite_sql($sql), $where_values));
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 权限和页面回调

当你编写自己的模块时,需要注意的另一个方面是“access arguments”键,你会在菜单钩子定义中的每个菜单项中用到它。在前面我们用来说明不安全代码的例子中,我们使用了下面的access参数:

 
/*
 * Implementation of hook_menu().
 */
function insecure_menu() {
$items['insecure'] = array(
'title' => 'Insecure Module',
'description' => 'Example of how not to do things.',
'page callback' => 'insecure_code',
'access arguments' => array('access content'),
);
return $items;
}
 
一个非常重要的问题是,允许谁访问这个回调函数。“access content”(“访问内容”)权限是个很宽泛的权限。你可能想使用hook_perm()来定义你自己的权限,并使用它们来保护你的菜单回调函数。权限就是一些唯一的字符串,用来描述权限。(更多详细信息可参看第4章的“访问控制”部分)。
由于你实现的菜单钩子实际上就是个门卫,它允许或者拒绝用户对菜单钩子后面代码的访问(通过回调),对于你在这里使用哪些权限,你需要认真的考虑一下。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 跨站点请求伪造(CSRF)

老葛的Drupal培训班 Think in Drupal

假定你登录到了drupal.org,在里面浏览论坛中的内容。接着,由于其它原因,你转而浏览另一个网站。有人在该站点上不怀好意的放了一个图片标签:
 
 
当你的浏览器加载该图片时,它将会对drupal.org请求该路径。由于你当前已经登录到了drupal.org,你的浏览器会在请求中把你的cookie也带上。现在就需要考虑一个问题了:当drupal.org收到这个请求时,它会把这个请求当作一个以登录的用户进行处理吗?你猜测它会!恶意用户的图片标签,以你的用户名点击了一个drupal.org上的链接。
首先,对于此类攻击,你要做的是永远不要使用GET请求来修改服务器上的东西;这样生成的任意请求都将无害。Drupal表单API遵循HTTP/1.1公约,GET方法除了数据检索以外不应采取任何行动。对于需要对服务器进行改动的动作,Drupal全部使用POST(参看http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1)。
其次,表单API使用令牌和标识ID,来确保从POST请求中提交的表单数据来自于Drupal生成的表单(更多详情,参看第10章)。当你编写模块时,对于你的表单,一定要为其使用表单API,这样你就能够自动的获得这一保护。在你的模块中,对表单输入结果进行的任何操作,都应该放到该表单的提交函数中。这样你就可以确保表单API对你进行了保护。
最后,如果需要的话,你还可以在URL中加个令牌(由drupal_get_token()生成)并使用drupal_valid_token()对令牌进行验证,来保护GET请求。

Drupal版本:

Drupal专业开发指南 第20章 文件安全

在处理文件和文件路径时,Drupal面对的危险和其它web应用是一样的。

 
文件权限
文件权限应该这样设置,那就是用户不能操作(添加,重命名,或者删除)文件。Web服务器对于Drupal文件和目录,应该仅具有只读的权限。不过有个例外,那就是文件系统路径。很明显,web服务器必须对该目录具有访问权限,这样才能写入上传文件。
 
受保护文件
Drupal自带的.htaccess文件包含了以下代码:
 
# Protect files and directories from prying eyes.
<FilesMatch "\.(engine|inc|info|install|module|profile|po
|sh|.*sql|theme|tpl(\.php)?|xtmpl)$|^(code-style\.pl
|Entries.*|Repository|Root|Tag|Template)$">
Order allow,deny
</FilesMatch>
 
Order指令设为了allow,deny,但是却没有包含Allow或者Deny指令。这意味着默认的行为是拒绝。换句话说,对表20-2所示文件的请求都会被拒绝访问。
 
20-2.根据FilesMatch指令的正则表达式所拒绝的文件
文件                 匹配描述
后缀 .engine         模板引擎
后缀.inc             库文件
后缀.info            模块和主题的.info文件
后缀.install         模块的.install文件
后缀.module          模块文件
后缀.profile         安装器
后缀.po              PO文件(翻译)
后缀.sh              Shell脚本
后缀.*sql            SQL脚本
后缀.theme           PHP主题
后缀.tpl.php         PHPTemplate模板文件
后缀.tpl.php4         PHPTemplate模板文件
后缀.tpl.php5         PHPTemplate模板文件
后缀.xtmpl            XTemplate文件
名为 code-style.pl    语法检查脚本
前缀为Entries.        CVS文件
名为Repository        CVS文件
名为Root              CVS文件
名为Tag               CVS文件
名为Template          CVS文件
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 文件上传

如果一个启用的模块允许文件上传,那么文件就应该放到一个特定的目录下,并通过代码来强制访问。

如果启用了文件上传,并在Administer >>Site configuration >> File system中选择了私有下载方法,那么该页面的文件系统路径必须位于web根目录以外。换句话说,如果强迫在特定应用的文件用户权限与web根目录保持一致,会恰得其反的。
文件上传的最大危险就是,如果有人能够上传一个可执行的文件,那么该文件就可以用来获取你服务器上的更多权限。Drupal从两方面对这一点进行了保护。首先,会向文件系统路径声明的目录写入.htaccess文件:
 
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006
Options None
Options +FollowSymLinks
 
指令SetHandler告诉Apache,这个目录下面的任何可执行文件,应有处理器Drupal_Security_Do_Not_Remove_See_SA_2006_006(实际并不存在)进行处理。这样,该处理器九覆写了由Apache定义的任意处理器,比如
 
AddHandler application/x-httpd-php .php
 
Drupal的上传模块还为多个扩展名的文件实现了重命名。这样,evilfile.php.txt经过上传就变成了evilfile.php_.txt。更多详细可参看http://drupal.org/node/65409http://drupal.org/node/66763
 
注意 前面的方案是针对Apache的。如果你的Drupal使用的是不同的web服务器,那么你应该清楚,如何解决用户可能上传可执行文件这一安全问题。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 文件名和路径

不要相信用户提供的文件名或者文件路径!当你编写一个模块时,你的代码期望能够收到somefile.txt,实际上它可能得到了其它文件,比如

 
../somefile.txt // File in a parent directory.
../settings.php // Targeted file.
somefile.txt; cp ../settings.php ../settings.txt // Hoping this runs in shell.
 
前面的两个例子,里面包含了两个.号,这意味着想操作当前目录的上一级目录。后面的一个,就是希望程序员执行一个shell命令,它包含了一个分号,这样该shell命令运行以后,将会运行一个额外的命令,以将settings.php文件设为可读,从而获取数据库用户名和密码。在前面的所有的例子中,都希望web服务器对文件系统路径以外的目录具有写权限。
无论你什么时候使用文件路径,都须调用file_check_location(),就像这样:
 
if (!file_check_location($path, 'mydirectory') {
// Abort! File path is not what was expected!
}
 
file_check_location()函数将找出文件的实际位置,并将其与你期望的目录进行对比。如果文件路径为OK,那么就返回文件的实际路径;否则,返回0。
一般来讲,你可能不会开始就想编写一个了不起的文件管理模块供大家使用。实际上你会,先学习现有的相关模块,熟悉了以后,再编写自己的文件模块。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 对邮件头部进行编码

老葛的Drupal培训班 Think in Drupal

当你编写的代码需要使用用户输入,并将其构建到e-mail消息中时,需要考虑一下两点:
 
1. E-mail头部用换行符(line feed)来分隔(只有在换行符后面没有空格或者tab键时才被作为头部分隔符)
 
2..如果你没有检查用户的输入是否包含换行符的话,那么用户就可以在邮件的主体中注入它们自己的头部。
 
例如,假如你希望用户为他/她的消息输入一个标题,而用户输入了以下内容,它包含转义的换行符 (%0A)和空格(%20)字符:
 
Have a nice day%0ABcc:spamtarget@example.com%0A%0AL0w%20c0st%20mortgage!
 
结果将如下所示:
 
Subject: Have a nice day
L0w c0st mortgage!
...
 
因此,Drupal内置的邮件函数drupal_mail()使用mime_header_encode()来过滤所有的头部。所有的不能打印的字符将根据RFC 2047编码为ASCII可打印的字符,这样就消除了潜在的风险。这涉及到在字符前面添加前缀=?UTF-8?B?,接着输出基于64位编码的字符,再加上?=。
我们鼓励你使用drupal_mail();如果你不使用它的话,那么你需要自己直接调用mime_header_encode()。
 

Drupal版本:

Drupal专业开发指南 第20章 用于生产环境的文件

并不是Drupal自带的所有文件,都是生产站点所必须的.例如,如果在一个生产站点上,有CHANGELOG.txt文件可用的话,那么互联网上的任何人都可以查看你的Drupal版本了(当然,一些高手黑客可用多种方式来探测你的网站是不是使用的Drupal;参看http://www.lullabot.com/articles/is-site-running-drupal)。表20-3列出了Drupal安装后,为了正常工作所需要的文件和目录;其它的都可以从生产站点上删除(不过要保留一份备份哦!)当然,你也可以采用另外一种方式,那就是限制对这些文件的读权限。

 
20-3. Drupal正常工作所需要的文件和目录
文件/目录             目的
.htaccess       在Apache上用于安全,简洁URL和缓存支持
cron.php        允许运行周期性调度任务
includes/       函数库
index.php       请求的主入口
misc/           JavaScript和图片
modules/        核心模块
robots.txt      阻止恶意的网络爬虫骚扰你的网站
sites/          站点特定的模块,主题和文件
themes/         核心主题
xmlrpc.php      XML-RPC终端;只有当你的站点需要接收XML-RPC请求时才有用。
老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 保护cron.php

Drupal中,有些周期性的调度任务是必须执行的,比如清理日志文件,更新统计等等。对于Unix系统,你可以使用cron任务,对于Windows,你可以使用任务调度器,来运行cron.php文件。可以通过命令行或者通过web服务器来运行该文件。在这个文件的执行中,它简单的做了一个完整的Drupal引导指令,并调用includes/common.inc中的drupal_cron_run()函数。这个函数使用信号量来阻止cron的重负运行(一个cron周期运行多次);尽管如此,特别小心的用户可能还想阻止任何用户访问http://example.com/cron.php。你可以通过在Drupal根目录下的.htaccess文件中添加以下代码,来实现这一点:

<Files cron.php>
Order deny,allow
Deny from all
Allow from example.com
Allow from 1.2.3.4
Allow from 127.0.0.1
</Files>
前面的指令,告诉Apache,拒绝任何客户对cron.php的访问,这里面还给出了特殊情况,example.com域名,IP地址为1.2.3.4的计算机,以及本地机器。
有些管理员会简单得将cron.php文件进行重命名。
老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 SSL支持

SSL支持

默认情况下,Drupal通过HTTP使用纯文本来处理用户登录。然而,如果你的web服务器支持HTTPS的话,那么Drupal也可以通过HTTPS来处理登录。此时不需要做任何修改。
 
独立的PHP
偶尔,你可能需要编写一个独立的.php文件来取代将代码放到Drupal的模块中。当你这样做时,你需要仔细的考虑安全性。假如,当你正在测试你的网站时,你快速的编写了一些有点垃圾的代码用来向数据库中插入一些用户,这样你就可以使用多用户来测试站点性能了。假定你将其命名为testing.php,并将其放到了Drupal站点的根目录下,挨着index.php。接着,你把它放到了浏览器的收藏夹中了,当你每次想插入一些新用户时,你就可以选择收藏夹中的该链接了:
 
<?php
/**
 * This script generates users for testing purposes.
 */
// These lines are all that is needed to have full
// access to Drupal's functionality.
include_once 'includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
 
db_query('DELETE FROM {users} WHERE uid > 1'); // Whoa!
for ($i = 2; $i <= 5000; $i++) {
$name = md5($i);
$pass = md5(user_password());
$mail = $name .'@localhost';
$status = 1;
db_query("INSERT INTO {users} (name, pass, mail, status, created, access)
VALUES ('%s', '%s', '%s', %d, %d, %d)", $name, $pass, $mail, $status,
time(), time());
}
print t('Users have been created.');
 
 这对于测试很有用,如果你一不小心把这个脚本放到了你真实的站点上,那么将会发生什么呢?任何发现了这个脚本的人都可以使用一个简单的请求来删除你数据库中的用户数据。这就是为什么安全检查这么重要,即便是对于一次性的脚本来说,也需要包含它,如下所示:
 
<?php
/**
 * This script generates users for testing purposes.
 */
// These lines are all that is needed to have full
// access to Drupal's functionality.
include_once 'includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
 
// Security check; only superuser may execute this.
if ($user->uid != 1) {
print t('Not authorized.');
exit();
}
 
db_query('DELETE FROM {users} WHERE uid > 1'); // Whoa!
for ($i = 2; $i <= 5000; $i++) {
$name = md5($i);
$pass = md5(user_password());
$mail = $name .'@localhost';
$status = 1;
db_query("INSERT INTO {users} (name, pass, mail, status, created, access)
VALUES ('%s', '%s', '%s', %d, %d, %d)", $name, $pass, $mail, $status, time(),
time());
}
print t('Users have been created.');
 
通过这个例子领会到以下两点:
 
1.即便是在快速编写的脚本中也要包含安全检查,你可以从一个包含必要代码的模板做起。
2.记住在部署中的重要一步,就是删除或者禁用测试代码。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 Ajax安全性

 

当你需要使用Ajax比如jQuery时,你一般在开发Ajax的服务器端代码时,都会假定对这些代码的调用是通过JavaScript完成的,而与Ajax相关的安全性的一个要点是,恶意用户可以直接调用Ajax的服务器端代码(例如,可以使用命令行工具比如curl或者wget,或者直接在浏览器中输入该URL)。你需要从这两个方面来对你的代码进行测试。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 表单API的安全性

使用表单API的一个好处就是,它为你处理了许多安全性问题。例如,Drupal通过检查来保证,用户从下拉选择框中选择的值,确实是Drupal生成的选项中的一个。表单API使用了一系列的事件集,比如表单构建、验证、和执行。在验证阶段前面,你不能够使用用户输入,因为用户输入还没有被验证。例如,如果你使用的值来自$_POST,那么你就不能确保用户是否操作了该值。还有就是,使用#value元素在表单中传递信息,尽可能的使用它来代替隐藏域,因为恶意用户可以操作隐藏域,但是访问不了#value元素。

对于用于构建表单的用户提交的数据,也需要进行适当的安全清理,这和用户提交的其它数据一样,如下所示。
不安全:
 
$form['foo'] = array(
'#type' => 'textfield',
'#title' => $node->title, // XSS vulnerability!
'#description' => 'Teaser is: '. $node->teaser, // XSS vulnerability!
'#default_value' => check_plain($node->title), // Unnecessary.
);
 
安全:
 
$form['foo'] = array(
'#type' => 'textfield',
'#title' => check_plain($node->title),
'#description' => t('Teaser is: @teaser', array('@teaser' => $node->teaser)),
'#default_value' => $node->title,
};
 
对于默认值,没有必要使用check_plain(),这是因为表单元素类型的主题函数实现了这一点(在这里,为includes/form.inc中的theme_textfield()函数)。
 
警告 如果你使用自己的主题函数来覆写Drupal的默认主题函数,那么你需要检查一下,在默认的主题函数中是不是对一些用户输入作了安全处理,如果有的话,在你的代码里,也需要实现这一点。
 
更多关于表单API的信息参看第10章。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 保护超级用户帐号

获取一个Drupal网站密码的最简单的方式是,打个电话给这个网站的秘书,这样说“你好,我是Joe。<一对客套话>。我是给你们的网站提供技术支持的,在技术支持中,我们遇到了问题。需要使用用户名和密码进行登录,你的用户名和密码是多少?”不幸的是,很多人会轻易的将这一私密信息告诉他人。尽管技术对此有所帮助,但是对于这种攻击,最好的办法还是对用户进行教育。

正因为此,最好不要将超级用户(用户1)授予任何人。对于任何维护网站的用户,只能授予他完成任务所需要的权限。这样,即使出现了安全问题,危害也是可以控制的。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 使用eval()

千万不要使用它。使用PHP函数eval(),你可能觉得它是进行元编程的极好的方式,或者想用它来减少多行代码,这个函数将一个字符串文本作为输入,并使用PHP解释器对其求值。这完全是一个错误。如果有任何方式允许一个用户使用eval()来操作输入的话,那么你就会将PHP解释器的威力暴露给用户。这距离泄露私密数据的时间也不会太长了,因为用户就可能使用这一方式来获取你的数据库中的用户名和密码。

这也是为什么你只能在Drupal后台管理接口中使用PHP过滤器的原因,而且只有在非常特定的环境中并具有相关授权的前提下才使用它。为了能够睡个安稳觉,避免使用eval()和PHP过滤器。Drupal的确在其内核中使用了eval(),但是情况非常少,并且使用了drupal_eval()对其进行封装,drupal_eval()可以阻止要进行估值的代码,覆盖调用drupal_eval()的代码中的变量。drupal_eval()位于includes/common.inc。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 总结

读完本章后,你应该知道:

    • 永远不要相信用户的输入
    • 如何将用户的输入转换为安全的形式
    • 如何阻止XSS攻击
    • 如何阻止SQL注入攻击
    • 如何编写考虑了节点访问模块的代码
    • 如何阻止CSRF攻击
    • Drupal是如何保护上传文件的
    • 如何阻止e-mail头部注入

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 开发最佳实践

在本章中,我们给出了许多的代码小提示和最佳实践,它能帮你成长为一个合格的Drupal开发者,并帮你摆脱电脑的折磨。我们首先学习Drupal的编码标准,接着学习如何为模块创建文档以方便其它开发者理解代码。还介绍了如何在Drupal的核心中快速的查找东西,介绍了版本控制,并详细的说明了如何维护一个第3方模块,最后我们讨论了如何调试和剖析代码。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 Devel模块的其它用途

老葛的Drupal培训班 Think in Drupal

Devel模块还有一些其它的方便函数,它们能帮你提升开发效率。
    例如,你可以实时的切换当前查看Drupal页面的用户。这在技术支持和调试其它角色时,会非常有用。为了切换到另一个用户,导航到URL http://example.com/?q=devel/switch/$uid,其中$uid是你想切换到的用户的ID。还有一种方式是,启用“Switch users”(切换用户)区块,它提供了一组链接,能够用来实现相同功能。
    Devel模块提供了一个名为Execute PHP(执行PHP)的附加区块,可以用来方便的输入和执行简短的代码片断(这也是为什么在实际的站点中,一定要禁用devel模块的一个原因!)。
你可以使用dsm(), dvm(),dpr(),和 dvr()函数来输出调试消息,这些消息对于其它用户是不可见的。
 
• dpm()向页面的消息区域输出一个简单变量(比如,一个字符串或者一个整数)。可把它理解为“调试输出消息”(“debug print message”)的意思。
 
• dvm()向页面的消息区域输出一个var_dump()。对于复杂的变量比如数组或者对象,可以为其使用这个函数。可把它理解为“调试变量消息”(“debug variable message”)的意思。
 
• dpr()使用一个特殊的递归函数(dprint_r()),在页面顶部输出一个复杂的变量(比如一个数组或者对象),这里输出结果的格式也是很漂亮的。
 
• dvr()在页面顶部输出一个格式良好的var_dump()。
 
    这些函数的输出,对于那些没有“访问devel信息”权限的用户,是不可见的,这在实时调试时非常方便。
 
    下面给出了一个例子:
 
dpr(node_load(5)); // Display the data structure of node 5.
dvr($user); // Display the $user variable.
 

Drupal版本:

Drupal专业开发指南 第21章 Drupal核心中的标签

标签就是一个特定分支在某个时间的快照。在Drupal世界中,标签用来标记beta,bug-fix,和安全发布。这样就得到了更小的版本,比如Drupal 6.1 和 6.2。规范的标签名有DRUPAL-4-7-1,DRUPAL-4-7-2, DRUPAL-5-7, DRUPAL-6-0, DRUPAL-6-1, 和DRUPAL-6-2(再次注意,在Drupal5中,命名规范改变了)。Drupal核心使用的标签名字的完整列表,可参看http://drupal.org/node/93997

    在Drupal6还处于正在开发的时候,核心负责人想创建一个beta发布,这样就可以方便人们测试代码了。所以他们创建了一个标签,DRUPAL-6-0-BETA-1,如图21-3所示。
 
21-3.创建了一个标签DRUPAL-6-0-BETA-1。
 
    标签DRUPAL-6-0-BETA-1指的是处于特定状态的代码;也就是,在某个时间点上,代码的一个准确快照。如果你现在真的需要的话,那么你仍然可以使用CVS下载Drupal的beta 1发布。
    随着越来越多的bug被修正,那么将会使用标签,比如DRUPAL-6-0-RC-1和DRUPAL-6-0-RC-2,来创建一个或多个候选发布。当代码足够成熟时,将会从基准代码开发中创建一个分支DRUPAL-6,而HEAD则将用于Drupal7的开发。这样,值得庆祝的一天到来了,DRUPAL-6-0标签创建了;关于Drupal6的技术文章、博客,都会疯狂的增加;Drupal6也将应用于各种实际的网站之中。而在DRUPAL-6分支背后,许多Drupal程序员将继续为其修正bug,这样就有了标签DRUPAL-6-1, DRUPAL-6-2,以此类推。
 
-dev后缀
    同时,HEAD上的开发仍在继续。不过在Drupal社区中,一般不把它称作HEAD,而是把它看作是Drupal的下一版本,因为这就是正在开发的真正东西。在图21-4中,你可以看到,7.x-dev就是开发Drupal 7的地方。
 
21-4.–dev快照指的是开发的前沿。
 
    当Drupal 7正式发布时,核心负责人将为Drupal7添加一个稳定的分支,在该分支上将会继续添加新的标签。注意,因此,7.x-dev不是一个标签!这意味着它指的不是处于给定状态的代码。更准确的说,它指的是一个分支上的继续开发。每天,drupal.org上的打包脚本都会从分支上获得一个快照,将它作为一个“开发快照”提供给大家下载,如图21-5所示。不过,这样做只是为了方便;它不是CVS的一个特性。
 
21-5.http://drupal.org/download上,你可以下载Drupal下一个版本的开发快照。
 
    同样,在稳定的分支上,仍会进行bug的修正工作。看一下图21-4中的DRUPAL-6分支。从图中我们可以看出,在创建了DRUPAL-6-2标签以后,已经又修正了一个bug,但是现在还没有创建新的标签。并不是每次修正一个bug后,都创建一个标签的;只有在修正了足够多的bug后,核心维护者判定应该批准一个新发布时,才创建一个标签(例外情况就是,安全性修正,它通常会导致一个立即发布)。当时间到了以后,核心维护者就会创建DRUPAL-6-3标签,这样就创建了一个新发布。
    所以,让我们再次回到图21-4上;DRUPAL-6分支上的最新代码,它超过了DRUPAL-6-2发布,里面包含了一个在发布以后的bug修正,我们把它称为6.x-dev。这意味着它是Drupal 6.3的开发版本;在核心维护者创建DRUPAL-6-3标签时,这一代码将成为Drupal 6.3。在此以后,分支末端的代码还是6.x-dev,因为该代码将成为Drupal 6.4。
 
提示 当开发者提到分支末端的代码时,他们一般不会停下来检查哪个才是实际的下一版本(它是6.1? 6.2? 6.3?)。在这种情况下,他们使用一个“x”,来替代“6.1”中的“1”或者“6.2”中的“2”,这样就可以简单把它称为6.x-dev ----- 也就是,“x”指的是将会成为Drupal 6的下一版本的代码,而不管具体的版本号是多少。
 
    现在,你应该能够理解标签和分支的不同了,以及标签是如何关联到核心发布上的。这一信息总结在了表21-2中。
 
21-2.标签、分支、发布、Tarball(沓包)之间的关系
标签          出现的分支     发布           Tarball沓包
DRUPAL-5-7      DRUPAL-5        Drupal 5.7      drupal-5.7.tar.gz
DRUPAL-6-0      DRUPAL-6        Drupal 6.0      drupal-6.0.tar.gz
DRUPAL-6-1      DRUPAL-6        Drupal 6.1      drupal-6.1.tar.gz
DRUPAL-6-2      DRUPAL-6        Drupal 6.2      drupal-6.2.tar.gz
HEAD            None            7.x-dev         drupal-7.x-dev.tar.gz
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 PHP注释

Drupal遵循大多数的Doxygen注释样式指南。所有的文档区块必须使用下面的语法:

 
/**
 * Documentation here.
 */
 
    除了第一行以外,其它各行在星号(*)前面必须要有一个空格。
 
注意 Doxygen是一个能够友好支持PHP的文档生成器。它从代码中提取PHP注释并生成适合用户阅读的文档。更多信息,可参看http://www.doxygen.org
 
    当为一个函数添加说明文档时,文档区块必须紧挨着放在函数前,中间不能存在空行。
 
Drupal能够理解下面所列的Doxygen构造体;尽管我们接下来会介绍到其中的大多数,不过关于如何使用它们的更多信息,请参看Doxygen的官方站点。
 
• @mainpage
• @file
• @defgroup
• @ingroup
• @addtogroup (as a synonym of @ingroup)
• @param
• @return
• @link
• @see
• @{
• @}
 
    遵循这些标准的好处是,你可以使用第3方的API模块,为你的模块自动生成文档。API模块实现了Doxygen文档生成器规范的一个子集,专门针对Drupal代码的文档生成进行了优化。访问http://api.drupal.org,你就可以看到这个模块的一个实例,而关于API模块的更多信息,可参看http://drupal.org/project/api
老葛的Drupal培训班 Think in Drupal
 

Drupal版本:

Drupal专业开发指南 第21章 devel模块

Devel模块是个大杂烩,里面包含了许多实用功能,开发者可用它来调试和检查代码的各种细节。

    你可以从http://drupal.org/project/devel下载该模块(或者使用CVS签出并获得更酷的效果)。安装了devel模块后,一定要启用devel区块。下面是devel区块中一些含义比较模糊的链接,这里给出了明确的解释:
 
• Empty cache(清空缓存):这个将执行includes/common.inc中的drupal_flush_all_caches()函数。这与你导航到“管理➤站点配置➤性能”点击“清除缓存数据”的效果是一样的。也就是说,CSS和JavaScript缓存被清空了;新压缩的CSS和JavaScript文件被重新命名,这样就强制客户下载新文件了;主题注册表被重新构建;菜单被重构;node_type表被更新;数据库中的缓存表,用来存储页面、菜单、节点、区块、过滤器、和变量缓存的表,也全被清空了。具体一点,清空的缓存表有cache, cache_block, cache_filter, cache_menu, 和cache_page。第3方模块的自定义缓存表,如果模块实现了hook_flush_caches()(它返回一个数组,里面包含了要被清空的缓存表的名字),那么这些缓存表也被清空。
 
Enable Theme developer(启用主题开发者): 这个链接用来启用主题开发者模块,启用这个模块后,将鼠标指向一个页面元素,就会为你显示出用来生成这个页面元素的模板或主题函数了(参看第8章)。
 
• Function reference(函数引用): 这个函数提供了一列用户函数,这些函数是在这个请求期间使用PHP的get_defined_functions()定义的。点击一个函数的名字,可以查看它的文档。
 
• Hook_elements(): 这个链接使用一种便于阅读的格式来显示hook_elements()调用的结果,在使用表单API时,它会非常有用。
 
• Rebuild menus(重构菜单): 这个调用menu_rebuild(),它将清空并重新构建menu_router表,同时更新menu_links表(参看第4章)。
 
• Reinstall modules(重装模块): 通过运行hook_uninstall()和hook_install()来重新安装一个模块。模式版本号将被设置成最近更新的号码。在重新安装模块以前,对于任何已有的数据库表,如果其对应模块没有实现hook_uninstall(),那么一定要手动的将其清除。
 
• Session viewer(会话查看器): 使用这个链接来显示你的$_SESSION变量的内容。
 
• Variable editor(变量编辑器):这个链接列出当前存储在variables表中以及你的settings.php文件的$conf数组中的所有变量及它们的值,它还允许你对这些变量进行编辑。一般可以使用variable_get() 和 variable_set()来访问这些变量。
 

Drupal版本:

Drupal专业开发指南 第21章 为函数编写文档

老葛的Drupal培训班 Think in Drupal

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

 
/**
 * Short description, beginning with a verb.
 *
 * Longer description goes here.
 *
 * @param $foo
 * A description of what $foo is.
 * @param $bar
 * A description of what $bar is.
 * @return
 * A description of what this function will return.
 */
function name_of_function($foo, $bar) {
    ...
    return $baz;
}
 
    简短描述的开头,应该使用现在时态的祈使动词,比如“Munge form data”(“混合标单数据”)或“Do remote address lookups”(执行一个远程地址查找)(而不是“Munges form data” 或“Does remote address lookups”)。让我们看一个来自于Drupal核心的一个例子,它位于system.module中:
 
/**
 * Add default buttons to a form and set its prefix.
 *
 * @ingroup forms
 * @see system_settings_form_submit()
 * @param $form
 * An associative array containing the structure of the form.
 * @return
 * The form structure.
 */
function system_settings_form($form) {
    ...
}
 
    在前面的例子中有一些新的Doxygen结构体:
• @see告诉你可参考哪些其它函数。前面的代码是一个表单定义,所以@see指向了表单的提交处理器。当API模块解析这个注释来生成文档时(比如http://api.drupal.org中可用的文档),它将把@see后面的函数名转换为一个可点击的链接。
 
• @ingroup将一组相关的函数联系到了一起。在这个例子中,它创建了一组提供表单定义的函数。你可以创建你想要的任意的小组名字。可能的核心值有:batch, database, file, format, forms, hooks, image, menu, node_access, node_content, schemaapi, search, themeable, validation
 
提示 你可以在http://api.drupal.org查看一个给定组的所有函数。例如,表单构建器函数位于http://api.drupal.org/api/group/forms/6,而可主题化的函数位于http://api.drupal.org/api/group/themeable/6
 
    实现普通的Drupal结构体的函数,比如钩子函数或表单验证/提交函数,可以完全省略@param和@return语法,但是仍然应该包含一行描述函数功能的说明,如下面的例子所示:
 
/**
 * Validate the book settings form.
 *
 * @see book_admin_settings()
 */
function book_admin_settings_validate($form, &$form_state) {
    ...
    }
}
 
    如果一个函数是一个菜单回调(也就是,使用hook_menu()映射到一个URL上),那么最好能加以说明:
 
/**
 * Menu callback; print a listing of all books.
 */
function book_render() {
    ...
}
 
为钩子实现编写文档
    当一个函数是一个钩子实现时,此时不需要为钩子编写文档。简单的说明一下实现了哪个钩子就可以了,例如:
 
/**
 * Implementation of hook_block().
 */
function statistics_block($op = 'list', $delta = 0, $edit = array() {
    ...
}

Drupal版本:

Drupal专业开发指南 第21章 从CVS签出Drupal

我们将学习如何在命令行中使用CVS。现在有许多图形化的CVS客户端,一旦学会了这些基本的命令以后,你应该能够很容易的实用这些图形化的CVS客户端了。Windows用户通过安装Cygwin环境(参看http://drupal.org/node/150036),就可以使用CVS命令行了。如果你使用的是CVS命令行客户端,那么这会便于你与社区中的其它开发者交流CVS问题。

    用CVS行话来说,你将需要从官方的CVS资源库中签出一个Drupal的工作拷贝。这可能有点唠叨,但是使用正确的术语是非常重要的。下面是一个CVS命令,它用来从CVS服务器上获取Drupal 6.2:
 
cvs -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout -d
~/www/drupal6 -r DRUPAL-6-2 drupal
 
    让我们来分解一下这个命令。cvs执行一个CVS客户端;也就是说,它在你的计算机上运行一个名为cvs的程序:
 
cvs -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout -d
~/www/drupal6 -r DRUPAL-6-2 drupal
 
    cvs中的选项–d,代表“目录”,它用来指定CVS资源库的位置:
 
cvs -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout -d
~/www/drupal6 -r DRUPAL-6-2 drupal
 
    一个资源库,用CVS的话来说,就是一个使用CVS维护的文件树所在的位置。现在,如果资源库也位于同一台计算机上,那么-d选项就可以简单许多:cvs -d /usr/local/myrepository。然而,Drupal的资源库是位于远程服务器上的,所以我们需要指定更多的连接参数。让我们更具体的分析一下这个命令。
    -d选项的每个参数都使用冒号进行了隔离。pserver代表“密码验证服务器”,它是用来连接到Drupal资源库的连接方法。然而,CVS也可以通过其它协议进行连接,比如SSH。
    接着,指定了用户名和密码。对于Drupal CVS资源库,这两者是一样的:anonymous。跟在@符号后面的是要连接的主机名:cvs.drupal.org。最后,我们需要指定资源库在远程主机上所处的位置:/cvs/drupal。
 
注意 当你通过了CVS服务器的一次认证以后,下次你就不需要再次认证了,因为在你的根目录下将创建一个名为.cvspass的文件,它存储了登录信息。接下来,应用于这个资源库的CVS命令,就不再需要使用这个全局的-d参数了。
 
    现在,连接参数已经建立,我们可以指定cvs实际执行的命令了;在这里,我们使用checkout命令从Drupal资源库中获取一个工作拷贝:
 
cvs -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout -d
~/www/drupal6 -r DRUPAL-6-2 drupal
 
不要将下面的-d与前面的全局选项-d混淆了:
 
cvs -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout -d
~/www/drupal6 -r DRUPAL-6-2 drupal
 
    这个-d用来,将资源库的一个工作拷贝存放在你计算机上的drupal6目录中,这个目录位于根目录下面的www目录的下面。这是一个可选的参数,如果没有这个参数,那么就使用资源库的名字在本地创建一个文件夹,用来保存工作拷贝。由于在这种情况下,资源库的名字为drupal,所以它将创建一个名为drupal的文件夹,来保存资源库的工作拷贝。
    -r参数代表“修订本”。一般来说,它应该是一个标签或者分支。我们将在接下来讨论什么是标签和分支。在前面的命令中,我们请求名为DRUPAL-6-2的修订本,这里的DRUPAL-6-2是一个标签,对应于Drupal 6.2发布。你可以将它替换为Drupal当前版本的标签。
 
cvs -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout -d
~/www/drupal6 -r DRUPAL-6-2 drupal
 
    Drupal核心的所有标签和分支的列表,可参看http://drupal.org/node/93997
 
    最后,drupal是要签出的资源库的名字。
 
cvs -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout -d
~/www/drupal6 -r DRUPAL-6-2 drupal
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 使用CVS更新代码

如果你想将你的站点更新到最新的Drupal代码上,甚至更新到下一个新版本上,那么你可以使用cvs update命令来实现这一点。首先测试一下cvs update命令将做出哪些变更,运行下面的命令:

 
cvs -n update -dP
 
    这将为你显示将被修改的东西,而实际上并未修改。要执行实际的更新,使用这个命令:
 
cvs update –dP
 
    这将使得你的Drupal工作拷贝,与当前分支的最近变更保持同步。CVS通过查看存储在CVS文件夹里的CVS元数据来获知当前分支,在你初次执行签出时,信息就被保存到了那里,所以你不需要每次都指定它。当资源库中已有目录在你的工作拷贝中不存在时,-d选项将为你创建相应得目录。-P将清除掉所有无关的空目录。
 
注意 在运行任何可能修改文件的CVS命令以前,一定要先备份你的数据。另一种最佳实践是,在将这些变更放到实际站点以前,先在一个实验站点上执行CVS更新,并解决任何潜在的文件冲突,然后再将这些变更放到实际站点上。
 
    向一个更高的Drupal主版本的升级,实际上就是CVS update命令的一个变体。我们假定你现在用的是Drupal 5.7,并想把它升级到6.2。再提醒一次,在运行下面的命令以前,一定要确定你位于Drupal的根目录下。
    在当前分支上,更新到最近的正式发布,我们在这个例子中假定它就是Drupal 5.7。在下面的命令中,实际上并不需要指定DRUPAL-5-7(因为cvs知道你的当前分支),不过罗嗦一点也有好处,它能让你确保所做的变更就是你想要的:
 
cvs update -dP -r DRUPAL-5-7
 
警告 如果你正在将Drupal更新到一个新版本上,那么你首先应该禁用所有的非核心模块和主题,然后再运行cvs update命令来更新Drupal核心。更详细的操作指南,可参看http://drupal.org/upgrade
 
    接着,将核心代码升级到Drupal 6。让我们假定Drupal 6.2就是最新的版本。这样,下面的命令就会从CVS树的DRUPAL-6分支上,获取标签为DRUPAL-6-2的代码:
 
cvs update -dP -r DRUPAL-6-2
 
    对于标准更新流程的其余部分,比如更新第3方的模块和主题、以及通过访问update.php更新数据库,你仍然需要执行,但是现在你不需要下载核心的新版本并覆写你的核心文件了。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 使用egrep来查找代码

egrep是一个Unix命令,用来在文件中搜索匹配给定正则表达式的位置。不,它不是一个鸟儿(那是egret(白鹭))。如果你是一个Windows用户,并想学习一下这些例子,你可以先安装一个预编译的版本(参看http://unxutils.sourceforge.net)或者安装Cygwin环境(http://cygwin.com),这样就可以使用egrep了。否则,你只能使用操作系统内置的搜索功能,而不能使用egrep。

    当你需要在Drupal核心中查找钩子实现时,当查找错误消息生成的位置时,以及其它一些情况时,egrep都是一个很方便的工具。让我们看一些例子,这里在Drupal根目录下使用egrep:
 
$ egrep -rl 'hook_init' .
./includes/bootstrap.inc
./includes/path.inc
./modules/aggregator/aggregator.module
./modules/book/book.module
./modules/forum/forum.module
./modules/node/node.module
./modules/poll/poll.module
./modules/system/system.module
./update.php
 
    在前面的情况中,我们在Drupal文件中从当前目录(.)递归的搜索(-r)包含hook_init的实例,并将匹配实例的文件名打印出来(-l)。现在看下面的例子:
 
$ egrep -rn 'hook_init' .
./includes/bootstrap.inc:1011: // Initialize $_GET['q'] prior to loading
modules and invoking hook_init().
./includes/path.inc:9: * to use them in hook_init() or hook exit() can make
them available, by
./modules/aggregator/aggregator.module:261: * Implementation of hook_init().
./modules/book/book.module:164: * Implementation of hook_init(). Adds the
book module's CSS.
./modules/forum/forum.module:160: * Implementation of hook_init().
./modules/node/node.module:1596: * Implementation of hook_init().
./modules/poll/poll.module:24: * Implementation of hook_init().
./modules/system/system.module:538: * Implementation of hook_init().
./update.php:18: * operations, such as hook_init() and hook_exit() invokes,
css/js preprocessing
 
    这里,我们在我们Drupal文件中,递归的(-r)搜索带有字符串“hook_init”的实例,并将它们出现的位置以及行号(-n)打印出来。我们可以进一步的提炼搜索结果。在下面的这个例子中,我们在前面例子的搜索结果集的基础上,进一步的搜索单词“poll”出现的情况:
 
$ egrep -rn 'hook_init' . | egrep 'poll'
./modules/poll/poll.module:24: * Implementation of hook_init().
 
    提炼你的搜索的另一种方式,就是为egrep使用-v标记,它意味着“反转这个匹配”,也就是说,不匹配该字符串的就是所要匹配的。让我们查找单词“lock”出现的所有地方,同时排除单词block或Block的情景:
 
$ egrep -rn 'lock' . | egrep -v '[B|b]lock'
./includes/common.inc:2548: // See if the semaphore is still locked.
./includes/database.mysql.inc:327:function db_lock_table($table) {
./includes/database.mysql.inc:332: * Unlock all locked tables.
...
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 使用一个标签或分支的名字签出Drupal

我们在前面的“从CVS签出Drupal”一节中,已经学到了如何根据DRUPAL-6分支上的一个标签来获取代码的一个版本。我们以此为基础,做些变通,来取回各种标签和分支下的代码,并把它们放在当前目录中的drupal文件夹下。

    签出DRUPAL-6分支的一个拷贝,代码与它的第一个beta发布完全相同:
 
cvs -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout
-r DRUPAL-6-0-BETA-1 drupal
 
    签出Drupal 6.2的一个拷贝:
 
cvs -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout
-r DRUPAL-6-2 drupal
 
    签出6.x-dev的一个拷贝(也就是DRUPAL-6分支上的最近代码,包括最近发布以后的任何bug修正):
 
cvs -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout
-r DRUPAL-6 drupal
 
    签出HEAD的最近版本的一个拷贝(也就是7.x-dev)。注意,在这种情况下不需要指定分支:
 
cvs -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal checkout
drupal
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 使用带有CVS的Drupal

 

那么使用这个带有CVS的Drupal,能够给你带来哪些好处呢?
 
你可以在正式的安全公告发布以前,对Drupal基准代码应用安全更新。我是不是已经提到了很容易便可实现这一点?不需要下载一个完整的Drupal最新版本,你只需要简单的运行单个CVS命令就可以了。
能够维护对Drupal代码的自定义修改。修改Drupal核心是最不应该做的事情,但是如果你必须修改它的话,那么修改时使用CVS。即便是你修改了核心文件,CVS也将聪明的尝试更新,这样在更新流程中,你就不会在不经意间将你的自定义修改覆盖掉了。
你也可以使用CVS来发现其他开发者对Drupal核心文件所做的修改。如果你的Drupal工作拷贝与Drupal服务器上干净的基准代码有所不同,那么你可使用一个简单的命令,将这些不同之处逐行列出。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 使用编码器模块

http://drupal.org/project/coder,你将找到一个宝贝,它能帮你节省不少的时间,减轻你的烦恼。这就是编码器模块:一个用来评估其它模块代码的模块。

    下载最新的版本,将它放到sites/all/modules/contrib/下,接着在“管理➤站点构建➤模块”启用它,这和其它模块的安装一样。
    为了使用编码器模块评估你的模块,点击你站点导航中的新链接“代码评估”,选择你想要的评估类型,以及选择你想要评估的模块或主题。或者导航到“管理➤站点构建➤模块”,在模块列表中,这个模块提供了一个方便的“代码评估”链接供你使用。
 
提示 如果你想快速的适应Drupal的编码习惯,那么编码器模块应该是你的必选模块之一。
 
    你甚至可以更进一步,那就是使用编码器模块自带的coder_format.php脚本。这个脚本事实上可以修正你的代码格式错误。下面是如何使用coder_format.php来检查我们在第2章编写的注释模块:
 
$ cd sites/all/modules
$ php contrib/coder/scripts/coder_format/coder_format.php \
custom/annotate/annotate.module
 
    脚本就地修改了annotate.module文件,并将原始文件保存为了annotate.module.coder.orig。为了看到脚本都做了什么,可以使用diff:
 
$ diff custom/annotate/annotate.module custom/annotate/annotate.module.coder.orig
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 函数调用

函数调用

    在函数调用中,在操作符(=, <, >,等等)的两边应该各有一个空格,而在函数名和函数的开括号“(”之间则没有空格。在函数的开括号“(”和它的第一个参数之间也没有空格。中间的函数参数使用逗号和空格分隔,在最后一个参数和闭括号“)”之间没有空格。下面的例子说明了这几点:
 
错误的
$var=foo ($bar,$baz);
 
正确的
$var = foo($bar, $baz);
 
    这个规则也存在例外的情况。在一个包含多个相关赋值语句的区块中,如果能够提高可读性,那么可以在赋值操作符周围插入更多空格:
 
$a_value       = foo($b);
$another_value = bar();
$third_value   = baz();
 
函数声明
    在函数的名字和它的开括号“(”之间没有空格。在编写函数时,如果它的有些参数需要使用默认值,那么需要把这些参数列在后面。还有,如果你的函数生成了任何有用的数据,那么你需要返回该数据,以供调用者使用。下面给出了一些函数声明的例子:
 
错误的
function foo ($bar = 'baz', $qux){
    $value = $qux + some_function($bar);
}
 
正确的
function foo($qux, $bar = 'baz') {
    $value = $qux + some_function($bar);
    return $value;
}
 
函数名字
    在Drupal中,函数的名字都是小写的,并基于模块的名字或者它们所属系统的名字。这个习惯避免了命名空间冲突。下划线用来分隔函数名字的描述性部分。在模块名的后面,应该紧跟一个动词,接着是动词作用的对象:modulename_verb_object()。在下面的第一个例子中,函数名字没有正确的使用模块前缀,并且动词和它的对象颠倒了。在接下来的例子中,很明显,修正了这些错误。
 
错误的
function some_text_munge() {
    ...
}
 
正确的
function mymodule_munge_some_text() {
    ...
}
 
    私有函数与其它函数一样,遵守相同的习惯,不过它在函数名字前面加了一个下划线。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 创建一个Drupal6分支

Drupal7出来以后,我们想继续开发Drupal6下的模块。而现在, DRUPAL-5分支的开发工作基本上停止不前了。但是我们也不能在HEAD上同时开发Drupal 7和Drupal 6下的版本啊?现在需要为Drupal 6创建一个分支,将特定于Drupal 6的开发放在那里进行。首先,我们需要确定我们使用的是HEAD的最近版本。接着,为Drupal 6创建分支。

 
cvs update -A
cvs tag -b DRUPAL-6--1
 
    现在,我们的开发历史就如图21-14所示了。
21-14.Drupal 6创建一个分支。
 
    稍等一下!为什么对于DRUPAL-6分支,我们使用了DRUPAL-6--1,而没有使用DRUPAL-6?答案很简单:从Drupal 6开始,分支的名字能够更具体的描述它们的含义了(更多详细可参看http://drupal.org/node/147493)。你为DRUPAL-6分支将要创建的标签是用于发布的6.x-1.x系列的。这意味着这些发布与Drupal 6的任何版本都兼容,并且属于发布的第一个系列。图21-15显示了对应于你模块的6.x-1.2 和6.x-1.3发布的标签;图21-16显示了对应于标签的发布。
 
21-15.显示标签名的6.x-1.x发布系列
 
21-16.显示发布版本号的6.x-1.x发布系列
 

Drupal版本:

Drupal专业开发指南 第21章 创建一个兼容Drupal5的分支

让我们继续前进,来创建分支:

 
cvs tag -b DRUPAL-5
 
    不要被这里的单词tag搞晕了。由于我们加了-b选项,所以这里创建的是一个分支,而不是一个标签(更准确的说,一个分支就是一个标签,一个特殊的标签,不过我们这里将其简单化一点)。执行了上面的命令以后,模块的开发历史就带有了新的DRUPAL-5分支了,如图21-7所示。
 
 
21-7.带有Drupal5分支的模块开发历史
 
    注意,由于我们还没有做任何修改,所以此时代码在两个分支中还是完全相同的。
    现在让我们看一个现实的问题。我们的模块依赖于badger(徽章)模块!而我们尚未在.info文件中对此做出声明。此外,Drupal5描述依赖关系的语法与Drupal6及以后版本的语法不一样。所以,让我们为DRUPAL-5分支添加Drupal5版本。但是如何判定本地工作空间包含了什么呢?本地空间中的那些文件是来自于DRUPAL-5分支,还是来自于HEAD?让我们声明我们需要DRUPAL-5分支的文件:
 
cvs update -dP -r DRUPAL-5
 
    这个命令的意思是说,“获取DRUPAL-5分支的文件,创建任何需要的新目录,删除任何不再需要的空目录”。现在让我们修改.info文件:
 
// $Id: foo.info,v 1.2 2008/05/22 14:28:25 jvandyk Exp $
name = Foo
description = Sends badgers to those who use it.
dependencies = badger
 
    让我们查看这些变更:
 
cvs diff -up foo.info
 
===================================================================
RCS file: / cvs/drupal-contrib/contributions/modules/foo/foo.info,v
retrieving revision 1.2
diff -u -u -p -r1.2 foo.info
--- foo.info 22 May 2008 14:28:25 -0000 1.2
+++ foo.info 22 May 2008 16:40:53 -0000
@@ -1,4 +1,4 @@
// $Id: foo.info,v 1.2 2008/05/22 14:28:25 jvandyk Exp $
name = Foo
description = Sends badgers to those who use it.
-core = 6.x
+dependencies = badger
 
    注意我们删除了“core = 6.x”(因为这是个Drupal6特性,而现在是在DRUPAL-5分支上),让我们查看一下状态:
 
cvs status foo.info
 
===================================================================
File: foo.info Status: Locally Modified
Working revision: 1.2 2008-05-22 09:28:25 -0500
Repository revision: 1.2 / cvs/drupal-contrib/
contributions/modules/foo/foo.info,v
Commit Identifier: LYpsSr1ZkEut7Y3t
Sticky Tag:    DRUPAL-5 (branch: 1.2.2)
Sticky Date:    (none)
Sticky Options: (none)
 
    注意,状态现在为Locally Modified本地已修改),而Sticky Tag字段指示我们使用的是DRUPAL-5分支。
    最后,让我们提交变更:
 
cvs commit -m "Drupal-5-compatible dependency on badger module."
 
    图21-8给出了现在的模块开发历史。
 
21-8.DRUPAL-5分支提交代码后,模块的开发历史
 

Drupal版本:

Drupal专业开发指南 第21章 创建一个兼容Drupal6的分支

我们已经为Drupal5创建了一个分支,并在该分支上创建了一个标签。现在让我们把精力主要放在Drupal 6上,来添加对badger(徽章)模块的依赖关系。但是首先,我们需要做出一个决定。我们是应该立即创建一个分支呢?还是应该简单的使用HEAD?由于我们可以在任何想要的地方创建标签,所以这个问题就是,又没有必要创建一个DRUPAL-6分支?让我们检查一下这两种不同的方式。

 
为发布使用HEAD
    用于新发布的一种方式是,编辑HEAD上的foo.info文件来添加依赖关系。首先,由于我们刚才使用的是DRUPAL-5分支下的文件,所以我们需要从HEAD获取文件,并将其放入到本地工作空间中。你可能会认为使用下面的命令就可以了:
 
cvs update -dP -r HEAD
 
    然而,这将在你的本地工作空间生成一个粘性标签,如果你想使用设置为HEAD的粘性标签执行提交的话,那么你将收到一个如下所示的错误:
 
cvs commit: sticky tag `HEAD' for file `foo.info' is not a branch
cvs [commit aborted]: correct above errors first!
 
    解决方案是使用下面的命令,它将重置粘性标签:
 
cvs update –A
 
    现在,让我们添加依赖信息,这里使用了Drupal6所用的方括号格式:
 
// $Id: foo.info,v 1.2 2008/05/22 14:28:25 jvandyk Exp $
name = Foo
description = Sends badgers to those who use it.
dependencies[] = badger
core = 6.x
 
    你可以使用cvs diff和cvs status来检查变更。接着提交变更:
 
cvs commit -m "Drupal-6-compatible dependency on badger module."
 
    图21-11显示了我们最近的变更。
 
21-11.开发历史显示HEAD上有一提交
 
    由于现在我们还不为Drupal 7做任何开发工作,所以我们可以使用HEAD来放置新的开发工作。这意味着少加了一个分支,也就少添了一些麻烦。分支少,就意味着提交bug修正的地方少了。让我们继续前进,为Drupal6创建第一个标签。由于它是兼容Drupal核心6.x系列的第一个发布,所以我们把它叫做DRUPAL-6--1-0:
 
cvs tag DRUPAL-6--1-0
 
    这个标签是创建在HEAD上的,如图21-12所示。
 
21-12.一个应用于CVS树的主干的标签
 
    假定我们继续更新模块,又创建了多个提交和发布。那么我们的开发历史,将很快变成图21-13所示的样子。
 
21-13.Drupal6发布使用HEAD所做的开发
 

Drupal版本:

Drupal专业开发指南 第21章 创建一个分支

我们知道,还有许多懒惰的用户还在使用Drupal5,现在让我们看看如何为他们创建一个分支。

 
警告 只有在drupal.org上为你的模块创建了一个工程以后,才能为你的模块创建一个分支。
 
    首先,你需要确定你使用的是HEAD的最近版本:
 
cvs update –dP
 
    另一种检查的方式是,使用CVS状态命令。让我们检查一下foo.info文件的状态:
 
cvs status foo.info
 
===================================================================
File: foo.info Status: Up-to-date
Working revision: 1.2 2008-05-22 09:28:25 -0500
Repository revision: 1.2 / cvs/drupal-contrib/
contributions/modules/foo/foo.info,v
Commit Identifier: LYpsSr1ZkEut7Y3t
Sticky Tag: (none)
Sticky Date: (none)
Sticky Options: (none)
 
    注意上面所列的状态为Up-to-date(最近的)。这意味着你的本地文件与资源库中的文件完全相同。如果你修改了你的本地文件却没有提交到资源库,或者你忘记删除一些调试代码了,那么状态将变为Locally Modified本地已修改)。还有Sticky Tag字段的值为(none),这意味着你在使用HEAD。
 

Drupal版本:

Drupal专业开发指南 第21章 创建一个发布节点

为了使那些不熟悉CVS的人也可以下载你的模块,你应该在drupal.org上创建一个发布节点。一个发布节点提供了给定发布标签的相关信息,而drupal.org上的打包脚本能够自动的为发布标签指示的文件构建一个tarball(沓包)。例如,你能为你模块的DRUPAL-6--1-3标签创建一个发布节点。打包脚本从DRUPAL-6—1分支取出DRUPAL-6--1-3标签所标示的文件,然后为其创建一个tarball(沓包)和一个链接,这样drupal.org上的访问者就可以下载这个tarball(沓包)了。而tarball(沓包)的名字则应该为foo-6.x-1.3.tar.gz。

    为了创建一个发布节点,我们导航到我们在http://drupal.org创建的工程页面(参看“在drupal.org上创建一个工程”),并点击“添加发布”链接。接着选择这个发布所代表的CVS标签,并指出这个发布中的变更是安全更新呢,还是bug修正,还是新特性。
    在发布节点的正文中,应该列出这个特定发布的新功能。可以把这些看作是发布笔记。我们还应该列出都解决了哪些问题,最好能够给出指向问题节点的链接。图21-20给出了一个典型的发布节点。有一个方便的脚本,位于http://cvs.drupal.org/viewvc.py/drupal/contributions/tricks/cvs-release-notes,它能帮我们自动生成一列修正的问题。
 
21-20.漂亮菜单(nice menus)模块的6.x-1.1版的发布节点
 
    一旦创建了发布节点并运行了打包脚本以后,发布节点将被添加到工程页面,任何人都可以在这里下载tarball(沓包),或者阅读你输入的发布笔记,如图21-21所示。
 
21-21.在工程页面可下载已发布的沓包。

Drupal版本:

Drupal专业开发指南 第21章 创建和应用补丁

Think in Drupal

如果你想修正bug,或者想测试一下他人的潜在的bug修正,或者由于这个或者那个原因需要修改核心代码,那么此时你就开始需要创建或者应用一个补丁了。一个补丁就是用户和计算机都可读的一个文本文件,它根据对Drupal代码资源库所做修改,为用户显示逐行的报告。补丁是由diff(或 cvs diff)程序生成的,我们在前面的“追踪Drupal代码变更”一节中,已经看到了一个例子。
 
创建一个补丁
    下面是补丁的示例,它用来整理includes/common.inc中t()函数的文档:
 
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.591
diff -u -r1.591 common.inc
--- includes/common.inc 28 Mar 2007 07:03:33 -0000 1.591
+++ includes/common.inc 28 Mar 2007 18:43:18 -0000
@@ -639,7 +639,7 @@
 *
 * Special variables called "placeholders" are used to signal dynamic
  * information in a string, which should not be translated. Placeholders
- * can also be used for text that that may change from time to time
+ * can also be used for text that may change from time to time
 * (such as link paths) to be changed without requiring updates to translations.
 *
 * For example:
 
    在对includes/common.inc文件做了修改以后,开发者在Drupal根路径下运行下面的命令:
 
cvs diff -up > common.inc_50.patch
 
    这个命令获得cvs diff的输出,并将其放到名为common.inc_50.patch的新文件中。接着开发者访问drupal.org,并在这里报告bug:http://drupal.org/node/100232
 

Drupal版本:

Drupal专业开发指南 第21章 初始提交

现在到了关键时刻了。是时候将你的文件提交到资源库中了!是不是有点紧张。检查/path/to/local/copy/of/contributions/modules/foo,看看它是不是包含了所有的文件并且里面包含了你想要提交的代码。接着,输入决定性的命令。使用一个简洁的句子来描述你模块的功能,接着继续前进:

 
cvs commit -m "Initial commit of foo module. This module sends badgers to those
who use it."
 
    -m标记意味着,后面引号内中的内容是一个消息,它将和代码提交被一同记录下来。在你的消息中,你应该提供一些有用的信息。如果你想输入多行的文本,而你安装的CVS自动打开了一个文本编辑器,那么省略-m标记可能会有所用处。在我的OS X机子上,它打开了一个vim编辑器,给我显示了一个如下所示的界面:
 
CVS: ----------------------------------------------------------------------
CVS: Enter Log. Lines beginning with `CVS:' are removed automatically
CVS:
CVS: Committing in .
CVS:
CVS: Added Files:
CVS: foo.info
CVS: foo.module
CVS: ----------------------------------------------------------------------
~
~
 
    如果你以前从没有用过vim,这可能会有点恐怖。使用向下箭头键导航到以“CVS:”开头的最后一行,接着按下“o”键(就是“oh boy!”中的“o”)。接着键入你的长一点的提交消息,完成以后,按下Esc键,接着键入:wq来退出程序。通过设置CVSEDITOR环境变量,你就可以使用自己喜欢的编辑器了;例如,对于emacs,可以这样设置变量:
 
export CVSEDITOR=emacs
or like this for Textmate:
export CVSEDITOR="mate -w"

Drupal版本:

Drupal专业开发指南 第21章 利用版本控制

对于任何软件项目,版本控制都是必须的,同样Drupal也不例外。版本控制用来追踪Drupal中的每个文件上的所有变更。它保存了所有修订本的历史以及每个修订本的作者。你可以从中得到一个逐行的报告,里面包含谁做了变更以及什么时间什么原因。版本控制也可以简化新版本的发布流程。Drupal社区使用久经锤炼的可靠的CVS软件,来维护它的修订本历史。

 
提示 有多种版本控制系统(Bazaar, CVS, Git, Subversion,等等),对于它们之间的优缺点的讨论,在Drupal开发邮件列表中会经常出现。如果你也想发篇关于这方面的文章,那么你可以先看看归档文章,熟悉一下以前的讨论。
 
    修订本控制的好处不局限于管理Drupal工程。你也可以利用Drupal的CVS来帮助维护基于Drupal的工程,这样能够极大的降低你的维护成本。首先,你需要改变一下你的Drupal安装方式。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 在drupal.org上创建一个工程

由于你把你的模块贡献给了社区,如果能够使用一种结构化的方式,让模块的其它用户能与你进行交互,这应该是再好不过了。这样,你就不会经常收到哪些不期而遇的电子邮件了,而且还有一种标准的方式用来追踪请求的特性、bug修正、等等。登录到drupal.org以后,访问http://drupal.org/node/add/project,或者使用站点导航菜单导航到“创建内容➤工程”,然后填充表单;你需要格外注意一下“完整描述”字段,在这里你可以描述你的模块(或者主题)。填完表单以后,你就可以访问你的工程了,工程地址为:http://drupal.org/project/yourprojectname

 
警告 在你执行任何标签化或分支化以前,一定要创建一个工程。
 

Drupal版本:

Drupal专业开发指南 第21章 在工程管理中混合使用SVN和CVS

Drupal的核心代码使用了CVS的,然而你工程的其余部分,可能完全没有使用版本控制,也可能使用了一个不同的版本控制系统。

    通常的实践是,使用另外一个的不冲突的版本控制系统比如Subversion(SVN),来将整个工程(包括Drupal和它的CVS元数据)存放在它自己的资源库中。这一想法是,我们首先对Drupal核心执行CVS更新操作(从cvs.drupal.org获取变更),接着切换到SVN,使用SVN提交这些变更(这将它们放进你的SVN资源库中)。你可以使用这个SVN资源库,来存放任何自定义的模块、主题、图片、甚至你的工程的数据库模式。
 
注意 关于Subversion的更多详细,可参看http://subversion.tigris.org
 

Drupal版本:

Drupal专业开发指南 第21章 处理耗费时间的查询

Think in Drupal

下面是一个例子,如何使用devel模块识别缓慢的查询,从来提高站点的性能。假定我们已经编写了一个自定义节点模块task(任务),而且使用hook_load()来向节点对象添加关于任务的附加信息。表的模式如下:

 
CREATE TABLE task (
    nid int,
    vid int,
    percent_done int,
    PRIMARY KEY (nid,vid),
    KEY nid (nid)
);
 
    在运行了devel.module和查看了查询日志以后,我们注意到对前面这个表进行的查询拖累了站点性能!注意超过5毫秒的查询,就被默认为缓慢的(导航到“管理➤站点配置➤Devel设置”,你可以修改这个值)。
 
毫秒            函数           查询
27.16        task_load      SELECT * FROM task WHERE vid = 3
 
    那么,为什么这个查询这么耗费时间呢?如果它是一个使用多表关联的复杂查询,那么我们将考虑使用更好的方式来组织数据,但是在这里它是一个非常简单的查询。首先,我们使用SQL的EXPLAIN语法,来查看数据库是怎么解释这个查询的。当我们在一个SELECT语句前面添加一个关键字EXPLAIN时,数据库将返回这个查询执行计划的相关信息:
 
EXPLAIN SELECT * FROM task WHERE vid = 3
 
    MySQL给出了下面的报告:
 
Id select_type table  type    possible_keys key  key_len  ref  rows  Extra
1  SIMPLE      task    system      NULL      NULL NULL     NULL  1
 
    这里最重要的一列就是key列,它现在为NULL。这告诉我们,MySQL在取回结果集时没有使用任何主键、唯一键、或者索引键;它需要逐行进行查找。所以提升这个查询的速度的最好方式,就是向vid列添加一个唯一键。
 
ALTER TABLE task ADD UNIQUE (vid);
 
    关于MySQL的EXPLAIN的更多信息,可参看http://dev.mysql.com/doc/refman/5.0/en/explain.html
 

Drupal版本:

Drupal专业开发指南 第21章 安装一个CVS客户端

老葛的Drupal培训班 Think in Drupal

在命令行中运行下面的命令,来测试是否安装了一个CVS客户端:

 
$ cvs
 
    如果你收到了一个“命令未找到”的错误,那么你可能需要安装一个CVS客户端。Windows用户可以看一下TortoiseCVS(http://tortoisecvs.sourceforge.net/)。而Mac用户可参看这篇文章http://developer.apple.com/internet/opensource/cvsoverview.html。Linux用户,你自己应该知道怎么做。
    如果运行了这个cvs命令以后,系统为你输出了下面的CVS文档,那么说明你已经安装了CVS客户端!
 
Usage: cvs [cvs-options] command [command-options-and-arguments]
 

Drupal版本:

Drupal专业开发指南 第21章 安装带有CVS的Drupal

当你从drupal.org的下载页面,下载了Drupal压缩包时,代码的这份拷贝没有带有任何版本信息;版本信息可用来告诉你基准代码的当前状态。

    对于版本问题,使用CVS的开发者可以迅速的找到答案并应用更新,而其他的开发者则仍然需要下载新版本。
 
注意 有两种下载Drupal的方式,它们唯一的可见区别是, CVS签出的包含了一个名为“CVS”的附加文件夹,Drupal中的所有目录里面都包含了一个这样的文件夹,它们用来存放CVS信息。Drupal的.htaccess文件包含了一组规则,如果你使用的是Apache的话,这些规则可用来自动屏蔽掉这些文件夹(一些CVS客户端,比如TortoiseCVS,能够默认隐藏CVS文件夹)。
 
    可能会有人告诉你CVS版的Drupal不安全,而且使用CVS获取的最新代码也不稳定。这是一个常见的误解,它混淆了两个概念。这些人在这里指的是一个工程的HEAD版本;也就是,在这个Drupal版本中(或者使用CVS的其它工程),为了准备下一个发布,正在测试新特性。然而,CVS可同时用来维护一个软件的HEAD版本和所有的稳定版本。

老葛的Drupal培训班  Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 将你的模块添加到资源库

现在你有了一份贡献资源库中modules子目录的拷贝,你可能想现在就可以将你的模块其它上千个模块放到一起了。我们先不要急!首先,花点时间调查一下资源库中是不是已经有一个模块解决了你的问题。下面是一些资源,可帮你确定这一点:

 
•允许你根据类别,名字,或日期浏览模块,还可以使用主发布兼容性(Drupal 6, Drupal 5,等等)过滤模块。
http://drupal.org/node/23789概括了一些基本的方式,如何在他人的基础之上贡献自己的力量。
http://drupalmodules.com可方便的用来搜索第3方模块,同时还可以对模块进行评价和打分。
 
    如果你觉得你的模块值得编写,现在就可以开发它了。让我们创建一个模块。
    下面是.info文件:
 
// $Id$
name = Foo
core = 6.x
 
    而下面是模块本身:
 
<?php
// $Id$
 
/**
 * @file
 * The greatest module ever made.
 */
 
    现在,模块的目录就包含了前面的两个文件,它看起来应该这个样子:
 
foo/
    foo.info
    foo.module
 
    继续前进,将新模块复制到你新签出的贡献资源库中:
 
cp -R foo /path/to/local/copy/of/contributions/modules
 
    接着,将新目录告诉CVS:
 
cd /path/to/local/copy/of/contributions
cvs add modules/foo
 
    把目录中的文件也添加进来:
 
cvs add modules/foo/*
 
    CVS将提醒你,尽管这些文件的添加已被预订了,但是你还需要提交它们:
 
cvs add: 使用`cvs commit'来永久的添加这些文件
 
    如果你的模块包含子目录,由于CVS不能递归的添加,所以你也需要添加这些子目录:
 
cvs add modules/foo/subdir1
cvs add modules/foo/subdir1/*

Drupal版本:

Drupal专业开发指南 第21章 常量

常量

    PHP常量应该全部大写,并使用下划线来正确的分隔字词:
 
/**
 * First bootstrap phase: initialize configuration.
 */
define('DRUPAL_BOOTSTRAP_CONFIGURATION', 0);
 
    常量的名字也应该使用它们的模块名作为前缀,这样就可以避免常量之间的命名冲突。例如,假定你在编写tiger.module,那么需要使用TIGER_STRIPED,而不是使用STRIPED。
 
全局变量
    在你自己的模块中,最好不要使用全局变量。如果你必须使用全局变量,那么命名方式为:下划线,接着是你的命名空间(也就是你的模块或主题的名字),再接一个下划线,之后紧跟一个描述性的名字。
 
错误的
global $records;
 
正确的
global $_mymodulename_access_records;

老葛的Drupal培训班  Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 干净的修改核心代码

 

你应该尽可能的不去修改核心代码。但是有时候,你可能不得不修改。如果你需要修改,那么要确保能有一种方式让你准确的追踪变更。让我们看一个简单的例子;我们将编辑sites/default/default.settings.php文件。在143行,你将看到下面一行代码:
 
ini_set('session.cookie_lifetime', 2000000);
 
    这个值用来控制cookies的存活时间(单位为秒)。让我们假定数据库中的sessions表膨胀的太快了,所以我们需要降低这些会话的有效期。我们可以直接修改这个值,但是如果资源库中的这一行也被修改了,那么在接下来的CVS更新中,我们将得到一个冲突并需要手工的解决这一问题。
    一个干净的解决方案,就是将我们想要修改的那行代码注释掉,复制该行代码并将其放在文件中原有代码的下面,然后再做修改:
 
/* Original value - Changed to reduce cookie lifetime
ini_set('session.cookie_lifetime', 2000000);
*/
ini_set('session.cookie_lifetime', 1000000); // We added this.
 
    由于原始的那行代码未被修改,所以运行CVS时就不会出现冲突了。
 

Drupal版本:

Drupal专业开发指南 第21章 应用一个补丁

 

补丁是根据cvs diff或者diff命令的输出所创建的文件。在你创建或者下载了一个补丁以后,导航到Drupal的根目录并运行下面的命令:
 
patch -p0 < path/to/patchfile/patchfile.patch
 
    如果补丁是在Drupal安装的根目录下创建的,而你也是在你的Drupal安装的根目录应用的,那么路径应该是相同的,所以-p0(这里是一个零)标记用来告诉补丁程序使用在补丁文件中找到的路径(也就是,从路径前缀中去掉0部分)。
    如果你在应用补丁时遇到了问题,那么可以在http://drupal.org/node/60116寻找相关帮助。
    有时,为了提升速度或者添加缺失的功能,你可能想为你的实际站点应用一个补丁。这件事的最佳实践是,创建一个patchescvs diff –up”命令来重新创建补丁。在同一个文件夹下,你还需要创建一个文本文件用来说明每个补丁的应用原因。你可以使用一个命名习惯,使得名字能够提供一些清晰的上下文信息,例如:文件夹,用来在每个补丁应用后存放它的拷贝。如果你还没有这样做,你可以对文件运行“
 
modulename-description-of-problem-NODEID-COMMENTNUM.patch
 
    假定你使用了工作流和令牌(token)模块,但是这两个模块之间整合的不大好。有人提交了一个补丁,修正了这个问题,但是模块的开发者还没有把补丁合并到一个新发布中,而你的站点明天就要上线,你现在就需要它。你应该这样为补丁命名:
 
workflow-conflict-with-token-api-12345-67.patch
 
    这样,当需要更新站点时,负责更新的任何人现在都可以断定以下几点:
 
•这个安装的哪些部分被修改了?
•为什么做这些修改?
•这个补丁现在是不是被合并到了主流代码中?
•如果没有的话,是不是有人也遇到了同样的问题并提供了更好的解决方案?
 

Drupal版本:

Drupal专业开发指南 第21章 应用剖析和调试

下面的PHP调试器和集成开发环境(IDE),提供了一些强大工具,能够帮你快速找到Drupal的瓶颈所在;它们也能够帮你找出模块中的低效算法:

 
Zend Studio IDE: http://www.zend.com/
Eclipse IDE: http://www.eclipse.org/
Xdebug PHP Extension: http://www.xdebug.org/
 
    在下面的图中,我们使用了Zend Studio(拥有最漂亮的图形输出,这存在争议)的截图,而其它的IDE也能够生成类似的输出。图21-22显示了一个图形输出,它是使用应用剖析器(application profiler)追踪Drupal请求得到的。结果显示了每个文件里面的函数的运行所占用的相对时间。在这个情况下,Drupal看起来在includes/bootstrap.inc中花费了将近一半的时间。
 
21-22.Zend IDE中,一个Drupal请求的时间饼图
 
    在图21-23和21-24中,我们向下钻取,来查看哪些函数在一个请求期间耗费了相对较多的处理器时间。这一特性,能方便的帮你判定哪些地方需要多花一点工夫进行优化。
 
21-23.Zend IDE中,一个Drupal请求的调用踪迹
 
21-24.Zend IDE中,一个Drupal请求的函数统计
 
    实时调试是一个PHP的特性,而不是Drupal的特性,但是它值得在这里讨论一下,如果你的笔记本上装了一个实时调试器,那么别人就会把你当作Drupal高手。
    使用一个PHP调试器,可以让你在运行时暂停PHP代码的执行(比如,设置一个断点),并逐步的检查发生了什么。熟悉一个PHP调试器,是你最应该掌握的一门技术之一。一帧一帧的追踪代码执行,就像电影中的慢动作一样,这是调试的最好方式,它能帮你慢慢的熟悉Drupal这样一个庞然大物。
    Drupal初学者的成人礼,就是端上一杯茶水,启动调试器,花一些时间逐步的追踪一个标准的Drupal请求,这能够帮你获得Drupal工作原理的第一手资料。

Drupal版本:

Drupal专业开发指南 第21章 总结

读完本章后,你应该能够:

• 按照Drupal的编码规范编写代码。
• 为你的代码添加注释,这样可以使用API模块来将你的注释转化为文档。
• 使用egrep来搜索Drupal的基准代码。
• 使用版本控制下载和更新Drupal。
• 干净的修改Drupal核心。
• 使用统一的diff格式生成显示代码变更的补丁。
• 使用别人创建的补丁。
• 使用标签和分支来维护一个第3方模块。
• 使用devel.module来提高你的开发效率。
• 通过开发者的最佳实践来识别Drupal编码高手。
 

Drupal版本:

Drupal专业开发指南 第21章 控制结构

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

 
控制结构体在控制关键字(if, elseif, while, for, 等等)和开括号“(”之间应有一个空格,从而将其与函数调用(也使用圆括号,但是没有空格)区分开来。“{”应该与关键字位于同一行(而不是自成一行)。“}”应该自成一行。
 
错误的
if ($a && $b)
{
    sink();
}
 
正确的
if ($a && $b) {
    sink();
}
elseif ($a || $b) {
    swim();
}
else {
    fly();
}
 
    花括号“{}”一般总是使用的,即便是不需要的时候,为了增强可读性并降低出错的可能,也应使用“{}”。
 
错误的
while ($a < 10)
    $a++;
 
正确的
while ($a < 10) {
$a++;
}
 
    切换语句的格式应该这样(注意“break;”语句在默认情况下不是必需的):
switch ($a) {
    case 1:
        red();
        break;
 
    case 2:
        blue();
        break;
 
    case 3:
        purple();
        // Fall through to default case.
 
    default:
        green();
}
 
    当一个情况执行完以后,打算继续执行下一情况时,此时可以省略“break;”语句,注意前面代码中的注释。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 提交一个bug修正

如果你的签出正常工作了,那么在sites/all/modules/contrib下,应该包含以下内容:

foo/
    CVS/
    foo.info
    foo.module
 
    我们刚刚分享了我们的代码,就已经有人在我们的问题列表http://drupal.org/project/yourprojectname/issues中,创建问题了。drupal.org上的用户flyingpizza在一篇文章http://drupal.org/node/1234567中指出,我们忘记在我们的.info文件中添加一个描述了!现在让我们添加一个描述:
 
// $Id: foo.info,v 1.1 2008/05/22 14:15:21 jvandyk Exp $
name = Foo
description = Sends badgers to those who use it.
core = 6.x
 
    注意,文件中的第一行,已经被CVS从“// $Id$”修改为文件的实际标识信息了。如果你看到的仍然是“// $Id$”,那么你使用的版本应该不是从CVS中签出的。
 
    在我们提交这个变更以前,通过运行cvs diff命令,让我们预览一下我们的变更:
 
cvs diff –up
 
    输出如下所示:
 
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/foo/foo.info,v
retrieving revision 1.1
diff -u -u -p -r1.1 foo.info
--- foo.info 22 May 2008 14:15:21 -0000 1.1
+++ foo.info 22 May 2008 14:21:54 -0000
@@ -1,3 +1,4 @@
// $Id: foo.info,v 1.1 2008/05/22 14:15:21 jvandyk Exp $
name = Foo
+description = Sends badgers to those who use it.
core = 6.x
 
    注意,在输出中,我们新加的一行的前面有个+字符。现在让我们继续前进,提交这个变更:
 
cvs commit -m "#1234567 by flyingpizza: Added missing description line."
 
    提交消息中的#1234567,将被自动修改为一个超链接http://drupal.org/node/1234567,显示在drupal.org的提交日志中(例如,位于http://drupal.org/cvs)。
 
提示 提交消息应该简洁并具有描述性,同时应该指出哪些人为这个提交做出了贡献。里面应该包括问题的节点号,提交补丁的用户的名字,以及为这个提交做出其它贡献的用户的名字。这样,我们就可以从CVS提交消息中方便的链接到drupal.org上的节点1234567的讨论了。如果你在怀疑是否应该在提交消息中包含某人的用户名,那么还是慷慨一点,把他人的贡献也列出来。通过指出贡献者的工作,比如修正了bug,这样可以给贡献者增加信誉,对于你自己所做的贡献,在提交时,也要恰如其分的指出。把Drupal社区的团结之爱传遍整个地球!
 
    漂亮。图21-6给出了我们到目前为止的开发工作。
 
21-6. foo模块的开发

Drupal版本:

Drupal专业开发指南 第21章 数组

 

对于数组,也是使用空格对它的每个元素和每个赋值操作符进行分割的。如果数组区块跨越了80个字符,那么每个元素都应独立成行。为了提高可读性和可维护性,最好将每个元素全部独立成行。这样你就可以方便的添加或者删除数组元素了。
 
错误的
$fruit['basket'] = array('apple'=>TRUE, 'orange'=>FALSE, 'banana'=>TRUE,
    'peach'=>FALSE);
 
正确的
$fruit['basket'] = array(
    'apple'  => TRUE,
    'orange' => FALSE,
    'banana' => TRUE,
    'peach'  => FALSE,
);
 
注意 数组中最后一个元素的后面有一个逗号,这不是一个错误,PHP允许这样。放在这里是为了防止犯错,这样开发者就可以方便的在数组列表的最后添加或者删除一个元素。这一规范是允许用的,推荐大家使用这一规范,不过它不是必须的。
 
    在创建内部的Drupal数组时,比如菜单项或者表单定义,总是将每个元素单独成行:
 
$form['flavors'] = array(
    '#type' => 'select',
    '#title' => t('Flavors'),
    '#description' => t('Choose a flavor.'),
    '#options' => $flavors,
);

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 文件名

文件名应该是小写的。例外情况就是文档文件,它们全部大写并使用.txt后缀,例如:

 
CHANGELOG.txt
INSTALL.txt
README.txt
 
    在给文件命名时,最好遵守核心中使用的习惯。核心中的手册模块的文件,如表21-1所示。
 
21-1.在手册模块和模块相关文件中使用的文件名字
文件名                       描述
book.info                       模块名字,描述,核心兼容性,依赖性
book.install                    模式定义;包含在模块安装、卸载、启用、禁用时运行                                 的钩子。
book.module                     代码
book.admin.inc                  包含了访问管理页面时所用的代码
book.pages.inc                  特定于用户(很少使用)的函数的代码
book.css                        手册相关的类和ID的默认CSS
book-rtl.css                    用于从右到左的语言的CSS覆写
book-all-books-block.tpl.php    默认模板文件
book-export-html.tpl.php        默认模板文件
book-navigation.tpl.php         默认模板文件
book-node-export-html.tpl.php  默认模板文件
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 文档例子

让我们从头到脚仔细的看看一个模块的基干,同时将不同类型的文档从中选出来进行单独说明。

    模块的第2行(在<?php开始标签之后),应该包含一个CVS标签,用来追踪文件的版本号:
 
// $Id$
 
    当把代码提交到CVS以及使用CVS更新代码(CVS的代码是最新的)时,系统将会自动对这一标签进行解析和扩展。之后,它将自动地变成下面的样子:
 
// $Id: comment.module,v 1.617.2.2 2008/04/25 20:58:46 goba Exp $
 
    在本章后面,你将学到更多关于如何使用CVS的知识。
 
    在声明函数以前,你需要花点功夫为模块写点文档,介绍模块能做什么,文档的格式如下所示:
 
/**
 * @file
 * One-line description/summary of what your module does goes here.
 *
 * A paragraph or two in broad strokes about your module and how it behaves.
 */
 
为常量编写文档
    PHP常量全部都应该大写,可以使用下划线分隔单词。当定义PHP常量时,最好能够解释一下它们是用来做什么的,如下面的代码片段所展示的这样:
 
/**
 * Role ID for authenticated users; should match what's in the "role" table.
 */
define('DRUPAL_AUTHENTICATED_RID', 2);
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 显示查询

打开页面http://example.com/?q=admin/settings/devel(如果你启用了开发区块的话,那么还可点击“Devel 设置”链接),选中“Collect query info”(收集查询信息) 和 “Display query log”(显示查询日志)旁的复选框。

    一旦你保存了这些设置,你将会看到,在每个页面的最底部,都有一列查询,这些查询就是为生成当前页面所用到的所有查询语句!此外,列表还会告诉你生成查询的函数,该查询所耗费的时间,以及查询的调用次数。
    你可以在许多方式中使用这一信息。例如,如果同一查询在单个页面中被调用了40次,那么你就需要检查一下,你的代码中是否存在一个坏的控制结构循环。如果确实如此的话,那么你可以考虑使用一个静态变量,在请求期间,来保存数据库查询结果。下面是一个例子,它给出了这个设计模式的大致样子(来自于modules/taxonomy/taxonomy.module):
 
function taxonomy_get_term($tid) {
    // Define a static variable to hold data during this page request.
    static $terms = array();
 
    // Look in the static variable and only hit the database if the data
    // for this term ID has not already been retrieved.
    if (!isset($terms[$tid])) {
        $terms[$tid] = db_fetch_object(db_query('SELECT * FROM {term_data} WHERE            tid = %d', $tid));
    }
 
    return $terms[$tid];
}
 
    我们创建了一个静态数组来保存结果集,这样,如果查询已被执行过了,那么我们就已经获得了该值,这样就可以直接将其返回,而不需要再次查询数据库了。
 

Drupal版本:

Drupal专业开发指南 第21章 查看一个文件的历史

你可以使用cvs log命令来查看一个文件的历史。让我们看看foo.info文件的两次提交:

 
cvs log foo.info
 
----------------------------
revision 1.2
date: 2008-05-22 09:28:25 -0500; author: jvandyk; state: Exp; lines: +2 -1;
commitid: LYpsSr1ZkEut7Y3t;
"#1234567 by flyingpizza: Added missing description line.
----------------------------
revision 1.1
date: 2008-05-22 09:15:21 -0500; author: jvandyk; state: Exp;
commitid: wcK48PdiM0yZ2Y3t;
Initial commit of foo.module. This module sends badgers to those who use it.
=============================================================================
 

Drupal版本:

Drupal专业开发指南 第21章 标签化和创建一个发布

现在模块已经可用于Drupal 5了。让我们继续前进,来创建一个发布。我们将通过创建一个标签来实现这一点。

 
注意 一个标签(tag)就是给处于特定状态的文件贴个标签(label)。当用户下载加了标签的代码时,他/她得到的文件与贴标签时的状态完全一样。这就是为什么标签化在创建发布时非常有用。
 
    记住,一个标签就意味着一个发布。由于这是DRUPAL-5分支上我们代码的第一个发布,所以我们知道该标签应该为DRUPAL-5--1-0。图21-9说明了该标签的实际含义。
 
21-9.在标签名字和生成的模块版本之间的关系
 
    在贴标签以前,最好先使用cvs status看看状态,以确定当前的文件就是你想要的。接着继续前进,来创建标签:
 
cvs tag DRUPAL-5--1-0
 
    回顾一下我们模块的开发历史,如图21-10所示。
 
21-10.开发历史显示了DRUPAL-5分支上的标签
 

Drupal版本:

Drupal专业开发指南 第21章 标签和分支

打标签和做分支是许多修订本控制系统的标准练习。我们将学习一下,如何在Drupal核心和贡献的模块中使用这些概念。花点时间好好的理解一下这些概念,可帮你节省不少的时间,并减轻不少的烦恼。

 
Drupal核心中的分支
    当发布一个新的Drupal版本时,,维护者将在CVS中创建一个分支,它实际就是当前HEAD基准代码的一个克隆。这既允许了在代码的原有主干上继续开发新特性,而同时也允许了社区来完善一个新的稳定版本。Drupal 6就是这样创建的。实际的规范分支名有DRUPAL-4-6-0, DRUPAL-4-7-0, DRUPAL-5,和DRUPAL-6(注意,在Drupal5 中,命名规范改变了;删除了第3个数字)。
    让我们看看它是怎么工作的。在下面的系列图示中,注意时间位于竖向轴上。随着Drupal开发的持续进行,bug修正和新特性被提交到了基准代码中;开发的最前沿(前线)叫做HEAD,如图21-1所示。
 
21-1. Drupal开发时间线
 
    当代码足够成熟,值得为其创建一个分支时,Drupal核心的负责人就会为给定发布在树上创建一个稳定的分支。此时,新建分支和HEAD中的代码是相同的。接着,继续向树的HEAD添加新特性和bug修正,向稳定的分支添加bug修正,如图21-2所示。稳定的分支,作为一个规则,它只接收bug修正;而新特性只能添加到HEAD中。它们之所以叫做“稳定的”分支,就是因为保证了它们不会被突然改变。
 
21-2.创建了一个分支。
 
    当向稳定分支提交了足够的bug修正以后,核心的负责人决定应该再创建一个Drupal正式的发布,这样又创建了一个发布。但是,这里的新发布是使用标签创建的,而不是使用分支,下面让我们学习一下标签。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 标签和版本号

在分支名字比如DRUPAL-6—1和标签名字比如DRUPAL-6--1-3中,我们看到有两个连字符是连着的。如果你把紧挨着6的连字符看作是Drupal的一个发布的通配符,那么就不难理解了。也就是说,DRUPAL-6--1-3标签,对应于你模块的6.x-1.3发布,它与Drupal 6的任意发布都兼容(Drupal 6.1, Drupal 6.2, Drupal 6.3,等等)。把标签名中主版本号后面的连字符,想象成可以翻译为发布号的x,如下所示:

 
译者注: 假定我们创建了如下的标签名,DRUPAL-6-1-1-3,DRUPAL-6-2-1-3,DRUPAL-6-3-1-3,这样这里面就有了4个数字,它们使用连字符分割,第一个数字就是Drupal的主版本号,第2个数字就是Drupal在主版本下的具体发布号,第3个数字就是模块的主版本号,第4个数字就是模块在主版本下的具体发布号,这样以来,就很好理解DRUPAL-6--1-3,在这里我们可以把它看成第2个数字为空的情况,而这种情况在默认情况下,匹配Drupal6下的所有具体发布。也就是说,省略第2个数字,表示模块兼容于Drupal主版本下的所有具体发布。我觉得这样解释,比上面的更容易理解一点。
 
    现在,我们为Drupal 6创建了一个分支,我们可以继续使用HEAD来开发Drupal7下的版本----直到Drupal8问世,那时我们需要为Drupal7创建一个分支。图21-17显示了Drupal 7开发的样子。这种方式与前面我们创建DRUPAL-5分支后开始Drupal 6开发的方式完全一样。
 
21-17.Drupal6开发位于自己的分支下,而Drupal7开发则使用HEAD

Drupal版本:

Drupal专业开发指南 第21章 模块名字

永远不要在模块名字中使用下划线。为了理解这一点,考虑以下情景:

 
1.一个开发者创建了node_list.module,它包含了一个名为node_list_all()的函数。
2.Drupal的下一版本中,核心节点模块添加了一个函数node_list_all()―――命名空间冲突了!
 
    如果开发者遵守模块的命名习惯,不使用下划线,那么前面的冲突就可以避免:nodelist_all()将永远不会与核心代码冲突。
 
    对这一点的最简单的理解方式就是,把第一个下划线左边的任何东西看作是模块的名字,从而把“模块名+下划线”看作是一个命名空间。例如,在node_命名空间下的所有东西都属于核心中的节点模块。如果你编写的函数以node_ , user_ , filter_,或者其它核心命名空间起头,那么你这是自寻烦恼。如果你贡献的第3方模块中存在命名空间冲突,那么就意味着你需要为其花费更多的时间,而基于你的模块编写代码的人也需要花费更多的功夫。
 
    译者注:这一点确实很有道理,但是在许多的第3方模块中,许多模块的名字使用了下划线,比如advanced_forum,backup_migrate,og_forum等等。不过在用的时候一定要小心。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 模块构建器模块

http://drupal.org/project/module_builder中,有一个很好的模块,它能帮你方便构建出模块的骨架。它向你询问你想要创建的钩子,并帮你创建它们,而且还带有示例代码。接着,你就可以在它的基础上开始工作了!

 

Drupal版本:

Drupal专业开发指南 第21章 测试和开发代码

软件测试就是将一个程序隔离成不同的部分,来判定它们的行为和预期的是否一致。在Drupal的接下来的版本中,测试将会是一个主要的目标。事实上,在Drupal 7中,测试将成为核心的组成部分。测试的好处包含以下几点:

 
•如果代码的变更(例如,代码重构)破坏了软件,那么这将被即刻发现。
•将检查代码错误的流程自动化。自动测试位于http://testing.drupal.org,它用来为核心检查引入的补丁。
•确保新代码正常工作。
 
    关于测试的更多信息,参看http://drupal.org/simpletest。你还可以参与测试小组,它位于http://groups.drupal.org/unit-testing
 

Drupal版本:

Drupal专业开发指南 第21章 签出你的模块

现在,你的模块已被提交到资源库中了,这和其它的第3方模块一样,你可以将它从CVS签出,并放在你的Drupal本地开发拷贝中(你可能首先需要创建modulescontrib目录):

 
cd /path/to/drupal
cd /sites/all/modules/contrib
cvs checkout -d foo contributions/modules/foo
 
    如果你收到了一个如下所示的错误,那么你应该没有设置你的CVSROOT环境变量(参看“签出贡献资源库”)。
 
cvs checkout: No CVSROOT specified! Please use the `-d' option
cvs [checkout aborted]: or set the CVSROOT environment variable.
 
提示 如果你维护的Drupal站点使用了从CVS签出的模块,那么你可以研究一下CVS部署模块,位于http://drupal.org/project/cvs_deploy。它将模块的CVS信息和Drupal内置的更新状态模块整合在了一起,后者用来报告哪些模块需要更新。
 

Drupal版本:

Drupal专业开发指南 第21章 签出贡献资源库

我前面提到了,drupal.org有两个资源库,一个用于核心代码,一个用于贡献的代码包括模块和主题。对于前者,只有很少的人能够访问;而对于后者,许多开发者都可以访问。你可以以匿名或者登录用户的身份,来签出贡献资源库。如果你是为了一个站点从贡献资源库签出代码的话(例如,你只想使用CVS获取一个模块的拷贝,这样你就可以运行它了),那么最好使用匿名用户的身份进行签出。否则,当下一个人来维护你创建的Drupal站点时,他想从CVS上更新模块代码,而系统则提示需要输入你设置的密码,那么此时他会晕死的!

    你可以签出整个资源库:
 
cvs -z6 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal-contrib
checkout contributions
 
    然而,并不鼓励你这样做,因为下载的太多了,会加重服务器的负担。最好只下载自己需要的。假定你创建了一个模块,并想将它贡献给Drupal社区。这意味着你只需要资源库中的modules子目录。如果你想把代码提交到资源库,那么你需要登录进来(你需要CVS帐号和密码;参看“获得一个Drupal CVS帐号”)。假定你的CVS用户名为sproinx,下面给出了如何登录进来:
 
cvs -d:pserver:sproinx@cvs.drupal.org:/cvs/drupal-contrib login
 
    系统会提示你输入密码,就是你申请CVS帐号时提供的。这个密码与你的drupal.org密码可以不一样。
 
提示 登录到drupal.org,点击“我的帐户”,点击编辑链接,并点击CVS标签,这样你就可以修改你的CVS帐号的密码了。
 
    接下来,你可以签出贡献资源库(就是drupal-contrib资源库)的modules子目录。你可以签出modules子目录所包含的所有模块,不过很少这样做,除非你想在一个长途飞行期间细读所有模块的拷贝:
 
cvs -z6 -d:pserver:sproinx@cvs.drupal.org:/cvs/drupal-contrib checkout
contributions/modules
 
    这将在你的本地计算机上创建一个modules子目录的拷贝。它应该看起来这样的:
contributions/
    CVS/
    modules/
        a_sync/
        aapi/
        about_this_node/
        abuse/
        ...
 
    或者,你可以只签出modules子目录,而不带有它所包含的模块,大多数的开发者都这样做:
 
cvs -d:pserver:sproinx@cvs.drupal.org:/cvs/drupal-contrib checkout
-l contributions/modules
 
注意 在编写本书时,modules子目录已经包含了300MB的数据。这就是为什么在签出该子目录时,在CVS命令中使用-z6标记(-z6在将数据通过网络传输前,会先对其进行压缩),或者使用-l标记,省略对所有模块的签出。
 
    注意在前面的CVS命令中,参数-d:pserver:sproinx@cvs.drupal.org:/cvs/drupal-contrib是重复的。由于每次都输入一遍这个参数,这样会很不方便,所以聪明的开发者可以把它放在CVSROOT环境变量中:
 
export CVSROOT=:pserver:sproinx@cvs.drupal.org:/cvs/drupal-contrib
 
    从现在起,CVS命令就会简短很多。设置了CVSROOT以后,前面的命令将会变成这样:
 
cvs login
cvs -z6 checkout contributions/modules
cvs checkout -l contributions/modules
 
    从现在起,我假定已经设置了CVSROOT环境变量。

Drupal版本:

Drupal专业开发指南 第21章 维护一个模块

在本节中,我们将详细的学习一下,如何在drupal.org上创建和维护一个模块。我们将覆盖大多数的常见任务。

 
获得一个Drupal CVS帐号
    Drupal有两个CVS资源库:一个Drupal核心资源库,只有很少的开发者能够对它提交代码;一个贡献资源库,用来保存所有贡献的模块、翻译、主题,这些资源你都可在drupal.org上找到,另外还保存了一些文档和方便开发者存储代码片断的沙箱文件夹。如果你有一个模块、主题、或者翻译,你想把它贡献出来,那么你可以申请一个CVS帐号,获取对Drupal CVS贡献资源库的写访问权,这样你就可以分享你的代码并向社区回馈你的贡献了。
    CVS帐号不是人人都可以拥有的。你需要向管理员说明你确实需要一个帐号。你需要提供获取帐号的具体动机。如果你打算贡献一个模块的话,你需要提供模块的一个拷贝以供审查,并证明它与已有的模块存在重大的不同。(你可以在drupal.org上花点时间,使用搜索表单搜索一下,确保你的模块是新的并且是不同的。在高级搜索表单上,选中工程复选框,这样你就只在贡献的资源中进行搜索了。)还有,确保你可以接受GNU GPL许可,因为贡献资源库中的所有代码都必须采用GPL许可。
    关于如何申请的更多详细,可参看http://drupal.org/node/59。提交代码和维护你自己贡献的模块的更多详细,可参看Drupal站点的http://drupal.org/handbook/cvs/quickstart;另外,我们接下来将会详细的介绍大多数的常见任务。
    另外,还有许多其它的方式来为Drupal社区贡献力量,比如编写文档和参与论坛讨论;具体可参看http://drupal.org/node/22286

Drupal版本:

Drupal专业开发指南 第21章 编码标准

Drupal社区认为,它的基本代码必须拥有一个标准的外观,从而提高可读性,也使得初学者更容易学习。社区也鼓励第3方模块的开发者采用这些标准。实际上,让我老实的告诉你:如果你没有遵守编码标准,那么你的模块在Drupal社区就不会得到认真对待。我们首先学习一下具体的标准,接着介绍了一些用来检查代码的自动工具(甚至为你纠正代码!)

 

 

 

行缩进
    Drupal代码缩进使用两个空格,而不是tab键。对于大多说编辑器,你可以设置一个首选,从而自动的将tab键代替为两个空格,这样你就可以继续使用Tab键了——如果你习惯使用了tab键。
 
PHP开始和结束标签
    包含代码的文件,比如.module或.inc文件,会使用PHP代码开始标签,如下所示:
 
<?php
...
 
    在Drupal中不能使用开始标签的简写形式“<?”。
    结束标签“?>”不是必须的,在Drupal代码中也不使用它。事实上,如果使用了这个标签的话,可能会带来麻烦。对于结束标签,也有一个例外,那就是在模板文件中,为了退出PHP并且回到HTML中,此时会使用结束标签,例如,在themes/bluemarine/block.tpl.php中:
 
<?php
// $Id: block.tpl.php,v 1.3 2007/08/07 08:39:36 goba Exp $
?>
    <div class="block block-<?php print $block->module; ?>" id="block-<?php
        print $block->module; ?>-<?php print $block->delta; ?>">
        <h2 class="title"><?php print $block->subject; ?></h2>
        <div class="content"><?php print $block->content; ?></div>
    </div>

 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 解决CVS冲突

如果你修改了Drupal的核心代码,那么当你执行CVS更新时就可能出现冲突。运行完cvs update命令以后,对于那些带有冲突的文件将会使用一个“C”将其标出,由于这些冲突的存在(CVS插入的用来标记冲突的文本,不是有效的PHP),所以你的站点也将不再工作。CVS尝试着合并文件的新版本和旧版本,但是它没有成功,所以现在需要人工干预,来手工的检查冲突文件。发生冲突时,包含冲突的文件就像下面的这样:

 
<<<<<<< (filename)
your custom changes here
=======
the new changes from the repository
>>>>>>> (latest revision number in the repository)
 
    你需要删除你不想要的那些行,并删除冲突指示字符,来保持代码的整洁。
老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 追踪Drupal代码变更

想检查开发小组中是否有人修改了核心文件?对于核心文件上所做的任何变更,想为其生成一个报告?cvs diff命令,根据代码的不同之处(也就是更新和修改),为用户生成一个逐行的输出。

 
注意Unix命令行上,diff命令(不是cvs diff命令)比较两个文件并为你显示变更。通过键入diff file1 file2,你就可以使用它了。cvs diff命令,比较的是本地文件与资源库中的文件,而不是比较两个本地文件。
 
    下面是运行cvs diff命令所得到的示例输出,这里使用了cvs diff –up:
 
Index: includes/mail.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/mail.inc,v
retrieving revision 1.8.2.2
diff -u -p -r1.8.2.2 mail.inc
--- includes/mail.inc 2 Apr 2008 08:41:30 -0000 1.8.2.2
+++ includes/mail.inc 15 May 2008 23:56:40 -0000
@@ -272,8 +272,8 @@ function drupal_html_to_text($string, $a
$string = _filter_htmlcorrector(filter_xss($string, $allowed_tags));
// Apply inline styles.
- $string = preg_replace('!</?(em|i)>!i', '/', $string);
- $string = preg_replace('!</?(strong|b)>!i', '*', $string);
+ $string = preg_replace('!</?(em|i)((?> +)[^>]*)?>!i', '/', $string);
+ $string = preg_replace('!</?(strong|b)((?> +)[^>]*)?>!i', '*', $string);
 
// Replace inline <a> tags with the text of link and a footnote.
// 'See <a href="http://drupal.org">the Drupal site</a>' becomes
 
    以单个加号(+)开头的行是添加进来的,而以单个减号(-)开头的行是被删除了的。它看起来像是有人修改了drupal_html_to_text()函数中的正则表达式。
    Drupal使用了unified diffs ,这由-u选项指出。这里也是用–p选项;这将在变更总结的后面输出函数的名字。当阅读这个输出时,它能帮你快速判定代码所在的函数,因为并不是所有的Drupal开发者都能够记住该行代码位于哪个函数中的。下面的这一行,取自前面的cvs diff输出,显示了受影响的函数:
 
@@ -272,8 +272,8 @@ function drupal_html_to_text($string, $a

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 通过程序来检查你的编码风格

有两种主要的方式,可用来检查你的编码风格是否符合Drupal的编码标准:一种方式是使用一个Perl脚本,另一种方式是使用一个第3方模块。

 
使用code-style.pl
    在你的Drupal根目录下的scripts目录中,你可以找到一个名为code-style.pl的Perl脚本,它可用来检查你的Drupal编码风格。下面讲述如何使用这个脚本。
    首先,修改文件的权限从而让它可执行;否则,你将得到一个“权限被拒绝”错误。使用chmod,通过命令行就可以实现这一点,如下所示:
 
$ cd scripts
$ ls -l | grep code-style
-rw-r--r-- 1 jvandyk jvandyk 4946 Feb 15 2007 code-style.pl
 
$ chmod u+x code-style.pl
$ ls -l | grep code-style
-rwxr--r-- 1 jvandyk jvandyk 4946 Feb 15 2007 code-style.pl
 
    Windows用户不用修改文件的权限,但是在你运行code-style.pl以前,一定要确保Perl已经安装了。关于Perl的更多详细,可参看http://www.perl.org
    现在,只要把将被评价的模块或者其它文件的位置传递给code-style.pl,你就可以执行这个脚本了。下面的例子说明了执行脚本的命令:
 
$ ./code-style.pl ../modules/node/node.module
 
    程序的输出通常采用下面的格式:
 
line number : 'error' -> 'correction' : content of line
 
    例如,下面的脚本告诉我们,我们需要在foo.module的30行的赋值操作符(=)周围添加空格,现在包含的代码为“$a=1;”:
 
foo.module30: '=' -> ' = ' : $a=1;
 
注意 一定要同时了解优点和不足。这一脚本做的很不错,但它还没有十全十美,所以你需要细心的评估每一个报告。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第21章 附加译文对照

Coding Standards: 编码标准

Line Indention:行缩进
PHP Opening and Closing Tags: PHP开始和结束标签
Control Structures:控制结构
Construct:结构体(这个词没有查证)
Coder Module :编码器模块
Repository: 资源库
Contribution: 贡献
Sandbox:沙箱, 沙盒
Check Out:签出(版本控制里的专有术语,好像都用“签出”,而不是“检出”)
Commit:提交
Project:工程(我有时候会把它翻译为“项目”,包括简体/繁体中文包中,都是使用的“项目”,我在以前的许多Drupal翻译中,也使用了“项目”;不过这里使用了“工程”,因为我记得VB的集成开发环境中,把project翻译为了“工程”,另外就是金山词霸也给出了“工程”。翻译成项目也没有错,很多人都习惯使用“项目”了)
Copy:复制,拷贝(作为动词时,我翻译为了复制,作为名词时,我翻译成了“拷贝”)。
Branch:分支
Release:发布
Tagging:标签化
Tag:标签
sticky tag:粘性标签
trunk:主干
tarball:沓包(汉语拼音拼出来的(tabao),沓有拥挤的意思,就是把文件压缩成一个拥挤的包)
timeline:时间线
Development snapshot:开发快照
Token:令牌
 

Drupal版本:

Drupal专业开发指南 第21章 高级分支化

在前面的例子中,我们假定在一个Drupal主版本下只存在模块的一个主版本,但是这也有例外的情况。例如,假定我们发布了foo模块的6.x-1.3版本。接着,灵感爆发了。我们想到了另外的一种实现方式,只需要一半的代码量,就可以实现同样的功能,而且跑得更快。不过,这需要修改API,而与foo模块相关的一切将全被打乱。解决的方案是使用新API发布一个2.0版本。由于模块仍然兼容于Drupal 6,所以我们使用DRUPAL-6--2-0作为标签名,而对应的发布号就是6.x-2.0。

    我们可以把代码提交到DRUPAL-6—1分支中,然后告诉他人6.x-1.3版是我们模块的1.x系列的最终发布了。但是如果安全小组在我们的模块中找到了一个安全漏洞,我们不得不发布6.x-1.4版,那该怎么办呢?所以,对于我们重写的模块,不能为它使用DRUPAL-6—1分支。
    那么解决方案呢?创建一个新分支,在那里我们发布模块的2.0版。我们把新分支命名为DRUPAL-6--2,在已有的DRUPAL-6--1分支上做出DRUPAL-6--2分支。首先,确定我们得到了DRUPAL-6--1分支下文件的最近版本。接着创建新分支:
 
cvs update -dP -r DRUPAL-6--1
cvs tag -b DRUPAL-6--2
 
    我们的开发历史,现在变成了图21-18所示的样子。
 
21-18.在已有的稳定分支上创建一个新分支
 
    另一种可选的方式是,如果我们还没有为Drupal 7做任何开发,那么对于模块的2.0发布的开发,我们可以为其使用HEAD,最后,我们再从HEAD上为2.0系列的发布创建一个分支(这和DRUPAL-6—1分支的创建方式一样)。这种方式如图21-19所示。
 
21-19.HEAD上为模块的第2个稳定分支创建一个分叉,模块的这个主版本也支持Drupal的同一个主版本(Drupal 6)
 
    这两种方式的具体选择,取决于实际情况。一般来讲,尽可能的不去创建新分支,直到必须创建的时候再为其创建。在图21-19中,创建DRUPAL-6--2分支的决定因素是开始为Drupal 7开发了模块了。如果没有DRUPAL-6--2分支的话,那么模块的2.0系列的开发将还会占据着HEAD,这样在CVS树中,就没有地方进行Drupal 7下的开发了。
 

Drupal版本:

Drupal专业开发指南 第22章 优化Drupal

Drupal的内核架构非常简洁并且非常灵活。然而,这种灵活性是有代价的。当启用的模块增加时,处理一个请求的复杂度也会增加。这意味着将耗费更多的服务器资源,必须实现一些策略,在一个站点日渐流行同时,来保证Drupal特有的简明。通过适当的配置,Drupal可以很容易的满足用户的需求。在本章中,我们将讨论性能(performance)和可升级性(scalability)。性能指的是你的站点响应一个请求所用的时间。可升级性指的是你的系统可以同时处理多少个并发请求,通常用“请求数/秒”来度量。

Drupal版本:

Drupal专业开发指南 第22章 Memcached(内存缓存)

当数据必须使用缓慢的设备比如一个硬盘进行交互时,系统通常会遇到一个性能问题。如果你能够为数据绕过这一操作,而且你能够承受得起数据的丢失(比如session数据),那会怎么样呢?此时我们可以使用memcached,这个系统将读写操作都放到内存中进行。与本章中介绍了其它解决方案相比,Memcached更加复杂,而且更难设定。,但是当你的系统需要在可升级性方面有所提高时还是值得考虑这一方案的。

Drupal有一个内置的数据库缓存,用来缓存页面,菜单,和其它Drupal数据,而MySQL数据库也能够缓存常用查询,但是当你的数据库不堪重负时那会怎样?你可以再买一台数据库服务器,或者你也可以直接将数据存放在内存中而不是存在数据库中,从而完全减轻数据库的重负。Memcached库(see http://www.danga.com/memcached/)和PECL Memcache PHP 扩展 (see http://pecl.php.net/package/memcache)都是专门为你实现这一点的工具。
Memcached系统将任意数据都保存在随机存取的内存中,而且能够迅速的从中读取数据。使用这种方式比任何使用磁盘的方式在性能上都要好一些。Memcached存储对象并使用唯一的键来引用对象。哪些对象应该被放到memcached中,这由程序员决定。对放到Memcached中的对象,Memcached不知对象的类型和本质;在它眼中,一切都是一堆等待取回的带有键的比特数据。
系统的简单性是它的优点。当为Drupal编写支持memcached的代码时,开发者可以决定对引起瓶颈的主要因素进行缓存。这可能是,频繁出现的数据库查询的查询结果,比如路径查找,或者是更复杂的构造比如完整加载的节点和分类词汇,这些都需要许多数据库查询和大量的PHP处理才能得到。
Drupal的memcache模块和使用PECL Memcache接口的Drupal专有API可在Drupal的Memcache项目中找到(参看 http://drupal.org/project/memcache)。
 

Drupal版本:

Drupal专业开发指南 第22章 PHP最优化措施

PHP最优化措施

Drupal中,由于PHP代码执行在处理一个请求中占了一大块,所以我们需要知道采取哪些措施才能加快这一进程,这一点非常重要。对编译后的PHP操作代码(opcodes)进行缓存,和剖析应用层来找出低效算法,能够带来重要的性能提升。
 
缓存操作代码
    有两种方式可以减少执行PHP代码所耗费的资源。很明显,一种是减少代码总量,可以通过禁用不必要的Drupal模块和编写高效的代码来达到这一点。另一种方式就是使用一个opcode缓存。PHP对于每个请求,都会将所有代码解析并编译成一种中间形态,在这种形态里包含了一系列的操作代码。添加一个opcode缓存可以让PHP能够重用前面编译过的代码,这样就会跳过解析和编译。常见的opcode缓存有Alternative PHP Cache (http://pecl.php.net/package/APC), eAccelerator (http://eaccelerator.net), XCache (http://trac.lighttpd.net/xcache/), 和 Zend Platform (http://zend.com)。Zend是一个商业产品,而其它几个则是免费的。
由于Drupal是一个需要大量数据库操作的程序,所以一个opcode缓存不能作为一个单独的解决方案,但它一个整体方案中的一部份。只需要最小的努力,他人仍然可以代码明显的性能提升。
 
22-1 Alternative PHP Cache (APC)带有一个接口,它能够展示内存分配情况和当前缓存中的文件。
 
剖析应用
通常定制的代码和模块对于小规模的站点能够很好的工作,如果将它放到大一点的站点上那么就可能成为站点的瓶颈。耗费CPU的代码循环,占用内存的算法,还有大量的数据库读取,这些都可以通过剖析你的代码来判定PHP在哪里花费了大量时间,因此,找到的关键点也就是你需要花费功夫进行调试的地方。更多关于PHP调试器和profiler的信息,参看第21章。
有时候,即便是添加了opcode并且进行了代码优化以后,你的web服务器仍然不能处理这么大的负载,那么现在你就应该换一个更强大的服务器,比如带有更多CPU或者更快的CPU,或者更换应用架构,采用多个web服务器。

Drupal版本:

Drupal专业开发指南 第22章 Web服务器用完了RAM

Web服务器进程处理一个请求时,用到RAM的地方包括,web服务器加载所有的模块(比如Apache的mime_module, rewrite_module,等等),还有PHP解释器使用的内存。启用的web服务器和Drupal模块越多,处理单个请求耗费的RAM就越多。

    因为RAM是个有限的资源,你应该决定对于每个请求分配多少内存,还有你的web服务器能够同时处理多少个请求。为了知道平均为每个请求使用多少内存,可使用一个像Unix中top一样的程序来查看你的进程列表。在Apache中,可以使用指令MaxClients来设置能够处理的最大并发请求数。一个常见的错误认为,解决满负荷的web服务器的方案是增加MaxClients的值。这只会让问题变得更加复杂,因为你将同时收到太多的请求。这意味着RAM将被耗尽,而你的服务器将开始进行磁盘交换并开始不能响应请求。让我们假定,你的web服务器拥有2GB的内存,而每个Apache请求大约使用20MB(对于Unix,你可以使用top来检查实际值;对于windows,则可以使用任务管理器)。通过下面的公式,你可以为MaxClients计算出一个合适的值;你要记住你需要为你的操作系统和其它进程预留一定的内存:
 
2GB RAM / 20MB per process = 100 MaxClients
 
如果禁用了不需要的web服务器模块,并且优化了定制的模块或者代码以后,你的服务器仍然耗尽内存,那么接下来你要做的是,确保数据库和操作系统不是产生瓶颈的原因。如果是由它们引起的,那么就添加更多的内存。如果不是,那么问题就很简单了,你收到的请求比你能够处理的请求要多;解决方案就是添加更多的web服务器。
 
提示 由于Apache进程用到的内存,随着它的子进程增多,有逐步增加的趋势,通过将MaxRequestsPerChild的值设得小一点比如300(实际的值依赖于你所处的环境)可以收回不少的内存。Apache将会更努力的工作来生成新的子进程,而新的子进程比替换掉的子进程所需的内存要少一些,这样你就可以使用较少的内存来处理更多的请求了。MaxRequestsPerChild的默认值为0,这意味着进程永不过期。(译者注:我不知道这里讲的什么意思,没学过)。
 

Drupal版本:

Drupal专业开发指南 第22章 优化表结构

优化表结构

另外,SQL的缓慢可能是由于,第3方模块中SQL表的不良实现引起的。例如,列没有索引的话就会引起查询缓慢。一个快速查看MySQL如何执行查询的方式是,从你的缓慢查询日志中取出一个查询,在其前面加上单词EXPLAIN,然后将这个查询发给MySQL。那么结果就是得到了一个显示使用了哪些索引的表。更多信息可参看MySQL的相关书籍。
 
手工缓存查询语句
    如果你必须要进行一些非常昂贵的查询,那么你可以在你的模块中手工的将结果缓存起来。关于Drupal缓存API的更多详细信息,参看第15章。
 
将表类型从MyISAM改为InnoDB
MySQL有两种常见的存储引擎,通常也称为表类型,它们是MyISAM 和InnoDB。Drupal默认使用的是MyISAM。
MyISAM使用表级别的锁机制,而InnoDB使用行级别的锁机制。锁对于数据库的完整性非常重要;它可以避免两个数据库进程同时对同一数据进行更新操作。在实际中,锁机制策略的不同意味着当你对MyISAM表进行写操作时,整个表将被锁住。因此,在一个繁忙的Drupal站点上,当正在添加多个评论时,在插入一个新评论的同时所有的评论都不能被读取。而在InnoDB上,这就不是一个问题,因为只有正被插入的那一行被锁住了,从而允许其它的服务器线程对其它各行继续进行操作。然而,在MyISAM中,表的读取更快,而数据维护和恢复工具更加完善。MySQL表的存储架构的更多信息,参看http://dev.mysql.com/tech-resources/articles/storage-engine/part_1.html或者http://dev.mysql.com/doc/refman/5.1/en/storage-engines.html
为了测试表级锁是不是引起性能缓慢的原因,你可以通过在MySQL中检查状态变量Table_locks_immediate和Table_locks_waited的值来分析锁的竞争程度。
 
mysql> SHOW STATUS LIKE 'Table%';
 
+-----------------------+---------+
| Variable_name         | Value     |
+-----------------------+---------+
| Table_locks_immediate | 1151552  |
| Table_locks_waited    | 15324     |
+-----------------------+---------+
 
Table_locks_immediate指的是能够立即获得表级锁的次数,而Table_locks_waited指的是不能立即获取表级锁而需要等待的次数。如果Table_locks_waited的值比较大的话,并且你遇到了性能问题,你可能希望将大表切分成小表;例如,你可以为一个定制模块创建一个专有的缓存(cache)表,或者通过其它方式来减小表的大小,或者降低表级锁命令调用的频率。对于一些表,比如cache_*,Watchdog, 和 accesslog表,减少表的大小的一种方式是减少数据的生命周期。使用Drupal的后台管理接口可以设置数据的生命周期。还有,确保每小时能够运行一次cron,从而能够不断的清理这些表中的过期数据。
因为Drupal可以运行在不同的应用下,所以不可能一刀切的具体到某个表就应该使用某个表引擎。然而,一般情况下,适合转变为InnoDB的表有cache, watchdog, sessions, 和 accesslog 表。幸运的是,转变到InnoDB上非常简单:
 
ALTER TABLE accesslog TYPE='InnoDB';
 
当然,这一转变应该在站点下线并且已经为你的数据做好备份时进行,而且你也应该首先了解InnoDB表的不同特性。
 
注意 尽管数据库API提供了db_lock_table() 和db_unlock_tables()函数供第3方模块使用,但是Drupal6中,核心代码并没有使用LOCK TABLES命令。
 
关于MySQL的性能调优,参看http://www.day32.com/MySQL/的性能调优脚本,这里提供了调整MySQL服务器变量的建议。

Drupal版本:

Drupal专业开发指南 第22章 其它web服务器最优化

为了让你的web服务器更有效的运行,这里有一些其它的措施。

 
Apache最优化
Apache是Drupal最常用的web服务器,通过对它进行调整可以获得更高的性能。下面的部分将给出一些可以一试的方式,供大家参考。
 
mod_expires
这个Apache模块将让Drupal发出ExpiresHTTP头部,在用户的浏览器中对静态文件缓存两周,或者直到一个文件存在新的版本为止。这适用于所有的图片,CSS和JavaScript文件,和其它静态文件。最终的结果是减少了带宽,而服务器需要发送的信息将会更少一些。Drupal为了使用mod_expires,对其进行了预先配置,一旦mod_expires可用,Drupal就会使用它。mod_expires的设置可以在Drupal的.htaccess文件中找到。
 
# Requires mod_expires to be enabled.
<IfModule mod_expires.c>
# Enable expirations.
ExpiresActive On
# Cache all files for 2 weeks after access (A).
ExpiresDefault A1209600
# Do not cache dynamically generated pages.
ExpiresByType text/html A1
</IfModule>
 
我们不能让mod_expires缓存HTML内容,这是由于Drupal生成的HTML内容不全静态的。这也是Drupal拥有自己的内部缓存系统的原因,内置缓存系统可对它的HTML输出进行缓存(比如,页面缓存)。
 
.htaccess文件中的指令迁移到httpd.conf
    Drupal带有两个.htaccess文件:一个位于Drupal根路径,而另一个将被自动创建,在你创建了用于存储上传文件的目录以后,访问Administer ➤ File system来设置目录的位置,此时系统将为你自动创建一个.htaccess文件。在处理每个请求时,所有的.htaccess文件都将被搜索,读取,和解析。相反,httpd.conf只在Apache启动时才被读取。Apache指令可以放在这两种文件中。如果你能够控制你自己的服务器,你应该将.htaccess文件中的内容转移到Apache主配置文件(httpd.conf)中,并通过将AllowOverride设置为None来禁用在你的web服务器根路径下查找.htaccess文件:
<Directory />
AllowOverride None
...
</Directory>
 
对于每个请求,这将阻止Apache通过遍历目录树来查找要执行的.htaccess文件。这样对于每个请求,Apache要做的工作就会有所减少,这样它就能够处理更多请求了。
 
其它的Web服务器
    还有另外一种选项,那就是使用其它服务器来代替Apache。Benchmarks已经说明了这一点,例如,一般情况下,LightTPD web服务器每秒能够为Drupal处理更多的请求。更多详细比较,参看http://buytaert.net/drupal-webserver-configurations-compared
 

Drupal版本:

Drupal专业开发指南 第22章 多个数据库服务器

多个数据库服务器带来了额外的复杂性,因为数据将被重复插入和更新,或者数据库被分割到多个服务器中。

 
数据库复制
    MySQL中的数据库复制,一个单独的主数据库接收所有的写操作,然后这些写操作将被复制到一个或多个从数据库上。读操作可以在任何主数据库或者从数据库上进行。在一个多层架构中,从数据库也可以是主数据库。
当前,在一个能够复制的数据库环境下运行Drupal的一个难点是,Drupal不能区分读和写操作。然而,由于所有的数据库查询都要通过数据库层,通过扫描查询中的关键字ALTER, CREATE, DELETE, FLUSH, INSERT, LOCK, UPDATE,等等,也不难做到这一点,从而将查询分发给合适的数据库。通过在http://drupal.org搜索“replication”可以找到这种方式的一些例子,另外还有一篇博客也值得一读http://buytaert.net/database-replication-lag
 
数据库分割
    由于Drupal可以处理多个数据库链接,为了增加你的数据库架构的可升级性,另一种策略是将一些表放在一个机子上的一个数据库中,将另一些表放到另一个机子上的不同数据库中。例如,将所有的cache表分离到一个独立的机子上的独立的数据库中,对于所有使用这些表的查询,使用Drupal的表前缀机制来对查询应用别名,从而提高你站点的可升级性。
 

Drupal版本:

Drupal专业开发指南 第22章 总结

在本章,你学到以下几点:

    • 如何发现性能瓶颈
    • 如何优化一个web服务器
    • 如何优化一个数据库
    • 特定于Drupal的最优化
    • 可能的多服务器架构

Drupal版本:

Drupal专业开发指南 第22章 找出瓶颈

如果你的网站运行性能达不到预期,第一步要做的就是分析问题所在。可能的原因包括web服务器,操作系统,数据库,和网络。

 
初步追踪
了解如何估算一个系统的性能和可升级性,即便是在出现大的问题时,它也能使你保持自信,从而快速的隔离并找到系统的瓶颈。你可以借助于一些简单的工具,通过询问一些问题,来发现瓶颈。这里给出了一种方式,用来分析一个存在严重性能问题的服务器。首先,我们需要知道性能是有哪些因素决定的,CPU,RAM,I/O,或者带宽都是影响性能的因素。那么你需要询问以下问题:
 
CPU占用率达到最大了吗?检查CPU的使用情况,在Unix上你可以使用top,在Windows上可以使用任务管理器,如果CPU占用率达到了100%,那么你的任务就是找出是什么程序占用了CPU。查看进程列表,可以让你了解到,是不是web服务器或者数据库占用了处理器的大部分计算资源。这两者都是可以解决的。
 
服务器的RAM用完了没有?Unix上你可以使用top,在Windows上可以使用任务管理器,,来方便的检查这一点。如果服务器还有大量的空闲内存,那么继续下一个问题。如果服务器用完了内存,你必须找出原因。
 
磁盘空间不足了吗?检查磁盘子系统,在Unix使用工具vmstat,在Windows下使用性能监测器,如果可用磁盘不能满足需求,同时又有大量空闲内存剩余,那么这就是一个I/O问题。可能的原因包括,记录了大量的冗余日志,对数据库不恰当的配置使得在磁盘上创建了许多临时表,后台脚本的执行,对于一个需要大量写操作的系统使用了一个不恰当的RAID级别,等等。
 
网络带宽用完了吗?如果网络带宽用完了,那么只有两种解决方案。一个是增加带宽。另一个是对发送的信息进行恰当的压缩,从而发送更少的信息。
 
Web服务器用完了CPU
如果你的CPU占用率达到了100%,而根据进程列表显示,是web服务器消耗了大量的资源而不是数据库(后面介绍),那么你应该去减少web服务器处理一个请求所耗费的资源。通常PHP代码的执行就是罪魁祸首。
 

Drupal版本:

Drupal专业开发指南 第22章 数据库瓶颈

Drupal需要进行大量的数据库操作,特别是对于登录的用户和定制的模块。数据库常常会成为产生瓶颈的原因。这里有一些基本的策略用于优化Drupal中数据库的使用。

 
启用MySQL的查询语句缓存
MySQL是Drupal最常用的数据库。它具有在内存中缓存常用查询语句的能力,这样一个给定的查询语句再次被调用时,MySQL将立即从缓存中将其返回。然而,在大多数MySQL中,这一特性默认是被禁用的。为了启用它,向你的MySQL配置选项文件添加以下代码;该配置文件的名称为my.cnf,它用来声明变量和你的MySQL服务器的行为(参看http://dev.mysql.com/doc/refman/5.1/en/option-files.html)。在这里,我们将查询语句缓存设为64MB:
 
# The MySQL server
[mysqld]
query_cache_size=64M
 
当前查询语句缓存的大小,可以通过MySQL的SHOW VARIABLES命令来查看:
 
mysql>SHOW VARIABLES;
...
| query_cache_size               | 67108864
| query_cache_type               | ON
...
 
不断的试验查询语句缓存的大小通常是有用的。缓存太小就意味着缓存了的查询语句很快就会过期。缓存太大就意味着搜索一个缓存可能需要花费相对较长的时间;还有就是使用内存进行缓存比使用其它一些方式要好,就像有更多的web服务器处理,memcache或者操作系统的文件缓存一样。
 
提示Drupal中,访问“管理➤报告➤状态报告”,点击MySQL版本号,来快速的查看一些重要的MySQL变量值。你还可以在这一页面查看,查询语句缓存是否被启用了。
 
识别耗费资源的查询
    如果你想了解在生成一个给定页面时都发生了什么,那么devel.module就会非常有用。它拥有一个选项,用来显示生成页面所用到的所有查询语句,以及每个查询所用的时间。关于如何使用devel.module,以及通过EXPLAIN语法来识别和优化数据库查询的更详细的讨论,参看第21章。
找出执行时间过长的查询的另一种方式是,在MySQL中启用缓慢查询日志。为了启用它,需要在MySQL的配置选项文件(my.cnf)中这样设置:
 
# The MySQL server
[mysqld]
log-slow-queries
 
这会将超过10秒的查询记录到MySQL数据目录中的日志文件example.com-slow.log中去。你可以修改秒数以及日志的位置,如下面的代码所示,这里我们将缓慢查询的最小值设为5秒:
 
# The MySQL server
[mysqld]
long_query_time = 5
log-slow-queries = /var/log/mysql/example-slow.log
 

Drupal版本:

Drupal专业开发指南 第22章 文件上传和同步

文件上传和同步

Drupal运行在单个web服务器上时,上传的文件一般存储在Drupal的files(文件)目录中。它的位置可在“管理➤站点配置➤文件系统”里配置。对于多个web服务器,则需要避免下面的场景:
 
1. 一个用户在web服务器A上,上传了一个文件;数据库被更新以反映这一情况。
2. 一个用户在web服务器B上查看一个页面时,引用了这个新文件。文件未找到!
 
很明显,答案就是将文件也同步到web服务器B上。有多种方式。
 
使用rsync
Rsync程序是一个解决方案,它通过仅仅复制修改了的文件来保持两个目录间的同步。更多信息,参看http://samba.anu.edu.au/rsync/。这种方式的不足之处是,同步会带来延迟,还有所有上传了的文件都有多个副本(因此增加了存储成本)。
提示 如果你有很多文件并定期的调度rsyncs,通过检查表file和upload,当文件未被修改时,不同步;文件被修改时,同步。
 
使用一个共享的固定的文件系统
与其在多个服务器间同步文件系统,你不如部署一个共享的固定的文件系统,它将文件存放在文件服务器上的一个单独的位置中。接着,web服务器可以使用一个协议比如网络文件系统(NFS)来安放文件。这种方式的优点有,能够很容易的添加廉价的web服务器,而资源将被集中存放在一个耐用的文件服务器上,它上面可以带有冗余存储系统比如RAID 5。这个系统的主要缺点是,仅有一个失败点;如果你的服务器或者文件系统出了问题,那么就会影响整个站点,除非你创建一个文件服务器集群。
如果需要提供大量的多媒体文件,最好将这些文件存放在一个单独的服务器上,可以使用一个轻量级的服务器比如Lighttpd,从而减轻你的web服务器的负担,使得Drupal能够处理更多的请求。做到这一点的一个简单的方式是,在你的web服务器上使用一个重写(rewrite)规则,将到来的针对特定文件类型的所有请求重定向到静态服务器上。下面是Apache的重写(rewrite)规则的一个例子,它重写了所有针对JPEG文件的请求:
RewriteCond %{REQUEST_URI} ^/(.*\.jpg)$ [NC]
RewriteRule .* http://static.example.com/%1 [R]
这种方式的不足是,web服务器仍然需要处理额外的工作——将请求重定向到文件服务器上。一个改进的方案是,在Drupal内部重写所有文件的URLs,这样web服务器就不再参与静态文件请求了。然而,现在,在Drupal内核里还没有一个简单的方式来进行修改。
 
超越单个文件系统
如果存储总量不断增长,以至于超过了单个文件系统能够承受的范围,你可以通过编写定制的代码来实现一个存储抽象层,从而满足需求。另一种选择是使用一个外部的存储系统比如Amazon的S3服务。
 

Drupal版本:

Drupal专业开发指南 第22章 架构

Drupal可用的架构就是那些其它的LAM堆栈软件,以及使得Drupal具有可升级性的技术。因此,我们将为你讲述不同的架构,并主要针对Drupal来讨论相关的技巧。

 
单个服务器
    这是最简单的架构。Web服务器和数据库运行在同一个服务器上。服务器可能是一个共享主机或者一个专用主机。尽管大多数Drupal站点能够在共享主机上很好的运转,如果期望具有一定的升级性的话,就应该把它放到专有主机上。
    在单主机架构下,配置非常简单,因为所有的东西都已设置好了。同样的,web服务器和数据库之间的通信非常快,这是由于不需要使用网络来传播数据,所以避免了由此带来的时间延迟。很明显,如果能够使用多核处理器那就更好了,这样web服务器和数据库之间就不需要争抢处理器了。
 
独立的数据库服务器
如果数据库是你的瓶颈,那么你可能就需要一个单独的强大的数据库服务器了。由于需要使用网络来发送请求,所以会影响性能,但是可升级性将会提高。
 
注意 无论什么时候你在使用多个服务器,你都需要确保使用一个快速的本地网络将它们连接起来。
 

Drupal版本:

Drupal专业开发指南 第22章 特定于Drupal的最优化

大多数针对Drupal的最优化措施,都在软件堆栈的其它层次中进行,也有一些专门针对Drupal本身的最优化措施,这也能使性能得到极大提升。

 
页面缓存
有时,一些简单的事情会被忽略掉,这也是为什么需要再次提到它们的原因。Drupal拥有各种内置的方式,它能够通过为匿名用户存储和发送压缩了的缓存页面,来减少数据库的负重。通过启用这一缓存,你可以使用一个单独的数据库查询来高效的读取页面,而不是使用许多查询来获取页面(在没有缓存可用时就使用这种方式)。Drupal的缓存默认是禁用的,它可以在“管理➤站点配置➤性能”中配置。更多信息参看第15章。
 
带宽最优化
这是“管理➤站点配置➤性能”页面中的另一个性能优化措施,它能够减少发送给服务器的请求次数。通过启用 “优化CSS文件”特性,Drupal将处理由modules创建的CSS文件,压缩它们,并将它们合并成一个文件,放到你的“文件系统路径”下的css目录中。而 “优化JavaScript文件”特性可以将多个JavaScript文件合并成一个,放到你的“文件系统路径”下的js目录中。这将减少每个页面的HTTP请求数量,以及下载页面的整体大小。
当将一个页面存储在页面缓存中时,Drupal将会检查是否启用了页面压缩。这个特性默认是启用的,你可以在“管理➤站点配置➤性能”将其禁用。如果启用了的话,在页面存储到缓存中以前,Drupal将会检查PHP的zlib扩展(如果存在的话),并使用gzencode($data, 9, FORCE_GZIP)对页面进行压缩。当页面从数据库中取出时,Drupal判定当前浏览器是否支持gzip编码,如果支持的话,它就简单的将缓存数据返回。否则,缓存数据在返回以前,将会使用gzinflate()进行解压处理。详情请参看includes/bootstrap.inc中的drupal_page_cache_header()。
 
调优Sessions表
Drupal将用户会话保存到了它的数据库中,而不是文件中(参看第16章)。这意味着Drupal能够很容易的应用到多个服务器上,但是由于要管理每个用户的会话信息这也增加了数据库的负担。如果一个站点每天有成千上万的用户访问,那么很容得就会看到这个表将会极速膨胀。
你可以通过PHP来控制多长时间清除一次旧的会话记录。Drupal将这一配置放到了它的settings.php文件中:
 
ini_set('session.gc_maxlifetime', 200000); // 55 hours (in seconds)
 
垃圾收集系统运行周期,默认设置为两天多点时间。这意味着如果用户两天内没有登录,那么它的会话将被删除。如果你的sessions表不断疯长,那么你需要提高PHP的会话垃圾收集系统的运行频率。
 
ini_set('session.gc_maxlifetime', 86400); // 24 hours (in seconds)
ini_set('session.cache_expire', 1440); // 24 hours (in minutes)
 
当调整session.gc_maxlifetime时,最好也将session.cache_expire设为相同的值,session.cache_expire用来控制缓存中会话页面的存活周期。注意session.cache_expire值的单位为分钟。
 

Drupal版本:

Drupal专业开发指南 第22章 独立的数据库服务器和web服务器集群

独立的数据库服务器和web服务器集群

多个web服务器具有更好的容错性,并能处理更多的访问。集群所需的最小计算机数量是2个web服务器。还有,你需要有一种方式能够用来在服务器之间切换流量。而当其中的一个机器不能工作时,集群中其余的机器能够处理整个负载。
 
负载均衡
    负载均衡器能够将web请求分配到多个web服务器上。还有一些其它类型的负载均衡器用来分发其它的资源,比如硬盘和数据库,但在这里,我们仅仅讨论对HTTP请求的分发。在多个web服务器的情况下,当一个web服务器当机或者处于维护状态时,负载均衡器允许web服务继续运行。
    负载均衡器可分为两大类。软件负载均衡器非常便宜甚至免费,但是它的维护和管理费用高于硬件负载均衡器。Linux Virtual Server (http://www.linuxvirtualserver.org/)是一个非常流行的Linux负载均衡器。硬件负载均衡器一般很昂贵,因为与基于软件的解决方案相比,它们包含了更高级的服务器切换算法并具有更好的可靠性。
除了负载均衡,多个服务器还带来了一些其它问题,主要包括文件上传和保持代码在服务器间的同步。

Drupal版本:

Drupal专业开发指南 第22章 管理已验证用户的访问

管理已验证用户的访问

由于Drupal可以为匿名用户提供缓存了的页面,而匿名用户一般也不需要与Drupal进行交互,你可能想要减少用户登录停留的时间,或者更疯狂一点,一旦用户关闭他们的浏览器就使他们退出。通过调整settings.php文件中的cookie生存周期来做到这一点。在下面这行代码中,我们将它的值改为24小时:
 
ini_set('session.cookie_lifetime', 86400); // 24 hours (in seconds)
 
而在这里一旦用户关闭浏览我们就将他们登出:
 
ini_set('session.cookie_lifetime', 0); // When they close the browser.
 
settings.php中的默认值(2,000,000秒)能够允许用户保持登录大约3周的时间(在此期间会话垃圾收集系统不会将他们的会话记录从sessions表中删除)。
 
清除错误报告日志
Drupal为模块开发者提供了watchdog()函数,使用它可以将信息写入到日志中。Drupal内置了两种方式,一种是记录到数据库中,另一种是记录到syslog中。
 
严重性级别
调用watchdog()时,PHP代码所使用的严重性级别符合RFC 3164,如表22-1所示。
 
22-1. Drupal看门狗系统的常量和严重性级别
Drupal 常量     整数     严重性级别
WATCHDOG_EMERG     0    紧急: 系统不可用
WATCHDOG_ALERT     1    警报: 需立即采取行动
WATCHDOG_CRITICAL  2    关键: 关键条件
WATCHDOG_ERROR     3    错误:错误条件
WATCHDOG_WARNING   4    警告: 警告条件
WATCHDOG_NOTICE    5    通知: 一般的,但是重要的消息
WATCHDOG_INFO      6    信息: 一般消息
WATCHDOG_DEBUG     7    调试: 调试级别消息
 
记录到数据库中
Drupal内置的数据库日志模块默认是启用的,可以在“管理➤报告➤最近的日志条目”查看日志条目。数据库中的watchdog表,用来存储日志信息,如果不对其进行定期地清理的话,那么它就会快速的膨胀。如果你发现watchdog表的大小导致了你的站点运行缓慢,你可以通过在“管理➤站点配置➤日志与报警 ➤数据库日志”里来调整相关配置以减小它的大小。注意,对该设置的修改将在cron下次运行时生效。不能定期的运行cron会使得watchdog表越来越大,从而为系统增加极大的负担。
 
记录到Syslog
Drupal核心自带的syslog模块,默认是禁用的,它使用PHP的syslog()函数将watchdog()的调用写入到操作系统中。这一方式就绕开了数据库日志模块所需要的数据库插入操作。
 

Drupal版本:

Drupal专业开发指南 第22章 识别耗费资源的页面

识别耗费资源的页面

    为了找出哪些页面是最耗费资源的,需要启用Drupal自带的统计模块。尽管统计模块增加了你的服务器的负担(由于它将你的站点的访问统计记录到了数据库中),但它能够帮助我们找出哪些页面的访问量最大,对于这些页面我们要进行更多的优化。它也可以用来追踪一个时期内生成页面的总时间,你可以在“管理➤报告➤访问日志设置”中进行声明。这可以用于识别耗费系统资源不能控制的网络爬虫,通过访问“管理➤报告➤浏览者排行”并点击 “禁止”来禁止该网络爬虫的访问。你还需要小心一点----有时候会很容易的禁用掉一个好的网络爬虫,就是能够给你的站点带来访问量的爬虫。在禁止网络爬虫以前,你一定要先检查一下它的出处。
 
识别耗费资源的代码
    看一下下面耗费资源的代码:
 
// Very expensive, silly way to get node titles. First we get the node IDs
// of all published nodes.
$sql = "SELECT n.nid FROM {node} n WHERE n.status = 1";
 
// We wrap our node query in db_rewrite_sql() so that node access is respected.
$result = db_rewrite_sql(db_query($sql));
 
// Now we do a node_load() on each individual node.
while ($data = db_fetch_object($result)) {
    $node = node_load($data->nid);
    // And save the titles.
    $titles[$node->nid] = check_plain($node->title);
}
 
完全加载一个节点是一个耗费资源的操作:运行钩子函数,通过模块处理数据库查询来添加或者修改节点,使用内存来在node_load()内部中缓存节点。如果你不依赖于其它模块对节点的修改,直接对节点表进行你自己的查询,速度将会更快一些。当然这仅仅是一个例子,但是常常会遇到同样的情景,也就是,许多时候数据是通过多个查询来取回的,而实际上可以将多个查询合并成一个简单的查询,或者不需要加载整个节点,就可以完成所要的操作。
 
提示 Drupal拥有一个内部的缓存机制(使用了一个静态变量),当在一个请求中多次加载同一节点时就会使用这一机制。例如,如果调用了node_load(1)。节点1将被完全加载并被缓存。当在同一个web请求中再次调用node_load(1)时,Drupal将会返回前面使用相同节点ID加载节点的缓存结果。
 

Drupal版本:

Drupal专业开发指南 第22章 运行cron

运行cron

尽管设定cron是Drupal安装指令的第5步,但是它常被忽视,而这一忽视能够给站点带来不小的麻烦。如果在一Drupal站点上没有运行cron,那么数据库就会充满日志信息、过期的缓存数据、以及其它的统计数据,这些都是应该从系统中定期清除的。我们应该把它作为正常安装流程中的一部份,及早的配置cron,这是一个很好的实践经验。关于设定cron的更多信息,参看Drupal的INSTALL.txt文件中的步骤5。
 
提示 如果你处于一个非常特殊的环境下,在一个访问量很大的站点上cron却永远没有运行过或者它没有被充分的运行,你可以手工的进行一些属于cron管理的操作。你可以随时清空缓存表(TRUNCATE TABLE 'cache', TRUNCATE TABLE 'cache_filter', and TRUNCATE TABLE 'cache_page'),而它将会重新构造自己。还有,在情急之时,你可以清空watchdog和sessions表来重新控制一个失控的Drupal站点。删除watchdog记录意味着你将丢失所有的错误消息,它们可能指示站点的问题所在。如果你想保存这些数据,那么在清空表watchdog以前,先对它进行备份。清空sessions表会使当前已登录的用户退出系统。
 
自动节流
Drupal在核心中包含了一个名为throttle.module的模块。这一模块通过对当前在线用户数量进行采样来测量站点的负载,如果采样显示超过了管理员设置的极值,那么它将关闭一些功能。在你配置一个站点时,最好启用这一模块,这样当一个页面成为热门话题并带来极大的访问量时,它使你能够应付这种局面。然而,节流阀模块不是一个万能药。为了进行节流,该模块本身会带来不小的负担。你也应该考虑一下其它的解决方案,比如使用memcached。
 
启用节流阀模块
当你启用了节流阀模块,你将会注意到在模块管理页面多了一列复选框。也就是,除了选择是否启用一个模块以外,你还可以选择它能否被节流。被节流意味着当module_list()返回了一列启用的模块时,由于访问量过大,启用了节流阀的模块将不被包含在内;被节流的模块此时将被禁用。
很明显,你将需要小心的选择你想对哪些模块进行节流。一般选择功能不重要但是耗费CPU时间或者许多数据库查询的模块。核心模块不能被节流(因为它们是Drupal正常运行所必需的),但是它们能在站点处于节流时,够意识到节流并使用它们自己的措施用来降低处理时间。例如,区块模块不能被节流,但是独立的区块可以被节流,如图22-2所示。
 
22-2 当站点处于大的负载时,它将不展示头部的搜索表单和右边栏中“Who’s new” 和 “Who’s online”区块,但是左边栏的导航和用户登录区块以及页脚的“Powered by Drupal”区块总是被显示的。
 
配置节流阀模块
为了使节流机制能够起作用,你必须为其提供一个阀值和一个采样频率。当启用了节流阀模块时,阀值可以在“管理➤站点配置➤节流阀”中设置。
 
设置阀值
    可以输入两个阀值:节流开始所需要的最小匿名用户数和节流开始所需要的最小登录用户数。由于匿名用户占用的资源比验证用户小,所以匿名用户的阀值应该更高一些。实际值则依赖于你的个人站点。用户数必须是在一个给定的时间内测量的。这个时间周期在“Who’s online”区块中设置,并作为Drupal变量user_block_seconds_online存储起来。如果它没有被设置,那它默认为900秒(15分钟),如图22-3所示。
 
22-3.用户最后一次访问以后仍被作为“在线”的时间周期,可以在“Who’s online”区块设置的User activity字段中进行设置。
 
设置采样频率
为了判定站点的负载量,以决定是打开还是关闭节流机制,节流阀模块必须查询数据库。这为数据库服务器增加了额外的负担。使用“Auto-throttle probability limiter”来设置检查的频率(实际中有可能检查发生在一个给定请求上)。例如,选择20%,那么对于每5个请求就会采样一次。
 
使得模块和主题懂得节流(Throttle-Aware)
节流机制可能开着,也可能关着。当你编写自己的模块和主题时,你可以对节流阀的状态进行判断,例如:
 
// Get throttle status.
// We use module_invoke() instead of calling throttle_status() directly
// so this will still work when throttle.module is disabled.
$throttle = module_invoke('throttle', 'status');
 
if (!$throttle) {
// Throttle is off.
// Do nonessential CPU-intensive task here.
}
 
提示 如果你拥有大量的多媒体文件,这些文件不重要,但又是主题的一部分,需要提供给用户,当你的网站不堪重负时,你可以对这些文件进行节流来减少带宽的总量。
 

Drupal版本:

Drupal专业开发指南 第23章 安装轮廓(profile)

当你安装Drupal时,会有一些模块被启用,一些特定的配置被选择,但是这些默认的设置可能并不是你所需要的。Drupal安装器使用了一个默认的安装过程轮廓,用来决定所有的这些配置。通过创建你自己的安装轮廓,你可以定制Drupal的初始安装,从而使你的站点带有你想要的模块和设置。假定你在为某一高校工作,你想创建一个安装过程profile,从而能够启用一个与学校单点登录系统相绑定的定制模块,能够为站点管理员创建一个新角色,能够在安装完成时向你发送e-mail。Drupal的安装器系统,允许你通过创建一个安装轮廓来定制安装时的各种操作。在本章你将学到如何做到这一点。

Drupal版本:

Drupal专业开发指南 第23章 一个批处理操作回调

导入了用户以后,那么将会调用importusers_optimize()。最后,当该操作也完成后,那么将会调用我们在finished键中指定的回调(importusers_finished())。下面是importusers_import()函数:

 
/**
 * Batch callback operation: Import users.
 *
 * @param $size
 * Number of users to import in each operation.
 * @param $context
 * Batch context containing state information.
 */
function importusers_import($size, &$context) {
    // Initialize sandbox the first time through.
    if (!isset($context['sandbox']['progress'])) {
        $context['sandbox']['progress'] = 0;
        $context['sandbox']['current_user_id'] = 0;
        $context['sandbox']['max'] = db_result(
            db_query('SELECT COUNT(DISTINCT user_id) FROM {old_users}'));
    }
 
    // Retrieve some users from the old_users table.
    $result = db_query_range("SELECT user_id, username AS name, email AS mail,
        pass FROM {old_users} WHERE user_id > %d ORDER BY user_id",
        $context['sandbox']['current_user_id'], 0, $size);
 
    // Transform them into Drupal users.
    while ($account = db_fetch_array($result)) {
        $new_user = user_save(array(), $account);
 
        // Update progress information.
        $context['sandbox']['progress']++;
        $context['sandbox']['current_user_id'] = $account['user_id'];
        $context['message'] = t('Importing user %username', array('%username' =>
            $new_user->name));
 
        // Store usernames in case the the 'finished' callback wants them.
        $context['results'][] = $new_user->name;
    }
 
    // Let the batch engine know how close we are to completion.
    if ($context['sandbox']['progress'] == $context['sandbox']['max']) {
        // Done!
        $context['finished'] = 1;
    }
    else {
        $context['finished'] = $context['sandbox']['progress'] /
        $context['sandbox']['max'];
    }
}
 
/**
 * Batch callback operation: Optimize users.
 * For now, this function does nothing.
 *
 * @param $context
 * Batch context containing state information.
 */
function importusers_optimize(&$context) {
    // Code would go here.
    // Inform the batch engine that we are done.
    $context['finished'] = 1;
}
 
    注意,除了你在批处理集操作数组中指出的参数以外,importusers_import()还接收了另一个名为$context的参数。$context是一个数组,它是通过引用传递的,它包含了来自于批处理引擎的关于当前批处理集的状态信息。$context的内容如下所示:
 
• sandbox:这个区域是供回调函数使用的。你可以在这里存储你需要的任何东西,并且它将会自动持久化。在我们的例子中,我们存储的信息有,要导入的用户数,当前正被导入的用户,等等。在批处理进行处理期间,使用这个来存储信息,而不是使用$_SESSION。如果你使用了$_SESSION,那么当用户打开了一个新的浏览器窗口,就可能出错。
 
• results:这个给finished回调使用的包含结果的数组。例如,如果用户想看到导入的用户名的列表时,那么就可以使用这个。
 
• message:是用来显示在进度页面的消息。
 
• finished:这是一个浮点数字,位于从0到1之间,用来指示处理了多少数据。当所有的数据处理完时,将它设置为1,来指示批处理引擎可以继续下一个操作了。
 
    下面是所有的批处理操作运行完成以后,所要调用的回调:
 
/**
 * Called when all batch operations are complete.
 */
function importusers_finished($success, $results, $operations) {
    if ($success) {
        drupal_set_message(t('User importation complete.'));
    }
    else {
        // A fatal error occurred during batch processing.
        $error_operation = reset($operations);
        $operation = array_shift($error_operation);
        $arguments = array_shift($error_operation);
        $arguments_as_string = implode(', ', $arguments);
        watchdog('importusers', "Error when calling operation '%s'('%s')",
            array($operation, $arguments_as_string));
        drupal_set_message(t('An error occurred and has been recorded
            in the system log.'), 'error');
    }
}

Drupal版本:

Drupal专业开发指南 第23章 使用批处理API

使用批处理API

    有时,你需要运行一系列的任务,这可能需要很长时间----有可能会超过PHP的时间限制。幸运的是,Drupal提供了一个API用来专门处理这种情况。你只需要指定要做什么,接着将它委托给批处理的处理器。这一般在一个表单提交后执行,当然这不是必须的。我们将检查在安装器中是如何使用批处理API的,接着学习一下如何在表单提交中使用批处理API。
 
使用批处理API启用模块
    隐藏在批处理API背后的基本思想是,你定义一组操作,定义用来显示进度消息的一些信息,定义如何运行这些操作,接着将其委托给批处理引擎。引擎将负责执行这些操作,如果需要的话,还会刷新HTTP,更新进度指示器。接着,当所有的操作执行完毕后,它将调用你定义的最终函数。
    下面是安装器使用批处理API启用模块的简化版本:
$operations = array();
foreach ($modules as $module) {
    $operations[] = array(
        '_install_module_batch', // Name of callback.
        array($module, $files[$module]->info['name']), // Array of parameters.
    );
}
$batch = array(
    'operations' => $operations,
    'finished' => '_install_profile_batch_finished', // Call this when done.
    'title' => st('Installing @drupal', array(
        '@drupal' => drupal_install_profile_name())
    ),
    'error_message' => st('The installation has encountered an error.'),
);
batch_set($batch);
batch_process($url, $url);
 
    首先,它创建了一个操作数组。每个操作都包含了,要调用的PHP函数的名字,和传递过来的参数的数组。由于在处理进行时,安装器将在后面调用这个PHP函数,所以它又被称为回调
    接着,定义了一个批处理集。这里面有,包含操作的数组,处理完成后要调用的回调的名字,在处理期间使用的标题,还有当出现错误时使用的错误消息。使用batch_set()来检验批处理集,接着调用batch_process()开始处理。
 
提示在这种情况下,所有的操作都调用了同一个函数,这里仅仅使用了不同的参数。然而,操作可以是你想要调用的任意函数。
 
    面是来自于install.php的_install_module_batch()函数的代码,对于每个操作都会执行这段代码:
 
/**
 * Batch callback for batch installation of modules.
 */
function _install_module_batch($module, $module_name, &$context) {
    _drupal_install_module($module);
    // We enable the installed module right away, so that the module will be
    // loaded by drupal_bootstrap() in subsequent batch requests, and other
    // modules possibly depending on it can safely perform their installation
    // steps.
    module_enable(array($module));
    $context['results'][] = $module;
    $context['message'] = 'Installed '. $module_name .' module.';
}

Drupal版本:

Drupal专业开发指南 第23章 安装轮廓的工作原理

Drupal的安装器启动时,它扫描profiles目录以查看有多少个可用的轮廓。如果它发现有多个轮廓时,它将显示所有的轮廓以供用户选择。例如,在创建了我们的university.profile文件,并向其中添加了university_profile_details()函数以后,访问http://example.com/install.php,将会产生一个如图23-1所示的界面。(当然,安装轮廓现在实际上还不能工作---还有很多工作需要去做。)

 
提示 如果Drupal只找到了一个轮廓,那么它将自动选择该轮廓。因此,如果你想直接运行你自己的轮廓,而不需要显示图23-1所示的界面,那么你只需要删除profiles/default/default.profile就可以了。
 
 
23-1 Drupal显示了一个界面,里面包含了有哪些安装轮廓可以选择
 
Drupal的安装器接下来还会回到安装轮廓上的。返回以后,它将找出安装轮廓想要运行的定制任务(这样,它就可以将它们添加到页面左边栏的步骤列表中了)。它还会找出轮廓想要起用的模块,并将这些模块自动启用。在安装过程的最后,安装器再次将执行委托给安装轮廓,以运行自定义的任务。在后面的这一阶段,会对Drupal进行进一步的定制。流程的概况如图23-2所示。
 
23-2 安装器是如何与安装轮廓交互的

Drupal版本:

Drupal专业开发指南 第23章 定义一个批处理集

我们在前面提到,一组处理又被称为一个批处理集。批处理API可以处理多个批处理集,而无须混合它们。可以按照先后顺序来处理多个批处理集,同时为每个批处理集使用一个新的进度指示器。

    让我们钻研一个例子。这里我们没有把它放在安装轮廓中,而是把它写在了一个单独的模块中了。这样,当你测试、调试、使用它时,就不需要每次都清空数据库和重新安装Drupal了。记住,只要稍作修改,你就可以在安装轮廓中使用这种方式了,比如,在一个自定义的轮廓任务中,可以使用批处理来响应显示给用户的表单。
    对于我们的场景,让我们使用最常见的例子,客户想将一个网站从自定义的内容管理系统迁移到Drupal中来。在原有的数据库中已经有了一个存储用户的数据库表,在我们导出的SQL中,它的样子应该是这样:
 
CREATE TABLE old_users (
    user_id int(32) NOT NULL,
    username varchar(32) NOT NULL,
    email varchar(32) NOT NULL,
    pass varchar(32) NOT NULL
);
INSERT INTO old_users VALUES (3, 'mary', 'mary@example.com', 'foo');
INSERT INTO old_users VALUES (4, 'joe', 'joe@example.com', 'bar');
INSERT INTO old_users VALUES (6, 'fred', 'fred@example.com', 'zou');
INSERT INTO old_users VALUES (7, 'betty', 'betty@example.com', 'baz');
INSERT INTO old_users VALUES (8, 'friedrich', 'freidrich@example.com', 'fre');
INSERT INTO old_users VALUES (9, 'martin', 'martin@example.com', 'aoi');
INSERT INTO old_users VALUES (10, 'fozzie', 'fozzie@example.com', 'lii');
INSERT INTO old_users VALUES (11, 'steve', 'steve@example.com', 'doi');
 
    让我们设立一个批处理,当管理员点击如图23-5所示的表单时,它将把这些用户作为Drupal用户导入进来。
 
23-5.用来选择一次导入多少用户的表单
 
    下面是我们模块的.info文件,我们应该把它放在sites/all/modules/custom/importusers/importusers.info:
 
; $Id$
name = Import Users
description = Imports users from a database using the batch API.
package = Pro Drupal Development
core = 6.x
 
    我们将首先实现菜单钩子,创建表单定义,和为表单定义编写处理器。sites/all/modules/custom/importusers/importusers.module的初始代码如下:
 
<?php
// $Id$
 
/**
 * @file
 * Example of using the batch API.
 */
 
/**
 * Implementation of hook_menu().
 */
function importusers_menu() {
    $items['importusers'] = array(
        'title' => 'Import users',
        'page callback' => 'drupal_get_form',
        'page arguments' => array('importusers_form'),
        'access arguments' => array('administer users'),
    );
    return $items;
}
 
/**
 * Menu callback: define form to begin user importation.
 */
function importusers_form() {
    $form['size'] = array(
        '#type' => 'select',
        '#title' => t('Import how many users per pass?'),
        '#description' => t('Choose a value and click the Begin button.'),
        '#options' => drupal_map_assoc(array(1, 5, 10, 25, 50)),
    );
    $form['submit'] = array(
        '#type' => 'submit',
        '#value' => t('Begin'),
    );
    return $form;
}
 
/**
 * Handle form submission by beginning batch operation.
 */
function importusers_form_submit($form_id, &$form_state) {
    $size = $form_state['values']['size'];
    $batch = array(
        'operations' => array(
            array('importusers_import', array($size)),
            array('importusers_optimize', array()),
        ),
        'finished' => 'importusers_finished',
        'title' => t('Importing Users'),
        'init_message' => t('The user import process is beginning.'),
        'progress_message' => t('Imported @current of @total.'),
        'error_message' => t('The importation process encountered an error.'),
    );
    batch_set($batch);
    // batch_process() not needed here because this is a form submit handler;
    // the form API will detect the batch and call batch_process() automatically.
}
 
    对于菜单钩子和表单定义函数,我们应该已经很熟悉了(如果不熟悉的话,那么可以分别参看第4章和第10章)。这里值得注意的地方是importusers_form_submit()函数,我们在它里面定义了我们的批处理集。一个批处理集,在它的关联数组中可以使用以下键。这里只有operations键是必须的。
 
• operations: 这是一个数组的数组。每个数组包含两个成员:回调函数的名字,和执行操作时传递给回调函数的参数。
 
• finished: 当所有的操作都完成时调用的回调函数的名字。这个函数将接收在处理期间所发生的事件信息,这样可以通过drupal_set_message()对它进行分析、总结,或者在其它方面使用这些信息。
 
• title: 这是为用户显示进度信息的页面的标题。如果没有设置title的话,那么将使用t('Processing')。
 
• init_message:当批处理集的处理进行到初始化时,将会显示这个消息。如果没有设置init_message的话,那么将使用t('Initializing')。
 
• progress_message:在批处理集的处理期间,显示的消息。在进度消息中可以使用以下占位符:@current, @remaining, @total, 和@percent。随着批处理集的不断处理,这些值也会随之变化。如果没有设置progress_message的话,那么将使用t('Remaining @remaining of @total.')。
 
• error_message: 在处理期间当发生错误时,显示给用户的消息。如果没有设置error_message的话,那么将使用t('An error has occurred.')。
 
• file:在一个普通的Drupal请求期间,如果operationsfinished的回调函数不在当前范围内,那么必须给出包含这些函数的文件的路径。该路径是相对于Drupal安装的base_path()的,可以使用drupal_get_path()来方便的构建这个路径。如果函数已经位于了范围以内,那么就不需要定义file了。
 
    前面的批处理集是非常简单的,它只包含两个操作.首先,批处理引擎将重复的调用importusers_import($size),直到该函数指出已经导入了所有的用户。记住, $size参数是每次调用所要导入的用户数。$size在这里很重要,这是因为,在批处理API让客户初始化另一个HTTP请求以前,在每个请求周期内,这个变量决定着工作量的大小。例如,如果你有100个用户需要导入,将$size设置为1将会产生100个HTTP请求;而将$size设置为50,那么将会产生2个HTTP请求。在每个请求中,你想要执行的工作量,是由你的服务器的强大程度、服务器的繁忙程度、总工作量的大小共同决定的。

Drupal版本:

Drupal专业开发指南 第23章 定义附加的安装任务

注意图23-1中左边栏中的任务列表(“选择轮廓,”“选择语言,” “验证需求,”等等)。让我们通过在我们的安装轮廓中定义一些任务,来将它们也添加到这个列表中。我们将编写一个函数,它的名字为:我们的轮廓名字+_profile_task_list:

 
/**
 * Return a list of tasks that this profile supports.
 *
 * @return
 * A keyed array of tasks the profile will perform during
 * the final stage. The keys of the array will be used internally,
 * while the values will be displayed to the user in the installer
 * task list.
*/
function university_profile_task_list() {
    return array(
        'dept-info' => st('Departmental Info'),
        'support-message' => st('Support'),
    );
}
 
选择了我们的轮廓以后,我们将看到刚刚定义的任务,如图23-3所示。
23-3.轮廓定义的任务(部门信息和支持)显示在了左边栏
 
    安装器将执行一系列的任务,包括内置任务和你的安装轮廓可能定义的任务。表23-2给出了内置任务列表。自定义任务时,确保在你的任务数组中定义的键的唯一性,也就是与内置任务的任务标识不冲突。
 
23-2.任务的名字和描述,这里按照它们的执行顺序进行排列
任务标识符            描述
profile-select          选择轮廓*
locale-select           选择语言
requirements            验证系统需求
database                设立数据库
profile-install         为模块的安装和启用准备批处理
profile-install-batch  安装轮廓(安装和启用模块)
locale-initial-import  为用于导入的界面翻译准备批处理
locale-initial-batch    通过导入.po文件设立翻译
configure               配置站点(用户填写表单)
profile                 将控制权转交给安装轮廓的_profile_tasks()函数
profile-finished        为用于导入的剩余界面翻译准备批处理
locale-remaining-batch 设立剩余翻译
finished                告诉用户安装已完成
done                    重构表单,注册动作,和显示初始页面
*如果仅有默认轮廓可用,那么在用户界面将不会显示“选择轮廓”任务,而“安装轮廓”任务也被重命名为了“安装站点”。
 
    你定义的任务用来指示安装流程中的步骤;在这里定义任务的目的是,让Drupal将它们包含在用户界面中。如果你想让你的安装轮廓更加模块化,那么你完全可以在university_profile_task_list()中定义更多的任务,但是在定义任务时,一定要避免与已有任务的命名冲突。

Drupal版本:

Drupal专业开发指南 第23章 总结

在本章,你学到了以下几点:

• 什么是安装轮廓
• 安装轮廓的存储位置
• 如何设立一个基本的安装轮廓
• 如何制定安装哪些模块
• 如何指定安装期间应该运行的轮廓任务
• 在安装阶段,当轮廓任务运行时,如何操作Drupal
• 如何在安装器中使用批处理API
• 如何创建你自己的批处理集
 

Drupal版本:

Drupal专业开发指南 第23章 指示需要启用哪些模块

通过添加函数university_profile_modules(),我们告诉Drupal我们的安装轮廓想要启用哪些模块(还有,我们知道这个函数的名称是由我们的轮廓名称加上_profile_modules合成)。这个函数返回一个数组,里面包含了轮廓所要启用的模块名称。对于数组中模块名称出现的顺序,你要小心一点,因为模块之间可能是存在依赖关系的,以需要正确的处理这种可能存在的依赖关系。

 
/**
* Return an array of the modules to be enabled when this profile is installed.
*
* The following required core modules are always enabled:
* 'block', 'filter', 'node', 'system', 'user'.
*
* @return
* An array of modules to be enabled.
*/
function university_profile_modules() {
    return array(
        // Enable optional core modules.
        'dblog', 'color', 'help', 'taxonomy', 'throttle', 'search', 'statistics',
 
        // Enable single signon by enabling a contributed module.
        'pubcookie',
    );
}
 
在启用这些模块以前,安装器会询问每一个模块,以查看Drupal底层的系统是否提供了该模块所有的必要条件。通过为每个模块调用hook_requirements('install')来完成检查。如果要求没有完全被满足,安装器将会失败,并报告缺少了哪些条件。
 
注意 必要条件钩子是一个可选的钩子,它允许模块在进行安装以前,测试是否准备好了所需要的环境。例如,一个模块可能在PHP的最小版本上有限制。必要条件钩子必须放在模块的.install文件中。关于该钩子的更多信息,可参看http://api.drupal.org/api/function/hook_requirements/6
 
    安装器在启用模块以前,首先会确保模块是存在的。它会在多个地方查找,表23-1列出了这些地方。由于我们要启用pubcookie模块(不包含在Drupal核心中的模块),在运行我们的安装轮廓以前,我们需要确保能够在下面所列的目录中找得到它。
 
23-1. Drupal模块可存放的目录
------------------------------------------------------------------------------
目录                     存放的模块
modules                         Drupal 核心模块
sites/all/modules               3方模块(适用于所有站点)
profiles/profilename/modules    位于安装轮廓中的模块
sites/*/modules                 与你的settings.php文件位于同一目录的模块
 
安装器还会在放置你站点settings.php文件的目录下查找模块。如果settings.php位于sites/default,那么Drupal会在sites/default/modules下面找。类似的,如果settings.php位于sites/example.com,那么Drupal将在sites/example.com/modules下面寻找模块。

Drupal版本:

Drupal专业开发指南 第23章 渐进式和激进式(Nonprogressive)批处理集

渐进式批处理集就是一个普通的批处理集,它使用一个进度指示器向用户提供反馈。然而,当时想通过drupal_execute()来使用程序提交表单时,你应该不需要渐进式批处理集。所以,在这种情况下,表单API认出这是由程序提交的表单,并将批处理集设置为激进式。激进式批处理集将在单个请求中执行所有的操作。渐进式和激进式批处理集的设置,如图23-6所示。

 
 
23-6.渐进式和激进式批处理器的起始处理
 
批处理请求周期
    当操作正被执行时,批处理引擎负责刷新进度指示器页面来避免PHP的超时。图23-7给出了周期的图示,通过阅读includes/batch.inc源代码可以深入的研究一下这个周期。
 
23-7.批处理请求周期概览

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第23章 设置Drupal变量

设置Drupal变量

    通过简单的调用variable_set()就可以设置Drupal变量:
 
variable_set('pubcookie_login_dir', 'login');
 
创建初始节点类型
    如果你需要创建节点类型的话,那么可以使用Drupal内置的内容类型支持,创建一个节点类型定义对象,并调用node_type_save()就可以了。在前面的轮廓例子中,我们最后创建了两个节点类型:page,用于普通的web页面(在我们调用default_profile_tasks()时,由默认轮廓创建);news,用于新闻条目。接着,我们使用variable_set()设置了默认的节点选项,这样当发布新闻条目时,就会自动将其推到首页,然而普通的页面则不可以。
    如果你已经启用的模块提供了节点类型,那么通过这些模块中的node_info()钩子,这些节点类型在Drupal中已经可用。
 
将信息保存到数据库中
    安装轮廓可能想调整一些数据库设置。由于数据库连接已经可用,所以可以使用db_query()来修改数据库。在我们的轮廓例子中,我们向Drupal站点添加了一个角色。在你的轮廓中,你可能想添加更多的信息,例如,向permissions表中插入权限。
    为了获取正确的查询语句,这里有一个方便的方式,那就是执行一个普通的Drupal安装,然后对其进行配置,直到得到你想要的。这可能包括创建一些节点作为占位符,添加一些URL别名。对于这个大学部门,它可能需要一个“关于”页面,一个“课程教学”页面,等等。在完成了这些配置以后,你可以使用数据库工具从你站点的数据库中导出SQL脚本。然后,你从SQL脚本中选出你想使用的INSERT SQL命令,然后把这些插入命令包含在你的安装轮廓中。
 
通过程序提交表单
    因为Drupal支持程序式表单提交,所以,当你需要与网站进行交互时,你可以使用drupal_execute()提交表单。在前面的例子中,我们就使用了这种方式向站点添加了分类术语。关于drupal_execute()的更多详细,可参看第10章。
 
在安装期间设置一个主题
    Drupal把默认主题的值存储在了名为theme_default的持久化变量中。因此,通过设置这个变量,你就可以为站点选择初始主题了,在安装完成后就显示出来初始主题。在前面的轮廓例子中,我们选择了一个名为university的自定义主题。
 
// Change to our custom theme.
$themes = system_theme_data();
$theme = 'university';
if (isset($themes[$theme])) {
    system_initialize_theme_blocks($theme);
    db_query("UPDATE {system} SET status = 1 WHERE type = 'theme' AND
        name = '%s'", $theme);
    variable_set('theme_default', $theme);
    menu_rebuild();
    drupal_rebuild_theme_registry();
}
 
    但是还有一些常规事务需要完成。调用system_theme_data(),并检查是否定义了$themes['university'],来确保Drupal发现了我们的自定义主题。需要做的还有,设立新主题中的区块,启用主题本身,接着重构菜单和主题注册表。
    这里的方式是,根据你感兴趣的流程(在这种情况下就是启用和设置一个默认主题),为其查找对应的函数,接着要么调用该函数,要么将其代码复制到你的安装轮廓任务重。在前面的例子中,代码是从modules/system/system.admin.inc的system_themes_form_submit()函数中提取出来的。

Drupal版本:

Drupal专业开发指南 第23章 资源

编写安装轮廓需要更多的技巧。在我们的例子中,尽管我们没有在university_profile_modules()中包含评论模块,但是我们需要把评论模块包含在范围内,这样就可以使用它的一些常量来设置首选项了。我们在输入分类术语时,使用了程序的方式来提交表单,在这里我们需要定义一个$form_state['clicked_button']条目,尽管看起来它是没用的,但是该表单的提交处理器需要这个条目。在你的安装轮廓中,你也需要花点时间注意一下类似的细节。

    尽管这些事情可能会花费额外的时间,但是你可以通过使用一个安装轮廓生成器,来节省不少时间。更多信息可参看http://drupal.org/node/180078。如果你对改进安装轮廓的当前状态感兴趣的话(没有双关的意思),可加入分发轮廓小组http://groups.drupal.org/distributions

Drupal版本:

Drupal专业开发指南 第23章 轮廓的存放位置

你的Drupal站点已经包含了一个安装轮廓。它是Drupal自带的默认安装轮廓,位于profiles/default/default.profile。我们想创建一个新的名为“university”(大学)的轮廓,所以我们首先需要在profiles/university/university.profile创建一个新文件。现在,我们向这个文件中添加一个单独的函数:

 
<?php
// $Id$
/**
* Return a description of the profile for the initial installation screen.
*
* @return
* An array with keys 'name' and 'description' describing this profile,
* and optional 'language' to override the language selection for
* language-specific profiles, e.g., 'language' => 'fr'.
*/
function university_profile_details() {
    return array(
        'name' => 'Drupal (Customized for Iowa State University)',
        'description' => 'Select this profile to enable settings typical for a
            departmental website.',
    );
}
注意,这里文件的名称与轮廓目录的名称相同,而在文件名的后面则使用了.profile后缀,文件university.profile中的所有函数都以前缀university_开头。
 
由于安装轮廓的选择界面出现在本地化选择界面以前,所以无法翻译这里name和description键对应的字符串。然而,安装轮廓中的其它字符串,都需要使用函数st(),注意在这里不是使用通常的t()函数,这是因为安装器在运行这段代码时,Drupal还没有完成一个完整的引导指令,所以在这里不能使用t()函数。如果有人想为我们的安装轮廓创建一个法语翻译的话,那么翻译需要放在profiles/university/translations/fr.po中(参看第18章)。

Drupal版本:

Drupal专业开发指南 第23章 运行附加的安装任务

Think in Drupal

在安装的profile阶段,将运行我们在university_profile_task_list()中指定的任务。在该阶段期间,安装器将重复的调用university_profile_tasks(),并向其传递参数$task和$url,其中$task变量包含了任务名字,而$url则是一个在表单函数中可能用到的URL。安装器初次调用它时,$task将包含字符串profile

    每个任务完成后,Drupal将使用includes/install.inc中的install_goto()请求浏览器来执行一个HTTP重定向,接着在进入下一个任务以前执行一个完整的引导指定。当所有的任务都完成以后,$task将被设置为profile-finished,安装器将停止对university_profile_tasks()的调用并继续前进。
    下面是university_profile_tasks()的一个骨架:
 
function university_profile_tasks(&$task, $url) {
    if ($task == 'profile') {
        // The value of $task is 'profile' the first time we are called.
        // Set up all the things a default installation profile has.
        require_once 'profiles/default/default.profile';
        default_profile_tasks($task, $url);
        // Then do our custom setup here.
 
        // Set $task to the next task.
        $task = 'dept-info';
        // Display a form requesting some info.
        return drupal_get_form('university_department_info', $url);
    }
    if ($task == 'dept-info') {
        // Send email indicating that a site was set up.
 
        // Set $task to key of next task.
        $task = 'support-message';
        // Build some output.
 
        return $output;
    }
    if ($task == 'support-message') {
        // Return control to the installer.
        $task = 'profile-finished';
    }
}
 
    由于我们想要的大多数设置都与平时的Drupal站点一样,所以我们加载了Drupal的默认轮廓,并简单的调用default_profile_tasks(),而不是在我们的安装轮廓中重复所有的代码。另外的一种方式就是将默认轮廓中的代码复制粘贴到第一个任务中。
 
提示 一个简单的安装轮廓不需要实现多个任务。在它被调用时,它可以忽略传递给它的参数并运行代码。当安装器看见$task变量没有改变的话,它将继续前进,运行后面的安装轮廓步骤。Drupal的默认安装轮廓就是这样的一个轮廓,所以我们可以直接在这里调用default_profile_tasks(),而不用担心它会修改$task的值。
 
    注意前面代码中的结构。它包含了一系列的if语句,每一个语句对应一个任务。在每个任务的结尾处,通过引用传递的$task变量,此时将被修改,并返回任意输出,这样将生成一个附加的界面供用户进行交互。
    由于在自定义安装任务运行以前,已经设立了数据库并建立了连接,所以,安装器可以在每个任务的结尾处调用variable_set('install_task', $task),使用一个持久化变量来追踪当前任务的名字。如果你想从一个任务中向另一个任务传递信息的话,那么也可以使用这一技术。只要记住,在你最后一个任务的结尾处,使用variable_del()来删除你用过的变量,这样就可以了。
    下面是大学安装轮廓的university_profile_tasks()函数的完整版本,让我们仔细的学习一下:
 
/**
 * Perform final installation tasks for this installation profile.
 */
function university_profile_tasks(&$task, $url) {
    if ($task == 'profile') {
        // $task is set to 'profile' the first time this function is called.
        // Set up all the things a default installation profile has.
        require_once 'profiles/default/default.profile';
 
        // Need constants defined by modules/comment/comment.module
        // to be in scope.
        require_once 'modules/comment/comment.module';
 
        default_profile_tasks($task, $url);
        // If the administrator enables the comment module, we want
        // to have comments disabled for pages.
        variable_set('comment_page', COMMENT_NODE_DISABLED);
 
        // Define a News Item node type.
        $node_type = array(
            'type' => 'news',
            'name' => st('News Item'),
            'module' => 'node',
            'description' => st('A news item for the front page.'),
            'custom' => TRUE,
            'modified' => TRUE,
            'locked' => FALSE,
            'has_title' => TRUE,
            'title_label' => st('Title'),
            'has_body' => TRUE,
            'orig_type' => 'news',
            'is_new' => TRUE,
        );
        node_type_save((object)$node_type);
 
        // News items should be published and promoted to front page by default.
        // News items should create new revisions by default.
        variable_set('node_options_news', array('status', 'revision',                   'promote'));   
 
        // If the administrator enables the comment module, we want
        // to have comments enabled for news items.
        variable_set('comment_news', COMMENT_NODE_READ_WRITE);
 
        // Create a News Categories vocabulary so news can be classified.
        $vocabulary = array(
            'name' => st('News Categories'),
            'description' => st('Select the appropriate audience for your news                  item.'),
            'help' => st('You may select multiple audiences.'),
            'nodes' => array('news' => st('News Item')),
            'hierarchy' => 0,
            'relations' => 0,
            'tags' => 0,
            'multiple' => 1,
            'required' => 0,
        );
        taxonomy_save_vocabulary($vocabulary);
 
        // Define some terms to categorize news items.
        $terms = array(
            st('Departmental News'),
            st('Faculty News'),
            st('Staff News'),
            st('Student News'),
        );
 
        // Submit the "Add term" form programmatically for each term.
        $form_id = 'taxonomy_form_term';
        // The taxonomy_form_term form is not in taxonomy.module, so need
        // to bring it into scope by loading taxonomy.admin.inc.
        require_once 'modules/taxonomy/taxonomy.admin.inc';
        foreach ($terms as $name) {
            $form_state['values']['name'] = $name;
            $form_state['clicked_button']['#value'] = st('Save');
            drupal_execute($form_id, $form_state, (object)$vocabulary);
        }
 
        // Add a role.
        db_query("INSERT INTO {role} (name) VALUES ('%s')", 'site administrator');
 
        // Configure the pubcookie module.
        variable_set('pubcookie_login_dir', 'login');
        variable_set('pubcookie_id_is_email', 1);
        // ...other settings go here
 
        // Set $task to next task so the installer UI will be correct.
        $task = 'dept-info';
        drupal_set_title(st('Departmental Information'));
        return drupal_get_form('university_department_info', $url);
    }
    if ($task == 'dept-info') {
        // Report by email that a new Drupal site has been installed.
        $to = 'administrator@example.com';
        $from = ini_get('sendmail_from');
        $subject = st('New Drupal site created!');
        $body = st('A new Drupal site was created: @site', array(
            '@site' => base_path()));
        drupal_mail('university-profile', $to, $subject, $body, $from);
 
        // Set $task to next task so the installer UI will be correct.
        $task = 'support-message';
        drupal_set_title(st('Support'));
        $output = '<p>'. st('For support, please contact the Drupal Support Desk
            at 123-4567.') .'</p>';
        // Build a 'Continue' link that goes to the next task.
        $output .= '<p>'. l(st('Continue'), $url) .'</p>';
        return $output;
    }
    if ($task == 'support-message') {
        // Change to our custom theme.
        $themes = system_theme_data();
        $theme = 'university';
        if (isset($themes[$theme])) {
            system_initialize_theme_blocks($theme);
            db_query("UPDATE {system} SET status = 1 WHERE type = 'theme' AND
                name = '%s'", $theme);
            variable_set('theme_default', $theme);
            menu_rebuild();
            drupal_rebuild_theme_registry();
        }
 
        // Return control to the installer.
        $task = 'profile-finished';
    }
}
 

Drupal版本:

Drupal专业开发指南 第23章 运行附加的安装任务(1)

Think in Drupal

我们的第一个自定义安装任务为用户显示了一个交互式的表单。现在让我们定义该表单。我们可以使用标准的表单API,但是在这里需要仔细一点,我们把$form['#redirect']设置为FALSE,把表单的动作设为安装器提供的URL。表单的处理由一个提交处理器负责,这和普通的表单一样。下面是表单定义和提交处理器。表单如图23-4所示。

 
/**
 * Define form used by our dept-info installer task.
 *
 * @param $form_state
 * Keyed array containing the state of the form.
 * @param $url
 * URL of current installer page, provided by installer.
 */
function university_department_info($form_state, $url) {
    $form['#action'] = $url;
    $form['#redirect'] = FALSE;
    $form['department_code'] = array(
        '#type' => 'select',
        '#title' => st('Departmental code'),
        '#description' => st('Please select the correct code for your                   department.'),
        '#options' => array('BIOL', 'CHEM', 'COMP', 'DRUP', 'ENGL', 'HIST', 'MATH',
            'LANG', 'PHYS', 'PHIL'),
    );
    $form['submit'] = array(
        '#type' => 'submit',
        '#value' => st('Save and Continue'),
    );
    return $form;
}
 
/**
 * Handle form submission for university_department_info form.
 */
function university_department_info_submit($form, &$form_state) {
    // Set a persistent variable.
    variable_set('department_code', $form_state['values']['department_code']);
}
 
注意 在整个安装轮廓中,我们使用了st()来替代t(),这样就可以将整个安装轮廓的翻译保存在一个安装轮廓翻译文件中了。它是一个位于安装轮廓的可选目录translations中的.po文件。关于.po文件的更多详细,可参看第18章。
23-4.我们的自定义任务的截图
 

Drupal版本:

Drupal专业开发指南 第23章 错误处理

错误处理

    让我们修改一下第2个操作,importusers_optimize(),来演示一下出现错误时的样子:
 
/**
 * Batch callback operation. Demonstrate error handling.
 */
function importusers_optimize() {
    // Cause fatal error by calling nonexistent function.
    go_bananas();
}
 
    批处理引擎将实际的捕获错误并将用户重定向到一个错误页面。在前面一节中所给的finished回调负责生成错误页面。
 
重定向
    在批处理处理完,和finished函数运行完以后,将会进行一次最终的重定向。重定向的目的地将是批处理在开始时设置的$destination变量。如果没有设置这个变量,那么将使用表单提交处理器中的$form_state['redirect']的值。如果这个也失败了,那么将使用$batch['redirect']。如果所有的都失败了,那么将使用批处理初始化时用户所在页面的URL。

Drupal版本:

Drupal专业开发指南 第23章 附加中英文对照:

installer:安装器

installation profile:安装轮廓
batch:批处理
    在这里最主要的是profile的翻译,在现有的Drupal中,有两个地方用到了profile:Drupal安装的profile,Drupal用户的profile。这两个地方,在英语中应该是同一个意思。但是在汉语中,在简体中文包中,用户的profile被翻译成了“个人资料”,这个含义比较贴近用户profile,但是也不完全吻合。而安装器中的,installation profile被翻译为了“安装包”,这个应该翻译错了,installation profile和我们通常说的安装包不是一回事。而把profile翻译为“包”,那更不对。Adrupal上也翻译过profile,不过我上网不方便,没有参考到。金山词霸中给出了多个意思:剖面, 侧面, 外形, 轮廓,我觉得都不贴切,也不切当。在drupal5版的翻译中,我好像没有翻译profile。但是为了给每个Drupal中的英语找个对应的汉语意思,我在这里翻译成了轮廓。理由是,与“剖面, 侧面, 外形,”这3个相比,轮廓读起来稍微顺口一点,我个人觉得,尽管不贴切。
    另外,installation profile和user profile中的含义是相同的,轮廓放在安装上,放在个人身上,能勉强的说过去。
    还有就是尽可能的从现有的语言翻译中找对应的意思,这个词是我从金山词霸中挑出来的。另外,四个词中“剖面”也相对较好一点,但是还是选用了轮廓。
    轮廓这个词的翻译就是这样,尽管生硬了一点。它是我在一群瘸子中挑出来的,如果有更好的,大可以换了它。

Drupal版本:

Drupal专业开发指南 附录A 数据库表参考

这个附录描述了Drupal核心的数据库表和字段。这里的描述来源于核心模块的.install文件,其中为了表意清晰我稍微做了一点改动。把这一信息放在这里,是为了大家使用时的方便。

 
    主键用粗斜体表示;索引用粗体表示。多列索引在这里没有指示出来,除非该索引是所在表的主索引。在你的Drupal中,在模块的.install文件的模式钩子内部,你可以找到当前的表定义,或者使用第3方的模式模块,位于http://drupal.org/project/schema。非模块核心表的定义位于modules/system/system.install文件中。如果一个表主要用于一个特定的模块,那么将会在表的名字后面的括号内列出该模块。对其它表的引用则使用大括号给出引用表的名字。
 
access(用户模块)
这个表存储站点访问规则。
名字  类型          空     默认值 描述
aid    serial          No              主键:唯一的访问ID
mask    varchar(255)    No      ''     用来过滤访问的文本掩码
type    varchar(255)    No      ''      访问规则类型:名字,邮件,或主机
status int:tiny        No      0       规则是允许(1)还是拒绝(0)
 
 
accesslog(统计模块)
这个表存储站点访问信息,用于统计。
名字  类型          空     默认值 描述
aid    serial          No              主键:唯一的访问日志ID
sid     varchar(64)     No      ''      访问页面的用户的浏览器会话ID
title  varchar(255)    Yes             被访问页面的标题
path    varchar(255)    Yes             被访问页面的内部路径(相对于Drupal根部)
url     varchar(255)    Yes             来源URI
hostname varchar(128)  Yes             访问页面的用户的主机名
uid    int,unsigned    Yes     0       访问页面的用户的{users}.uid
timer  int, unsigned  No      0       加载页面所用的时间,单位为毫秒
timestamp int,unsigned     No      0       页面被访问时的时间戳
 
actions(触发器模块)
这个表存储动作信息。
名字  类型          空     默认值 描述
aid    varchar(255)    No      '0'     主键:唯一的动作ID
type    varchar(32)     No      ''      动作作用的对象(节点,用户,评论,或自定                                        义类型)
callback varchar(255)  No      ''      动作运行时执行的回调函数
parameters text:big     No              传递给回调函数的参数
description varchar(255) No     '0'     动作的描述
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录A #drupal

#drupal

    讨论Drupal的开发。许多核心的开发者都挂在这里。这里只允许发布代码相关的问题。非代码的支持问题不允许发布到这里;请使用#drupal-support 或者 http://drupal.org的论坛来获取支持。
 
#drupal-dev
    这个频道是专门用来讨论Drupal代码的清静之地。开发者有时会觉得#drupal太吵了,或者为了讨论一个#drupal上他人不感兴趣的特定模块,一般常常跑到这里进行。
 
#drupal-consultants
    在这个讨论组里可以找到提供付费服务的Drupal顾问(也可以在Drupal论坛里找到相应的付费服务: http://drupal.org/forum/51)。关于费用的所有讨论都在私下进行。
 
#drupal-dojo
    Drupal Dojo小组(参看“用户小组和兴趣小组”)的课程在这里进行。
 
Videocasts (视频)
    许多时候,一些概念是很难解释的,但是通过视频则能很好的说明问题。在这里收集了许多视频http://drupal.org/videocasts,视频还在不断的丰富中。
 
博客
    博客就是在线日志。许多Drupal开发者都拥有博客,在里面他们可以记录他们的Drupal开发经验。
 
Planet(行星) Drupal
    这里聚合了与Drupal相关的博客。经常阅读这里的博客,能够帮助你把握住Drupal社区动态的脉搏。
 
聚会
    Drupal社区经常举办各种聚会,在聚会中,大家进行交流,讨论,以及其它活动.一般情况下,每个春季会在北美举办一次Drupalcon,而秋季则在欧洲举办一次,这样轮流进行。在聚会中,你可以学到最新的Drupal技术,建立各种业务联系,找到志同道合的朋友。如果你有机会参加的话,一定要过去看看。详情可参看http://drupalcon.org。在会议举办以前,以及举办期间,与会人员可以使用#drupalcon irc频道进行交流,讨论。
    通常,在一次Drupal聚会举办前,或者举办后,都会有一次代码sprint(攻坚)。
 
贡献
    贡献者是Drupal的最重要的资产,也是Drupal持续发展的原因,Drupal不仅仅是一个平台,它还是一个社区。在http://drupal.org/contribute你可以为Drupal贡献力量,不仅仅是通过开发的形式,也可以是文档,翻译,使用,捐赠,市场推广等等。这个页面列出了为Drupal项目在各个层面做贡献的途径。
 
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录A actions_aid(触发器模块)

actions_aid(触发器模块)

这个表为非默认动作存储动作ID,它用作可配置动作的序列表
名字  类型          空     默认值 描述
aid    serial            No              主键:唯一的动作ID
 
aggregator_category(聚合器模块)
这个表为聚合器种子和种子项目存储类别信息。
名字      类型       空     默认值 描述
cid        serial      No              主键:唯一的类别ID
title    varchar(255) No      ''      类别的标题
description text:big    No              类别的描述
block       int:tiny    No      0       在类别区块中显示的最新项目的数量
 
aggregator_category_feed(聚合器模块)
这个关联表用来将种子映射到类别上
名字       类型       空     默认值 描述
fid        int         No      0       种子的{aggregator_feed}.fid
cid        int         No      0       给种子分配的{aggregator_category}.cid
 
aggregator_category_item(聚合器模块)
这个关联表用来将种子项目映射到类别上
名字       类型       空     默认值 描述
iid        int         No      0       种子项目的{aggregator_item}.iid
cid        int         No      0       给种子项目分配的                                                            {aggregator_category}.cid  
 
aggregator_feed(聚合器模块)
这个表用来存储供聚合器解析的种子
名字       类型       空     默认值 描述
fid        serial      No              主键:唯一的种子ID
title varchar(255)    No      ''      种子的标题
url    varchar(255)    No      ''      种子的URL
refresh     int         No      0       检查新种子项目的频率,单位为秒。
checked     int         No      0       最近一次为种子检查新项目的时间,一个                                            Unix时间戳
link    varchar(255)    No      ''      种子的父网站;来自于种子中的<link>元素
description text:big    No              父网站的描述;来自于种子中的                                                <description>元素
image  text:big        No              一个表示种子的图片
etag    varchar(255)    No      ''      HTTP响应头部的实体标签;用来验证缓存
modified    int         No      0       种子最近一次修改的时间,一个Unix时间戳
block  int:tiny        No      0       在种子的区块中,显示的项目的数量
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录A aggregator_item(聚合器模块)

aggregator_item(聚合器模块)

这个表存储了从种子中导入的单独的项目
名字       类型       空     默认值 描述
iid        serial      No              主键:唯一的种子项目ID
fid        int         No      0       这个项目所属的{aggregator_feed}.fid
title  varchar(255)    No      ''      种子项目的标题
link    varchar(255)    No      ''      种子项目的链接
author varchar(255)    No      ''      种子项目的作者
description text:big    No              种子项目的正文
timestamp  int         Yes             种子项目的发布日期,一个Unix时间戳
guid    varchar(255)    Yes             种子项目的唯一标识符
 
authmap(用户模块)
这个表存储了分布式认证映射。
名字       类型       空     默认值 描述
aid        serial      No              主键:唯一的authmap ID
uid         int         No      0       用户的{users}.uid
authname varchar(128) No      ''      唯一的认证名字
module  varchar(128)  No      ''      负责认证的模块
 
batch (batch.inc)
这个表用来存储批处理相关的详细(在多个HTTP请求中运行的处理)
名字       类型       空     默认值 描述
bid        serial      No              主键:唯一的批处理ID。
token      varchar(64) No              一个基于当前用户的会话ID和批处理ID生成                                    的字符串令牌;用来确保只有提交批处理的用                                        户才能访问这个批处理。
timestamp  int         No              一个Unix时间戳,用来指示这个批处理被提                                         交的时间。过期的批处理将在cron运行时被                                         清除掉。
batch       text:big    Yes             一个序列化数组,里面包含了批处理的处理数                                        据。
 老葛的Drupal培训班 Think in Drupal
 

Drupal版本:

Drupal专业开发指南 附录A blocks(区块模块)

blocks(区块模块)

这个表存储区块设置,比如区域和可见性设置
名字       类型       空     默认值 描述
bid        serial        No              主键:唯一的区块ID。
module      varchar(64) No      ''      负责创建区块的模块;例如,在线用户区块的                                        为'user',对于任意自定义区块则使用                                          'block'。
delta       varchar(32) No      '0'     在一个模块内部,区块的唯一ID。
theme       varchar(64) No      ''      区块设置所应用于的主题。
status      int:tiny    No      0       区块启用状态(1表示已启用,0表示已禁用)
weight      int:tiny    No      0       在区域内的区块重量。
region      varchar(64) No      ''      区块所在的主题区域。
custom      int:tiny    No      0       一个标记,用来指示用户如何控制区块的可见                                        性(0表示用户不能控制区块;1,区块默认                                          是显示的,但是可被隐藏;2,区块默认是隐                                           藏的,但是可被显示出来)。
throttle    int:tiny    No      0       一个标记,用来指示当网站流量大时是否删除                                        区块(1意味着节流;0意味着不节流)。
visibility int:tiny    No      0       一个标记,用来指示如何在页面中显示区块(0                                       意味着,显示在除所列页面以外的所有页面;                                        1意味着只显示在所列页面;2意味着使用自                                           定义PHP代码来判定可见性)
pages       text        No              区块的"Pages"的内容;基于"visibility"中                                         的设置,它或者包含一列Drupal路径,用来                                         排除或包含,或者包含一段PHP代码。
title       varchar(64) No      ''      区块的自定义标题(如果为空字符串,那么将                                        使用区块默认标题;<none>则删除标题;如果                                        为一段文本,那么区块将使用指定的标题)。
cache       int:tiny    No      1       二进位标记,用来指示区块的缓存模式(-1                                          意味着不缓存;1意味着基于角色缓存;2,                                          基于用户缓存;4,基于页面缓存;8,全局缓                                      存区块)。
 
blocks_roles(区块模块)
这个表为区块存储了基于用户角色的访问权限
名字       类型       空     默认值 描述
module      varchar(64) No              区块所在的模块,来自于{blocks}.module
delta       varchar(32) No              在模块内部区块的序列ID
rid     int, unsigned  No              用户的角色ID,来自于{users_roles}.rid
 
 
book(手册模块)
这个表用来存储手册大纲信息,并将大纲中的每个节点连接到menu_links表中的一个唯一的链接上。
名字       类型       空     默认值 描述
mlid  int, unsigned  No      0       手册页面的{menu_links}.mlid。
nid    int, unsigned  No      0       手册页面的{node}.nid。
bid    int, unsigned  No      0       手册ID就是顶层页面的{book}.nid
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录A boxes (block module) (区块模块)

boxes (block module) (区块模块)

这个表存储了自定义区块的内容
名字       类型       空     默认值 描述
bid        serial      No              区块的{blocks}.bid。
body        text:big    Yes             区块内容。
info  varchar(128)    No      ''      区块描述
format int:small       No      0       区块正文的{filter_formats}.format;例如,                                       1意味着Filtered HTML
 
cache
通用cache表,用来缓存那些需要缓存但是又没有专有缓存表的数据。第3方模块也可以使用这个表来存储缓存项目。
名字       类型       空     默认值 描述
cid    varchar(255)    No      ''      主键:唯一的缓存ID
data    blob:big        Yes             一组要缓存的数据
expire        int         No      0       一个Unix时间戳,用来指示缓存条目的过期                                         时间, 0表示永不过期。
created     int         No      0       一个Unix时间戳,用来指示缓存条目的创建                                         日期
headers     text        Yes             任意自定义HTTP头部,用来添加到缓存数据                                         中
serialized int:small    No      0       一个标记,用来指示内容是否序列化了,0表                                     示没有
 
cache_block(区块模块)
区块模块使用这个缓存表来存储已经构建的区块,使用模块、delta、各种可能修改区块的上下文来标识缓存,这里的上下文可以是主题、本地语言、为该模块定义的缓存模式。
名字       类型       空     默认值 描述
cid    varchar(255)    No      ''      主键:唯一的缓存ID
data    blob:big        Yes             一组要缓存的数据
expire        int         No      0       一个Unix时间戳,用来指示缓存条目的过期                                         时间, 0表示永不过期。
created     int         No      0       一个Unix时间戳,用来指示缓存条目的创建                                         日期
headers     text        Yes             任意自定义HTTP头部,用来添加到缓存数据                                         中
serialized int:small    No      0       一个标记,用来指示内容是否序列化了,0表                                     示没有
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录A cache_filter(过滤器模块)

cache_filter(过滤器模块)

过滤器模块专用缓存表,用来存储已经过滤的文本片断,使用输入格式和文本的MD5哈希值进行标识。
名字       类型       空     默认值 描述
cid    varchar(255)    No      ''      主键:唯一的缓存ID
data    blob:big        Yes             一组要缓存的数据
expire        int         No      0       一个Unix时间戳,用来指示缓存条目的过期                                         时间, 0表示永不过期。
created     int         No      0       一个Unix时间戳,用来指示缓存条目的创建                                         日期
headers     text        Yes             任意自定义HTTP头部,用来添加到缓存数据                                         中
serialized int:small    No      0       一个标记,用来指示内容是否序列化了,0表                                     示没有
 
cache_form
表单系统专用缓存表,用来存储最近构建的表单和它们的储存数据,用在接下来的页面请求中。
名字       类型       空     默认值 描述
cid    varchar(255)    No      ''      主键:唯一的缓存ID
data    blob:big        Yes             一组要缓存的数据
expire        int         No      0       一个Unix时间戳,用来指示缓存条目的过期                                         时间, 0表示永不过期。
created     int         No      0       一个Unix时间戳,用来指示缓存条目的创建                                         日期
headers     text        Yes             任意自定义HTTP头部,用来添加到缓存数据                                         中
serialized int:small    No      0       一个标记,用来指示内容是否序列化了,0表                                     示没有
 
cache_menu
菜单系统专用缓存表,用来存储路由信息,以及为不同的“菜单/页面/用户”联合体生成的链接树。
名字       类型       空     默认值 描述
cid    varchar(255)    No      ''      主键:唯一的缓存ID
data    blob:big        Yes             一组要缓存的数据
expire        int         No      0       一个Unix时间戳,用来指示缓存条目的过期                                         时间, 0表示永不过期。
created     int         No      0       一个Unix时间戳,用来指示缓存条目的创建                                         日期
headers     text        Yes             任意自定义HTTP头部,用来添加到缓存数据                                         中
serialized int:small    No      0       一个标记,用来指示内容是否序列化了,0表                                     示没有

Drupal版本:

Drupal专业开发指南 附录A cache_page

cache_page

这个缓存表用来为匿名用户存储压缩的页面,这里假定启用了页面缓存。
名字       类型       空     默认值 描述
cid    varchar(255)    No      ''      主键:唯一的缓存ID
data    blob:big        Yes             一组要缓存的数据
expire        int         No      0       一个Unix时间戳,用来指示缓存条目的过期                                         时间, 0表示永不过期。
created     int         No      0       一个Unix时间戳,用来指示缓存条目的创建                                         日期
headers     text        Yes             任意自定义HTTP头部,用来添加到缓存数据                                         中
serialized int:small    No      0       一个标记,用来指示内容是否序列化了,0表                                     示没有
 
cache_update
更新模块专用缓存表,用来存储从中心服务器抓取过来的关于可用版本的信息。
名字       类型       空     默认值 描述
cid    varchar(255)    No      ''      主键:唯一的缓存ID
data    blob:big        Yes             一组要缓存的数据
expire        int         No      0       一个Unix时间戳,用来指示缓存条目的过期                                         时间, 0表示永不过期。
created     int         No      0       一个Unix时间戳,用来指示缓存条目的创建                                         日期
headers     text        Yes             任意自定义HTTP头部,用来添加到缓存数据                                         中
serialized int:small    No      0      一个标记,用来指示内容是否序列化了,0表                                     示没有
 
comments(评论模块)
这个表存储了评论和其关联数据。
名字       类型       空     默认值 描述
cid        serial      No              主键:唯一的评论ID
pid        int         No      0       这个评论回复的评论的{comments}.cid。如果                                        设置为0,那么这个评论就不是对已有评论的                                     回复。
nid        int         No      0       这个评论回复的节点的{node}.nid
uid         int         No      0       评论作者的{users}.uid。如果设置为0,那                                          么这个评论就是由匿名用户创建的。
subject     varchar(64) No      ''      评论标题。
comment     text:big    No              评论正文。
hostname varchar(128)  No      ''      作者的主机名。
timestamp  int         No      0       一个Unix时间戳,评论的创建或者最近编辑                                         时间。
status int:tiny,unsigned No    0       评论的发表状态(0意味着已发表,1意味着                                         未发表)。
format int:small       No      0       评论正文的{filter_formats}.format。
thread varchar(255)    No              在一个串列中,评论位置的前卫代码表示。
name    varchar(60)     Yes             评论作者的名字。如果用户登录了,那么使用                                        {users}.name;否则,使用评论表单中输入的                                        值。
mail    varchar(64)     Yes             来自评论表单的评论作者的电子邮件地址,这                                        里假定用户为匿名的,并且启用了“匿名用户                                        可以/必须输入联系信息”设置。
homepage varchar(255)  Yes             来自评论表单的评论作者的主页地址,这里假                                        定用户为匿名的,并且启用了“匿名用户可以                                        /必须输入联系信息”设置。
 

Drupal版本:

Drupal专业开发指南 附录A contact (contact module)(联系模块)

contact (contact module)(联系模块)

这个表用来存储联系表单的类别设置。
名字       类型       空     默认值 描述
cid        serial      No              主键:唯一的类别ID
category  varchar(255)No      ''      类别名字
recipients text:big    No              使用逗号分割的收件人电子邮件列表
reply       text:big    No              自动回复消息的文本
weight      int:tiny    No      0       类别的重量
selected    int:tiny    No      0       一个标记,用来指示类别在默认情况下是否选                                        中(1为是,0为否)
 
files (upload module)(上传模块)
这个表用来存储已上传文件的信息
名字       类型       空     默认值 描述
fid        serial      No              主键:唯一的文件ID
uid    int, unsigned  No      0       与这个文件关联的用户的{users}.uid
filename varchar(255)  No      ''      文件的名字
filepath varchar(255)  No      ''      文件相对于Drupal根部的路径
filemime varchar(255)  No      ''      文件MIME类型
filesize int,unsigned  No      0       文件的大小,单位为字节。
status        int         No      0       一个标记,用来指示文件是临时的(1),还是                                        持久的(0)
timestamp int,unsigned     No      0       Unix时间戳,文件的添加时间
 
filter_formats(过滤器模块)
这个表存储输入格式,它是过滤器的自定义组合,比如Filtered HTML。
名字       类型       空     默认值 描述
format        serial      No              主键:输入格式的唯一的ID
name  varchar(255)    No      ''      输入格式的名字(例如,Filtered HTML)
roles  varchar(255)    No      ''      一个逗号分割的角色字符串,参考{role}.rid
cache  int:tiny        No      0       一个标记,用来指示输入格式是否可被缓存(1                                       为可缓存,0为不可缓存)
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录A filters(过滤器模块)

filters(过滤器模块)

这个表用来将过滤器(例如,HTML校正者)映射到输入格式上(例如,Filtered HTML)。
名字       类型       空     默认值 描述
fid        serial      No              主键:自增的过滤器ID
format      int         No      0       外键:这个过滤器分配给的                                                    {filter_formats}.format
module      varchar(64) No      ''      过滤器的起源模块
delta       int:tiny    No      0       过滤器在起源模块中的标识ID
weight      int:tiny    No      0       过滤器在输入格式内部的重量
 
flood(联系模块)
这个表用来控制事件的上限,比如尝试联系的次数
名字       类型       空     默认值 描述
fid        serial      No              主键:唯一的注入事件ID
event       varchar(64) No      ''      事件的名字(例如,联系)
hostname varchar(128)  No      ''      访问者的主机名
timestamp  int         No      0       事件的时间戳
 
forum(论坛模块)
这个表存储节点与论坛术语之间的关系
名字       类型       空     默认值 描述
nid    int, unsigned  No      0       节点的{node}.nid
vid    int, unsigned  No      0       主键:节点的{node}.vid
tid    int, unsigned  No      0       分配给节点的论坛术语的{term_data}.tid
 
 
history(节点模块)
这个表存储一个记录----哪个用户访问了哪个节点
名字       类型       空     默认值 描述
uid        int         No      0       访问{node}.nid的用户的{users}.uid
nid         int         No      0       被访问的{node}.nid
timestamp  int         No      0       Unix时间戳,访问发生的时间
 
 

Drupal版本:

Drupal专业开发指南 附录A languages(本地模块)

languages(本地模块)

这个表存储了系统中所有可用的语言。
名字       类型       空     默认值 描述
language  varchar(12) No      ''      语言代码,例如,'de'或'en-US'
name        varchar(64) No      ''      语言的英文名字
native      varchar(64) No      ''      语言的本地名字
direction  int         No      0       语言的读写方向(0表示从左到右,1表示从                                         右到左)
enabled     int         No      0       启用标记(1表示已启用,0表示已禁用)
plurals     int         No      0       在这个语言中,复数索引的数字
formula  varchar(128)  No      ''      PHP代码形式的复数公式,用来计算复数索引
domain  varchar(128)  No      ''      为这个语言使用的域
prefix   varchar(128)  No      ''      为这个语言使用的路径前缀
weight      int         No      0       重量,用来语言列表中
javascript varchar(32)  No      ''      JavaScript翻译文件的位置
 
locales_source(本地模块)
这个表存储一列英语源字符串
名字       类型      空     默认值 描述
lid        serial      No              这个字符串的唯一标识符。
location varchar(255)  No      ''      在线发现翻译时为Drupal路径,导入字符串                                         时为文件路径。
textgroup varchar(255) No   'default' 一个模块定义的翻译组。
source        text        No              英文形式的原始字符串。
version     varchar(20) No      'none' 字符串最后使用时所在的Drupal版本(为了                                         本地最优化)
 
 
locales_target(本地模块)
这个表存储了字符串的翻译版本
名字       类型       空     默认值 描述
lid        int         No      0       原始字符串ID,参照{locales_source}.lid
translation text        No              在这个语言中的翻译字符串的值
language varchar(12)  No      ''      语言代码,参照{languages}.language
plid        int         No      0       用于复数字符串的父lid(在复数链中前一个                                     字符串的lid),参照{locales_source}.lid
plural      int         No      0       用于复数字符串的复数索引数字
 
 
menu_custom(菜单模块)
这个表存储顶层自定义菜单的定义(例如,一级链接)
名字       类型       空     默认值 描述
menu_name varchar(32) No      ''      主键:菜单的唯一键。这个用作区块的delta,                                       所以它的长度为32,这与{blocks}.delta匹                                         配。
title    varchar(255) No      ''      菜单标题,显示在区块的顶部。
description text        Yes             菜单描述。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录A menu_links(菜单模块)

menu_links(菜单模块)

这个表用来存储菜单内的单个链接
名字       类型       空     默认值 描述
menu_name varchar(32)  No      ''      菜单名字。具有相同菜单名字(比如                                            'navigation')的所有链接都属于该菜单的一                                        部分。
mlid       serial      No              菜单链接ID,一个整数主键。
plid     int, unsigned No      0       父菜单链接ID,是上一层链接的mlid;如果                                         该链接位于它的菜单的顶层,那么plid为0。
link_path varchar(255) No      ''      这个链接指向的Drupal路径或外部路径。
router_path varchar(255) No  ''      对于那些对应于一个Drupal路径(0意味着                                          外部的)的链接,这个用来将其与                                              {menu_router}.path关联起来。
link_title varchar(255) No      ''      为链接显示的文本,通过{menu_router}中存                                         储的标题回调可对其进行修改。
options     text        Yes             选项的序列化数组,用来传递给url()或l()                                         函数,比如一个查询字符串或者HTML属性。
module varchar(255)    No   'system' 生成这个链接的模块的名字。
hidden int:small       No      0       一个标记,用来指示链接是否应该呈现在菜单                                        中(1指示一个已禁用菜单项,可以显示在管                                     理界面;-1指示一个菜单回调;0指示一个普                                       通的、可见的链接)。
external int:small      No      0       一个标记,用来指示链接是否指向一个完整的                                        URL上,以某个协议打头的URL,比如http://                                        (1表示外部的,0表示内部的)。
has_children int:small No      0       一个标记,用来指示这个链接是否有子链接(1                                       意味着有子链接;0意味着没有子链接)。
expanded int:small      No      0       一个标记,用来指示这个链接在菜单中是否呈                                        现为展开的;展开的链接总会把它们的子链接                                        显示出来,不展开的链接只有在链接处于激活                                      状态时才显示子链接(1意味着展开,0意味                                         着不展开)。
weight      int         No      0       链接的重量,在同一菜单中的同一个深度下。
depth       int:small  No      0       相对于顶层的深度。plid == 0的链接,它的                                     深度depth == 1。
customized int:small  No      0       一个标记,用来指示用户是否手动的创建或编                                        辑了链接(1意味着定制的,0意味着未定制                                         的)。
p1      int, unsigned  No      0       具体路径中的第一个mlid。如果N = depth,                                        那么pN必须等于mlid。如果depth > 1,那                                         么p(N-1)必须等于plid。所有的X > depth 的                                     pX必须等于0。列p1 . . . p9也被称为父亲。
p2      int, unsigned  No      0       具体路径中的第2个mlid。参看p1。
p3      int, unsigned  No      0       具体路径中的第3个mlid。参看p1。
p4      int, unsigned  No      0       具体路径中的第4个mlid。参看p1。
p5      int, unsigned  No      0       具体路径中的第5个mlid。参看p1。
p6      int, unsigned  No      0       具体路径中的第6个mlid。参看p1。
p7      int, unsigned  No      0       具体路径中的第7个mlid。参看p1。
p8      int, unsigned  No      0       具体路径中的第8个mlid。参看p1。
p9      int, unsigned  No      0       具体路径中的第9个mlid。参看p1。
updated     int:small  No      0       一个标记,用来指示这个链接是否是从                                          Drupal5升级而来的。
 

Drupal版本:

Drupal专业开发指南 附录A menu_router

menu_router

这个表将路径映射到了各种回调上(例如,访问、页面和标题回调)。
名字           类型       空 默认值 描述
path      varchar(255)    No ''      主键:这个条目描述的Drupal路径。
load_functions varchar(255) No ''      一个序列化数组,里面包含了要调用的函数                                          (比如node_load),用来加载一个对象,它                                          对应于当前路径中的一部分的。
to_arg_functions varchar(255) No ''     一个序列化数组,里面包含了要调用的函数                                          (比如user_uid_optional_to_arg),用来将                                     路由路径中的一部分替换为另一个字符串。
access_callback varchar(255) No ''      一个回调,用来判定是否可以访问这个路由路                                        径;默认为user_access。
access_arguments text       Yes         一个序列化数组,里面包含了传递给访问回调                                        的参数。
page_callback varchar(255) No ''      呈现页面的函数的名字
page_arguments text         Yes         一个序列化数组,里面包含了传递给页面回调                                        的参数。
fit           int         No 0       路径特殊程度的数字表示。
number_parts    int:small  No 0       这个路由路径中组成部分的数量。
tab_parent    varchar(255)    No ''      仅用于本地任务(标签);父页面的路由路径                                         (它也可以是一个本地任务)。
tab_root    varchar(255)    No ''      最近的非标签父页面的路由路径。对于不是本                                        地任务的页面,这与它的路径相同。
title       varchar(255)    No ''      当前页面的标题;如果这是一个本地任务,那                                        么它就是标签的标题
title_callback varchar(255) No ''      一个可以修改标题的函数;默认为t()。
title_arguments varchar(255) No ''      一个序列化数组,里面包含了传递给标题回调                                        的参数。如果为空,那么标题将用作唯一的参                                        数传递给标题回调。
type            int         No 0       菜单项类型的数字表示,比如                                                  MENU_LOCAL_TASK。
block_callback varchar(255) No ''      一个函数名字,用来在系统管理页面中为这个                                        菜单项呈现区块。
description     text        No          这个菜单项的描述
position    varchar(255)    No ''      这个菜单项,在系统管理页面对应区块的位置                                        (左或右)。
weight          int         No 0       该元素的重量。轻的排在前面;重的排在后面。
file        text:medium     Yes         为这个元素包含的文件,通常页面回调函数位                                        于这个文件中。
老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录A node_comment_statistics(评论模块)

node_comment_statistics(评论模块)

这个表维护了节点和评论发布的统计,用来显示“新”和“已更新”标记
名字          类型       空 默认值 描述
nid        int, unsigned  No 0       汇编的统计信息所针对的{node}.nid
last_comment_timestamp int  No 0       在这个节点内,最近发布评论的时间,来自于                                        {comments}.timestamp
last_comment_name varchar(60) Yes       对这个节点最近发布评论的作者的名字,来自                                        于{comments}.name
last_comment_uid int        No 0       对这个节点最近发布评论的作者的用户ID,                                          来自于{comments}.uid
comment_count int,unsigned No 0       这个节点上评论的总数。
 
node_counter(统计模块)
这个表为节点存储访问统计。
名字          类型       空 默认值 描述
nid           int         No 0       这些统计的{node}.nid
totalcount int:big,unsigned No 0       {node}被查看的总次数
daycount int:medium,unsigned No 0       {node}今天被查看的总次数
timestamp  int,unsigned    No 0       {node}最近被查看的时间
 
 
node_revisions (节点模块)
这个表存储了一个节点的每个现有版本的相关信息。
名字          类型       空 默认值 描述
nid        int, unsigned  No 0       这个版本所属的节点
vid           serial      No          这个版本的主标识符
uid           int         No 0       创建这个版本的{users}.uid
title       varchar(255)    No ''      这个版本的标题
body            text:big    No          这个版本的正文
teaser          text:big    No          这个版本的摘要
log             text:big    No          日志条目,用来解释这个版本的变更。
timestamp       int         No 0       一个Unix时间戳,指示这个版本的创建时间
format          int         No 0       这个版本的正文所用的输入格式
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录A node_type (节点模块)

node_type (节点模块)

这个表存储了所有定义的{node}类型的相关信息。
名字          类型       空 默认值 描述
type       varchar(32)     No         这个类型的机器可读的名字。
name        varchar(255)    No ''      这个类型的用户可读的名字。
module      varchar(255)    No          实现这个类型的模块。
description text:medium     No          这个类型的简短描述。
help        text:medium     No          创建这个类型的一个{node}时,显示给用户的                                        帮助信息。
has_title int:tiny,unsigned No          布尔值,指示这个类型是否使用{node}.title                                        字段。
title_label varchar(255)    No ''      在编辑表单上,为标题字段显示的标签。
has_body int:tiny,unsigned No          布尔值,指示这个类型是否使用                                                {node_revisions}.body字段。
body_label varchar(255)    No ''      在编辑表单上,为正文字段显示的标签。
min_word_count int:small,unsigned No    正文必须包含的最小字数。
custom      int:tiny        No 0       一个布尔值,用来指示这个类型是由一个模块                                        定义的(0),还是由用户通过管理界面比如使                                        用CCK定义的(1)。
modified    int:tiny        No 0       一个布尔值,用来指示这个类型是否已被管理                                        员修改过;当前这个字段没有任何用处。
locked      int:tiny        No 0       一个布尔值,用来指示管理员是否可以修改这                                        个类型的机器名字。
orig_type  varchar(255)    No ''      这个节点类型的最初的机器可读的名字。如果                                        locked字段为0的话,那么它可能与当前类                                          型名字不同。
 
openid_association (openid模块)
这个表为OpenID认证存储临时共享键的关联信息。
名字               类型       空 默认值 描述
idp_endpoint_uri varchar(255)  Yes         OpenID供给者终端的URI
assoc_handle  varchar(255)  No          主键:用来在接下来的消息中引用这个关                                            联
assoc_type      varchar(32)     Yes         使用的签名算法:HMACSHA1或                                                  HMAC-SHA256
session_type    varchar(32)     Yes         有效的关联会话类型:noencryption,                                               DH-SHA1, 和DH-SHA256
mac_key         varchar(255)    Yes         这个关联的MAC键(共享密钥)
created             int         No 0       一个Unix时间戳,关联的创建时间
expires_in          int         No 0       这个关联的有效期,单位为秒。
 
 
permission(用户模块)
这个表为用户存储了权限信息
名字          类型       空 默认值 描述
pid           serial      No          主键:唯一的权限ID
rid        int, unsigned  No 0       权限分配给的{role}.rid
perm            text:big    Yes         正被分配的权限列表
tid         int, unsigned  No 0       最初打算用于基于分类的权限,但是从未使用                                        过。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录A node(节点模块)

node(节点模块)

这是节点的基础表。
名字          类型       空 默认值 描述
nid           serial      No          一个节点的主标识符。
vid        int, unsigned  No 0       当前{node_revisions}.vid版本标识符
type       varchar(32)     No ''      这个节点的{node_type}.type
language    varchar(12)     No ''      这个节点的{languages}.language
title       varchar(255)    No ''      这个节点的标题,一般作为纯文本进行处理。
uid           int         No 0       拥有这个节点的{users}.uid;最初,为创建                                         节点的用户。
status          int         No 1       布尔值,用来指示节点是否已发表(非管理员                                        可见)。
created       int         No 0       一个Unix时间戳,节点的创建时间。
changed       int         No 0       一个Unix时间戳,节点的最近修改时间。
comment         int         No 0       是否允许对这个节点进行评论:0意味着不可                                     以;1意味着评论为只读的;2意味着评论可                                         读可写。
promote         int         No 0       布尔值,用来指示是否将节点推到首页。
moderate      int         No 0       在以前,它是一个布尔值,用来指示节点处于                                        “审核中”;现在,核心中已不再使用这个字                                         段。
sticky          int         No 0       布尔值,用来指示节点在它出现的列表中是否                                        被置顶。
tnid       int, unsigned  No 0       这个节点的翻译集的ID,也就是每个翻译集                                          的源节点的节点ID。
translate     int         No 0       一个布尔值,用来指示这个翻译页面是否需要                                        更新。
 
node_access (节点模块)
这个表存储了“领域/授权”对儿,为了查看、更新、或删除特定的节点,用户必须拥有对应的“领域/授权”对儿。
名字          类型       空 默认值 描述
nid        int, unsigned  No 0       这个纪录影响的{node}.nid。
gid        int, unsigned  No 0       授权ID:在指定的领域中的给定节点上为了                                          获得这行的特权,用户必须拥有的授权ID。
realm      varchar(255)    No ''      用户拥有的授权ID适用的领域。每个节点访                                         问节点可以定义一个或多个领域。
grant_view int:tiny,unsigned No 0       布尔值,用来指示一个拥有“领域/授权”对                                         儿的用户是否可以查看这个节点。
grant_update int:tiny,unsigned No 0     布尔值,用来指示一个拥有“领域/授权”对                                         儿的用户是否可以编辑这个节点。
grant_delete int:tiny,unsigned No 0     布尔值,用来指示一个拥有“领域/授权”对                                         儿的用户是否可以删除这个节点。
 

Drupal版本:

Drupal专业开发指南 附录A poll(民意测验模块)

poll(民意测验模块)

这个表为民意测验节点存储特定于民意测验的信息。
名字          类型       空 默认值 描述
nid        int, unsigned  No 0       民意测验的{node}.nid
runtime         int         No 0       自{node}.created以来民意测验的开放时                                        长,单位为秒。
active      int, unsigned  No 0       布尔值,用来指示民意测验是否开放
 
poll_choices(民意测验模块)
这个表为所有的民意测验存储了所有的选项信息。
名字          类型       空 默认值 描述
chid          serial      No          主键:民意测验选项的唯一标识符。
nid        int, unsigned  No 0       这个选项所属的{node}.nid
chtext      varchar(128)    No ''      这个选项的文本
chvotes         int         No 0       这个选项所收到的来自所有用户的总票数
chorder         int         No 0       这个选项在同一节点内的所有选项中的排序
 
poll_votes(民意测验模块)
这个表为每个民意测验存储每个用户的投票
名字          类型       空 默认值 描述
nid        int, unsigned  No          这个投票针对的{poll}节点
uid        int, unsigned  No 0       这个投票来自的{users}.uid,除非投票者是                                         匿名的
chorder         int         No -1      {users}为这个民意测验的投的票
hostname  varchar(128)    No ''      这个投票来自的IP地址,除非投票者已登录。
 
profile_fields (profile module)(轮廓模块)
This table stores profile field information.这个表存储了轮廓字段信息
名字         类型       空 默认值 描述
fid           serial      No          主键:唯一的轮廓字段ID
title       varchar(255)    Yes         字段的标题,显示给终端用户
name       varchar(128)    No ''      字段的内部名字,用在表单HTML和URL中
explanation     text        Yes         字段的解释,用于终端用户
category  varchar(255)    Yes         轮廓类别,使用它对字段进行分组
page        varchar(255)    Yes         页面的标题,用在根据字段的值进行浏览时
type        varchar(128)    Yes         表单字段的类型
weight      int:tiny        No 0       字段的重量,相对于其它的轮廓字段
required    int:tiny        No 0       用户是否必须输入一个值(0为否,1为是)。
register    int:tiny        No 0       字段在用户注册表单中是否可见(0为否,1                                          为是)
visibility int:tiny        No 0       字段的可见性的层次(0表示隐藏;1表示私                                         有;2表示公布在轮廓页面,但是不显示在会                                     员列表页面;3表示公布在轮廓和列表页面)
autocomplete    int:tiny    No 0       是否启用表单的自动完成功能(0表示已禁                                           用,1表示已启用)
options         text        Yes         一列选项,用在列表选择字段。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录A profile_values(轮廓模块)

profile_values(轮廓模块)

这个表为轮廓字段存储了值
名字          类型       空 默认值 描述
fid        int, unsigned  No 0       字段的{profile_fields}.fid
uid        int, unsigned  No 0       轮廓的用户的{users}.uid
value           text        Yes         字段的值
 
role(用户模块)
这个表存储了用户角色
名字          类型       空 默认值 描述
rid           serial      No          主键:唯一的角色ID
name       varchar(64)     No ''      唯一的角色名字
 
search_dataset(搜索模块)
这个表存储了将被搜索的项目
名字          类型       空 默认值 描述
sid        int, unsigned  No 0       搜索项目ID,例如,对于节点就是节点ID
type       varchar(16)     Yes         项目的类型,例如,节点
data        text:big        No          来自于项目的一列空格分隔的字词
reindex     int, unsigned  No 0       设置为强制重新索引节点
 
search_index(搜索模块)
这个表存储了搜索索引和关联的字词、项目、和分数。
名字          类型       空 默认值 描述
word       varchar(50)     No ''      与该搜索项目关联的{search_total}.word
sid         int, unsigned  No 0       这个字词所属的可搜索项目的                                                  {search_dataset}.sid
type        varchar(16)     Yes         这个字词所属的可搜索项目的                                                  {search_dataset}.type
score           float       Yes         字词的数字分数,越高越重要
 
search_node_links(搜索模块)
这个表存储了链接到其它节点的项目(比如,节点);它用来改进那些拥有较多外链的节点的搜索分数。
名字          类型       空 默认值 描述
sid        int, unsigned  No 0       可搜索项目的{search_dataset}.sid,它包含                                        了指向节点的链接
type       varchar(16)     No ''      可搜索项目的{search_dataset}.type,它包                                         含了指向节点的链接
nid        int, unsigned  No 0       这个项目链接到的{node}.nid
caption       text:big      Yes         指向{node}.nid的链接的文本
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录A search_total(搜索模块)

search_total(搜索模块)

这个表存储字词的搜索总数。
名字          类型       空 默认值 描述
word          varchar(50) No ''      主键:在搜索索引中唯一的字词
count           float       Yes         字词在索引中的总数,这里使用齐波夫定律来                                        均一化概率分布。
 
sessions
Drupal的会话处理器对sessions表进行读写。每个记录表示一个用户会话,或者是匿名的,或者是认证的。
名字          类型       空 默认值 描述
uid        int, unsigned  No          对应于一个会话的{users}.uid,0表示匿名                                          用户。
sid        varchar(64)     No ''      主键:一个会话ID。它的值由PHP的会话API                                    生成。
hostname    varchar(128)    No ''      这个会话ID(sid)最后使用的IP地址。
timestamp     int         No 0       一个Unix时间戳,表示这个会话最近一次请                                         求页面的时间。过期的记录将被PHP自动清                                          除。参看sess_gc()。
cache           int         No 0       这个用户的最后发布的时间。这个用在站点指                                        定了一个minimum_cache_lifetime时。参看                                         cache_get()。
session     text:big        Yes         $_SESSION的序列化内容,一个包含了“名/                                          值”对儿的数组,使用这个会话ID可在多个                                         页面请求中使用这里存放的数据。Drupal在                                            每个请求的开始阶段从这里加载$_SESSION,                                         并在结束时将它保存回来。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录A system

system

这个表包含了位于Drupal的文件系统中的一列所有的模块、主题、和主题引擎。
名字          类型       空 默认值 描述
filename  varchar(255)    No ''      这个项目的主文件的路径,相对于Drupal根                                         部;例如,modules/node/node.module。
name        varchar(255)    No ''      项目的名字;例如,node
type        varchar(255)    No ''      项目的类型:module, theme, 或                                               theme_engine
owner       varchar(255)    No ''      一个主题的“父亲”;可以是一个主题,或者                                         是一个引擎。
status         int         No 0       布尔值,用来指示这个项目是否已启用。
throttle        int:tiny    No 0       布尔值,用来指示:当throttle.module禁用                                        其它可被节流的项目时,是否禁用这个项目。
bootstrap       int         No 0       布尔值,用来指示在Drupal的早期引导指令                                         阶段(也就是,在考虑页面缓存以前)是否加                                        载这个模块。
schema_version int:small    No -1      模块的数据库模式版本号。-1表示模块未安                                          装(它的表不存在)。如果模块安装了,那么                                         在初次安装时,这个字段要么为0,如果同时                                       运行了模块的hook_update_N()函数,那么这                                        个字段就是这里最大的N。
weight          int         No 0       这个模块的钩子的调用次序,这里相对于其它                                        模块。重量相同的模块按照名字排序。
info            text        Yes         一个序列化数组,里面包含了来自于模块                                            的.info文件的信息;键可以包括name,                                             description, package, version, core,                                            dependencies, dependents, 和php
 
term_data(分类模块)
这个表存储了术语信息
名字          类型       空 默认值 描述
tid           serial      No          主键:唯一的术语ID
vid         int, unsigned  No 0       这个术语所属词汇表的{vocabulary}.vid
name        varchar(255)    No ''      术语名字
description text:big        Yes         术语的描述
weight      int:tiny        No 0       这个术语的重量,相对于其它术语。
 
term_hierarchy(分类模块)
这个表存储了术语之间的层级关系。
名字          类型       空 默认值 描述
tid        int, unsigned  No 0       主键:术语的{term_data}.tid。
parent        int, unsigned  No 0       主键:术语的父亲的{term_data}.tid。0表                                          示没有父亲。
 
term_node(分类模块)
这个表存储了术语与节点之间的关系
名字          类型       空 默认值 描述
nid         int, unsigned  No 0       节点的{node}.nid
vid        int, unsigned  No 0       主键:节点的{node}.vid
tid        int, unsigned  No 0       主键:分配给节点的术语的{term_data}.tid
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录A term_relation(分类模块)

term_relation(分类模块)

这个表存储了术语之间的非层级关系。
名字          类型       空 默认值 描述
trid          serial      No          主键:唯一的术语关系ID
tid1        int, unsigned  No 0       关系中的第一个术语的{term_data}.tid
tid2       int, unsigned  No 0       关系中的第二个术语的{term_data}.tid
 
term_synonym (分类模块)
这个表存储了术语同义词。
名字          类型       空 默认值 描述
tsid          serial      No          主键:唯一的术语同义词ID
tid        int, unsigned  No 0       术语的{term_data}.tid
name        varchar(255)    No ''      同义词的名字
 
trigger_assignments(触发器模块)
触发器模块使用这个表将触发器映射到钩子和操作上
名字          类型       空 默认值 描述
hook       varchar(32)     No ''      主键:内部Drupal钩子的名字,将在它上面                                         触发动作;例如,nodeapi。
op         varchar(32)     No ''      主键:钩子的具体操作,将在它上面触发动作;                                       例如,presave
aid        varchar(255)    No ''      主键:动作的{actions}.aid
weight          int         No 0       这个触发器的重量,相对于其它触发器。
 
upload(上传模块)
这个表存储了已上传的文件的信息和表关联。
名字          类型       空 默认值 描述
fid        int, unsigned  No 0       主键:{files}.fid
nid         int, unsigned  No 0       与已上传的文件关联的{node}.nid
vid        int, unsigned  No 0       主键:与已上传的文件关联的{node}.vid
description varchar(255)    No ''      已上传的文件的描述
list    int:tiny, unsigned No 0       文件是否应该可见的列在节点上(1为是,0                                          为否)
weight      int:tiny        No 0       这个已上传的文件的重量,相对于这个节点中                                        的其它已上传的文件
 
url_alias(路径模块)
这个表为Drupal路径存储了一列别名;用户可以访问源路径,也可以访问别名路径。
名字          类型       空 默认值 描述
pid           serial      No          唯一的路径别名标识符。
src        varchar(128)    No ''      这个别名对应的Drupal路径,例如,node/12。
dst         varchar(128)    No ''      这个路径的别名,例如,title-ofthe-story。
language    varchar(12)     No ''      这个别名所属的语言;如果为空,那么别名将                                        可用于未知语言。可以为Drupal路径在每个                                         支持的语言下分别创建一个别名。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录A users(用户模块)

users(用户模块)

这个表存储了用户数据。
名字          类型       空 默认值 描述
uid           serial      No          主键:唯一的用户ID。
name       varchar(60)     No ''      唯一的用户名。
pass        varchar(32)     No ''      用户的密码(MD5哈希)。
mail       varchar(64)     Yes ''      用户的电子邮件地址。
mode        int:tiny        No 0       每个用户的评论显示模式(串列式vs平板                                           式),用于{comment}模块。
sort        int:tiny        Yes 0       每个用户的评论排序(由新到旧vs由旧到                                           新),用于{comment}模块。
threshold  int:tiny        Yes 0       以前由{comment}模块用于每个用户的首选;                                         现在已经不再使用这个字段。
theme       varchar(255)    No ''      用户的默认主题。
signature  varchar(255)    No ''      用户的签名。
created       int         No 0       创建用户时的时间戳。
access            int         No 0       用户上次访问站点的时间戳。
login           int         No 0       用户最后一次登录的时间戳。
status      int:tiny        No 0       用户是激活的(1),或是封锁的(0)。
timezone    varchar(8)      Yes         用户的时区。
language    varchar(12)     No ''      用户的默认语言。
picture     varchar(255)    No ''      指向用户的已上传头像图片的路径
init        varchar(64)     Yes ''      用于最初帐号创建的电子邮件地址。
data        text:big        Yes         一个序列化的数组,里面包含了与用户相关的                                        “名/值”对。在编辑用户期间,任何提交的                                         表单值都将被保存起来,在user_load()期                                         间,将被加载到$user对象中。不推荐使用这                                        个字段,在Drupal将来的版本中,这个字段                                         可能会消失。
 
users_roles(用户模块)
这个表用来将用户映射到角色上
名字          类型       空 默认值 描述
uid        int, unsigned  No 0       主键:用户的{users}.uid
rid        int, unsigned  No 0       主键:角色的{role}.rid
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录A variable

variable

这个表存储了指定的“变量/值”对儿,它们由Drupal核心或者其它模块或主题创建。在每个Drupal请求的开始阶段,所有的变量都会被缓存到内存中,所以开发者不需要考虑这里都存储了什么。
名字          类型       空 默认值 描述
name       varchar(128)    No ''      主键:变量的名字
value       text:big        No          变量的值
 
 
vocabulary(分类模块)
这个表存储了词汇表信息。
名字          类型       空 默认值 描述
vid           serial      No          主键:唯一的词汇表ID
name        varchar(255)    No ''      词汇表的名字
description     text:big    Yes         词汇表的描述
help        varchar(255)    No ''      为词汇表显示的帮助文本
relations int:tiny,unsigned No 0       在词汇表中是否启用关联术语(0表示已禁                                           用,1表示已启用)
hierarchy int:tiny,unsigned No 0       在词汇表中允许的层级类型(0表示已禁用,                                     1表示单层级,2表示多层级)
multiple  int:tiny,unsigned No 0       是否允许将来自于这个词汇表的多个术语同                                      时分配给一个节点(0表示已禁用,1表示已                                         启用)
required  int:tiny,unsigned No 0       对于使用这个词汇表的节点,其术语是不是必                                        须的(0表示已禁用,1表示已启用)
tags      int:tiny,unsigned No 0       是否为词汇表启用自由标签化(0表示已禁                                           用,1表示已启用)
module      varchar(255)    No ''      创建词汇表的模块
weight      int:tiny        No 0       词汇表的重量,相对于其它词汇表
 
vocabulary_node_types(分类模块)
这个表存储了词汇表可用于哪些节点类型
名字          类型       空 默认值 描述
vid        int, unsigned  No 0       主键:词汇表的{vocabulary}.vid
type       varchar(32)     No ''      主键:词汇表可用于的节点类型的                                              {node}.type
 老葛的Drupal培训班 Think in Drupal
 

Drupal版本:

Drupal专业开发指南 附录A watchdog(数据库日志模块)

watchdog(数据库日志模块)

这个表包含了所有系统事件的日志。
名字          类型       空 默认值 描述
wid           serial      No          主键:唯一的看门狗事件ID
uid             int         No 0       触发事件的用户的{users}.uid
type          varchar(16) No ''      日志消息的类型,例如“user”或“page not                                        found
message         text:big    No          将被传递给t()函数的日志消息文本
variables       text:big    No          一个序列化数组,里面包含了匹配消息字符串                                        并传递给t()函数的变量
severity int:tiny, unsigned No 0       事件的严重层次:从0(紧急)到7(调试)
link        varchar(255)    No ''      查看事件结果的链接
location        text        No          事件起源的URL
referer     varchar(128)    No ''      来源页面的URL
hostname    varchar(128)    No ''      触发事件的用户的主机名
timestamp       int         No 0       一个Unix时间戳,事件发生的时间
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录A 网管

网管

一个邮件列表,为维护http://drupal.org上的网站的志愿者服务。
 
CVS申请
用于向第3方资源库提交代码的CVS帐号,不是每个人都可以拥有的。为了得到一个帐号,一个新手必须在这个邮件列表中发送一个申请,并说明需要账号的原因。资深开发者将会对申请进行审核,他会根据申请者的条件批准或者拒绝本次申请。参看http://drupal.org/cvs-account
 
咨询
    Drupal顾问和Drupal服务和主机提供商,可以在这里讨论相关的Drupal付费服务。
 
用户小组和兴趣小组
本地或者区域用户小组,以及Drupal特定方面的兴趣小组,可以使用http://groups.drupal.org来组织活动和进行交流。该站点使用了organic groups(组织小组)模块来提供相应的功能。其中专门针对初级开发者的小组为Drupal Dojo小组(http://groups.drupal.org/drupal-dojo)。这个小组目的是为初级开发者讲授Drupal技巧,并让其逐步成为高手。
 
网络即时聊天系统(Internet Relay Chat(IRC)
    IRC主要用作Drupal开发者的实时聊天工具,人们在上面可以相互帮助和讨论和Drupal相关的问题。并非所有的开发者都使用IRC,而且有些人认为IRC上提供的帮助是无济于事的,因为对问题的回答不能被其他开发者看到,所以他们一般在http://drupal.org的论坛或者邮件列表中讨论相关问题。然而,当需要快速的针对特定话题进行交流时,使用IRC还是很方便的。这里有一些与Drupal相关的重要频道。偶尔在准备发布一个新版本时为了代码审查(就是对Drupal的某一部分代码进行赶工)和bug的快速修复,也会建立一些特定频道的。
     IRC上也有特定的习惯.为了避免闹出笑话,使用前,先看看“如何高效的使用IRC” 一文http://drupal.org/node/108355
    这里所列的所有频道都可以在freenode网络上使用(http://freenode.net)。
 
#drupal-support
    这个频道主要是用于志愿者回答Drupal相关问题,这里的问题大多是Drupal的使用问题,也就是通过后台的管理界面可以解决的,或者关于某个模块具有什么功能的。
 
#drupal-themes
    这个是讨论Drupal主题化的频道,包括主题的创建,修改和发布。
 
#drupal-ecommerce
讨论如何将Drupal应用到电子商务的频道(参看http://drupal.org/project/ecommerce)。
 
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录A 附加的中英文对照:

Parenthese: 括号(又名圆括号):()

curly brackets: 大括号(又名花括号):{}
bold italic type:粗斜体
schema module:模式模块
Null:空
Mask:掩码
access rule: 访问规则
accesslog:访问日志
Referrer:来源
Timestamp:时间戳
Trigger:触发器
Action:动作
nondefault actions:非默认动作
aggregator:聚合器
feed:种子(注,这个在简体中文包中没有翻译,RSS feed,我记得是翻译成RSS种子的,feed在这里应该有确切的译法,但是上网不便,没有具体查证)
bridge table:关联表
feed item: 种子项目(来自于种子的项目)
authentication:认证
distributed authentication:分布式认证
Binary:二进位的
Book:手册
book outline:手册大纲
top-level page:顶层页面
vancode:前卫代码(比如01.00.01.02,用于方便的判断评论的位置),van有“前卫,前驱,先锋”,这里使用了前卫,因为在判断评论的位置时,首先用到这一代码,原因就是效率高。参看评论模块,以及vancode2int这个函数。
Thread:串列(这个译法源于繁体中文,Thread的翻译在多个地方有多个不同的翻译,有的干脆没有翻译,有的翻译了,但不贴切。Thread的意思有“线, 细丝, 线索, 思路”的意思,评论与评论之间存在回复的关系,使用这种关系,把评论串到了一起,所以把它翻译成串列还是很恰当了。简体中文,翻译中文有很多译法,这个译法是其中的一个,很不显眼,但是最贴切,另外Thread在计算机中还有“线程”的意思,注意这里蕴含了“线”,“串”的含义,在中文的论坛里面,有“同主题阅读”这一词语,但是同主题和Thread在概念上不吻合,翻译成“同主题”,翻译错了,当然“同主题”有相仿的含义,“同主题”对应的是主题、话题,也就是Drupal节点,而这里Thread针对的是“评论”)
flood event:注入事件,在Drupal核心中有一个flood表,flood在繁体中文包中没有翻译,在简体中文包中翻译为了“闸门”,flood在这里有闸门的含义,这个译法还是很贴切的,闸门是与水有关的。我查了flood_register_event函数,以及调用这个函数的函数,基本上弄清了这个字的含义,就是当一件事情发生时,也就是一个事件完成以后,需要对这个事件注册一下,或者说是登记一下,有两种方式,一种是watchdog,另一种就是flood,如果把事件理解为水滴的话,那么flood数据库表就是一个瓶子,同一个人在一段时间内只能向瓶子中注入有限个水滴。“注入事件”的就是把“事件”注入到flood表中。Flood的中文意思有“洪水, 水灾,淹没, 使泛滥, 注满, 充满”,这里使用了“注满”,“注入”,注入事件,直到注满为止。flood表是一个核心数据库表,在Drupal核心中,只有联系模块用到了这个表,其它的模块,应该也可以使用这个表,来向里面注入其它的事件。
HTML corrector:HTML校正者
plural index:复数索引
Plural formula:复数公式(Drupal在处理多语言时,会遇到复数的形式(英语中),这样就用到了复数索引,复数公式这两个概念,通过复数索引和复数公式,就可以把复数形式的英语翻译成其它语言正确形式,举例来说,“1 item”,“@count items”,翻译成简体中文就是“@count个项目”,中文是没有复数形式的,简体中文的复数索引为2,复数公式为($n!=1)。另外“复数索引”和“复数公式”这两个翻译源自于简体和繁体中文包,里面还有其它的译法,这里使用了这两个)
materialized path:具体路径(菜单系统中的p1,p2,p3,。。。。。p9,这个和前面的前卫代码(vancode)类似,用来存储导航到这个链接所需的具体路径,我们拿    admin/settings/actions为例,它的mlid为23,plid为18,深度为3;
    admin/settings的mlid为18,plid为2,深度为2;
    而admin的mlid为2,plid为0,深度为1;
这里面,admin/settings/actions的p1为2,p2为18,p3为23,p4为0,。。。
        admin/settings的p1为2,p2为18,p3为0,。。。
        admin的p1为2,p2为0,。。。
router path:路由路径
translation set:翻译集(包含一个源节点,和各语言下的翻译节点)
Each node access node:每个节点访问节点(没有搞明白什么意思,领域/授权中的)
realm/grant:领域/授权(源自简体/繁体中文包,我以前把grant翻译为“许可”的)
machine-readable name : 机器可读的名字
human-readable name :用户可读的名字
poll:民意测验(poll的中文意思有:选举之投票, 民意测验。作为动词时,它有投票的意思,在简体中文和繁体中文中,它都被翻译为了“投票”,民意测验其实就是投票。但是与poll一起的还有vote这个词,vote作为名字,动词也都被翻译为了“投票”,如果把poll也翻译为投票的话,那么一句话中,同时出现poll和vote的时候就会引起歧义。比如“This table stores per-user votes for each poll”,为了避免这种情形,我把poll翻译为了“民意测验”,不过觉得美中不足的是这里是4个字,有点长,能缩写为了2个字的就好了,比如简称为“民调”)
vote:投票
voter:投票者
item:项目(item在简/繁体中文包中有的地方也被翻译为“条目”,建议将其统一为“项目”,项目由多个意思,而不仅仅是工程项目的意思,另外就是把entry翻译为“条目”)
entry:条目
store:存储(建议把这个词统一翻译为“存储”,把save翻译为“保存”,尽管这两个使近义词)
schema:模式(这个是我从百度知道查出来的,与简体/繁体中文包中的不同)。
Dblog:数据库日志(database log)
Watchdog:看门狗

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录B Drupal资源

对于Drupal开发者来说,有很多可以利用的资源。我们在这里列出了最常用的一些。

 
代码
    下面是一些Drupal代码资源。
 
Drupal CVS
我们在第21章已经讲解了如何访问Drupal CVS的核心资源库和第3方资源库。然而,前面的URL给出了一个方便的web,接口用来浏览有哪些资源库可用。这里有一点值得一提,那就是能够便捷的对代码进行着色来显示不同之处。
 
Drupal API 参考
注释来自于Drupal函数,同样,在Drupal CVS的第3方资源库的contributions/docs/developer区域也存在相关的文档,你可以在http://api.drupal.org查看该文档。在这里,代码是可被搜索的,相互关联的,而且根据主版本进行了归类。这个站点具有很高的价值,你得花点时间好好的看看这个站点。事实上,你也可以建立自己的本地版本;相关的指南参看http://drupal.org/node/26669
 
安全顾问
在这个页面,通过e-mail或者一个RSS种子就可以获得安全顾问服务。当你登录到http://drupal.org时,你可以从这个页面订阅顾问服务。
 
更新模块
当一个新的Drupal版本修改了一个API时,修改的技术性建议将会在这里进行说明。这个页面可帮助你,让你的模块与在Drupal代码库所做的的修改保持同步。
 
更新主题
这个页面与前面的“更新模块”页面提供类似的信息,但它是针对主题的。它对于将主题从Drupal的一个版本更新到另一个版本非常重要。
 
参考手册
在线参考手册位于http://drupal.org/handbooks,人们正不断的在对其进行更新和改进中。许多HOWTO的文档也发布在这里,提供了逐步的指南。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 附录B 论坛

论坛

论坛位于http://drupal.org/forum,它是一个能够获取Drupal相关帮助的极好的去处。你遇到的问题通常前人也遇到过,他们把相关经验已发布到了论坛上,你只需要去搜索就可以了。如果你的问题,很明显可能是第3方模块的bug的话,那么你最好将其发布到该模块的问题列表中,因为该模块的开发者更容易看到你的问题,如果放到论坛里的话,那么他们可能就注意不到.
 
提示 试着使用一个搜索引擎专门搜索位于http://drupal.org上的结果。例如,在Google的一个查询“installation profiles site:drupal.org”将得到在http://drupal.org中的所有包含“installation profiles”的页面。
 
邮件列表
    有许多特定主题的邮件列表可以使用。关于这些邮件列表的订阅管理和归档文件可在http://lists.drupal.org/listinfo中找到。
 
开发
    这一列表是为Drupal开发者服务的,它包括一般的讨论,比如Drupal的未来方向,开发相关的问题,以及不同方式的优缺点。如果要做一个大的修改,通常也在这里进行讨论。这是个热门的列表。
 
文档
这一列表是为文档编写者服务的。为Drupal的代码和功能编写文档是个永久性的任务。编写文档对于Drupal的成功非常重要,关于文档改进和修改的讨论常在这里进行。初级开发者在这个列表中花些时间,对其工作会很有帮助。
 
drupal-cvs
    这一列表包含了所有的CVS提交消息。它对于追踪CVS资源库都发生了哪些变化非常有用。另外的选择包括,在http://drupal.org/cvs?rss=true&nid=3060用RSS种子订阅Drupal的核心资源库的变化,以及访问http://drupal.org/cvs查看关于最近提交的列表。
 
基础设施
这是为那些维护Drupal基础设施的志愿者服务的,Drupal的相关项目都运行在这些基础设施上,包括web服务器,数据库服务器,CVS资源库,邮件列表,等等。
 
支持
尽管可在http://drupal.org的论坛得到大多数支持,但是还是有一个提供同样服务的邮件列表,人们在这里可以相关帮助。
 
主题
用于主题开发者讨论主题相关的问题。
 
翻译
一个邮件列表,用于讨论将Drupal接口翻译成其它语言。
 老葛的Drupal培训班 Think in Drupal

Drupal版本: