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

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

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

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

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

Drupal版本:

创建相应的文件

老葛的Drupal培训班 Think in Drupal

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

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

Drupal版本:

实现一个钩子

老葛的Drupal培训班 Think in Drupal

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

Drupal版本:

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

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

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

Drupal版本:

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

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

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

Drupal版本:

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

老葛的Drupal培训班 Think in Drupal

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

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

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

Drupal版本:

添加数据输入表单(1)

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

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

老葛的Drupal培训班 Think in Drupal

Drupal版本:

添加数据输入表单(2)

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

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

Drupal版本:

添加数据输入表单(3)

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

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

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

老葛的Drupal培训班 Think in Drupal

Drupal版本:

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

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

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

Drupal版本:

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

老葛的Drupal培训班 Think in Drupal

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

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

Drupal版本:

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

老葛的Drupal培训班 Think in Drupal

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

Drupal版本:

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

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

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

Drupal版本:

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

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

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

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

老葛的Drupal培训班 Think in Drupal

Drupal版本:

定义你自己的管理部分

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

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

Drupal版本:

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

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

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

Drupal版本:

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

老葛的Drupal培训班 Think in Drupal

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

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

Drupal版本:

验证用户提交的设置

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

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

Drupal版本:

存储设置

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

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

老葛的Drupal培训班 Think in Drupal

Drupal版本:

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

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

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

Drupal版本:

篇外话

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

老葛的Drupal培训班 Think in Drupal

 

Drupal版本:

总结

老葛的Drupal培训班 Think in Drupal

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

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

Drupal版本: