你在这里

Chapter 02: Writing a Module

 模块是构成Drupal的基础建筑构件(积木),模块也是为由现成的Drupal版本,又称Drupal核心功能提供扩展的机制,我经常对那些不熟悉Drupal的人说明:Drupal的模块就像乐高积木。他们遵循事先定义的方针,完美地融合在一起,并且,利用模块的组合,你可以创建丰富的和复杂的解决方案。
通常有两类Drupal模块,核心的和贡献的。核心模块是Drupal搭载的诸如poll、menu、taxonomy、search、feed、聚合和论坛。贡献模块是其它所有由社区创建的,从drupal核心功能扩展的那些模块。有大量的贡献模块可以从http://drupal.org/project/modules下载,功能跨越一切,从简单地显示一个日期到复杂的电子商务前端。
在本章,我将展示如何毫无基础地创建一个自定义模块,你建筑一个模块时,你将学到模块应该遵循的标准。我需要一个显示目标,让我们聚焦真实世界问题:评注。当我们查看Drupal站点的页面时,你可能想写一个关于此页面的注解,我们可以使用Drupal的评论特性来完成这些,但是评论通常对任何浏览用户都是可见的,另一方面,我只想让注解只有它的所有者可见。

创建文件

我们首先要做的东西就是为模块选择一个名字。“annotate”看起来不错,简短且能说明问题。下一步,我需要个地方放置模块,贡献、和自定义模块存放于/sites/all/modules目录中,每一个模块都有一个自己的目录,和模块名相同。
注意:Drupal核心模块存在/modules目录中,以保护你的贡献和自定义模块在更新的时候不被覆盖和删除。

你可能希望创建一个/sites/all/modules/custom目录来存放你的任一个白手起家做的模块,使某些人看着更明白,理解那些模块是从drupal.org下载的贡献模块,那些是你自己为这个站点创建的模块。下一步我需要在/sites/all/modules/custom目录内创建一个annotate目录,来此存放与annotate模块关联的所有文件。
为新模块创建的第一个文件时annotate.info文件,drupal7中的每一个模块都应当包含一个.info文件,名称必须匹配模块名称。对于annotate模块,为了使Drupal7识别模块所必需的基本信息是:

name = Annotate
description = "Allows users to annotate nodes."
package = Pro Drupal Development
core = 7.x
files[] = annotate.module
files[] = annotate.install
files[] = annotate.admin.inc
configure = admin/config/content/annotate/settings

文件的这个结构适用于所有Drupal7的模块,name项用来在模块配置页面显示名字;description项用来在模块配置页面显示模块的描述;package项定义模块分配到那些包或组中,在模块配置页面,模块按照包来分组显示;core字段定义模块为哪个Drupal版本所写;php项定义这个模块需要的php版本;files项是个数组,包含一些与此模块关联的文件的名字,在此,与annotate模块关联的文件是annotate.module和annotate.install文件。
我们能在前面列表中指派一些可选的值,这有个例子,一个模块需要PHP5.2、安装时依赖于forum和taxonomy模块才能工作。

name = Forum confusion
description = Randomly reassigns replies to different discussion threads.
core = 7.x
dependencies[] = forum
dependencies[] = taxonomy
files[] = forumconfusion.module
files[] = forumconfusion.install
package = "Evil Bob's Forum BonusPak"
php = 5.2

现在我们准备去建立一个真正的模块,在你的子目录sites/all/modules/custom/annotate中创建一个annotate.module文件,输入PHP标签和一个CVS占位符,紧接着是一个注释:

<?php
/**
* @file
* Lets users add private annotations to nodes
*
* Adds a text field when a node is display
* so that authenticated users may make notes.
*/

首先,注意注释风格,我们用/**开始,随后的每一行,我们用一个缩进一个空格的星号开始,在注释结束时用*/@file符号表示接下来的行是文件做什么的描述,one-line描述常常想api.module那样,Drupal的自动文档抽取并格式它,能找到这个文件时什么的,当你在drupal.org浏览http://api.drupal.org时,在这你能找到drupal提供的每个api的细节文档。我建议你用点时间去看drupal.org的这部分,它对于我们这些模块开发和修改者来说是无价的资源。

后面是个空行,我们增加一个比较长的描述针对那些审查我们代码的程序员(毫无疑问)。注意我们故意不使用关闭标签?>,这在PHP中是可选的,如果包括,文件中尾随的空格可能引发问题(请看http://drupal.org/coding-standards#phptags)。

注意:为什么我们对结构化如此挑剔,因为有时几百个开发者围绕着一个项目一起工作,如果每个人都按照统一的标准工作将节省时间。Drupal编码风格细节能在 Developing for
Drupal Handbook 的Coding standard(( http://drupal.org/coding-standards)节找到。
我们工作的下一步就是定义一些东西像我们能用一个基于web的表单去选择那些节点可以注释。这个用两部完成,首先,我们将定义一个路径是我们能够访问我们的设置,然后,我们将建立设置表单,为制造个路径,我们需要实现一个钩子,就是hook_menu。

实现一个钩子

Drupal建立在钩子(有时叫回调)系统之上,在执行的过程中,Drupal询问模块是否要做什么事情,例如,当一个节点被从数据库载入并在显示到页面之前,Drupal检查所有已经激活的模块,看看它们是否有实现的hook_node_load()函数,如果有,Drupal在节点被渲染到页面之前执行模块的钩子。我们来看看在annotate模块中它是如何气作用的。
我们的第一个钩子是实现hook_menu()函数,我们用这个函数在站点的管理菜单增加两个项目。我们在主admin/config菜单增加一个项目“annotate”,并在“annotate”项下增加一个子菜单项,叫“settings”,当我们点击它时将打开annotate配置页面。我们菜单项是由键和值组成的数组,值描述Drupal当路径被请求时应该做什么。我们将在第4章“Drupal菜单系统”中讲述细节。我们将hook_menu命名为“annotate_menu” -- 用模块名替换“hook”,这种规则对所有模块来说都是一致 -- 总是用你的模块名字替换单词“hook”。

将这些加到我们的模块

/**
 * Implementation of hook_menu().
 */
function annotate_menu() {
  $items['admin/config/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/config/annotate/settings'] = array(
    'title' => 'Annotation Settings',
    'description' => 'Change how annotations behave.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('annotate_admin_setings'),
    'access arguments' => array('administer site configration'),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'annotate.admin.inc',
  );
  return $items;
}

此时不要过多担心细节,这段代码说:“当用户进入http://example.com/?q=admin/config/annotate/settings时,调用函数drupal_get_form(),并且传给它表单ID annotate_admin_settings,寻找在文件annotate.admin.inc中描述的函数,只有有访问权限的用户才能看见这个菜单项” 。当Drupal完成对所有模块的菜单项询问后,请求路径时将调用一个恰当的函数生成一个菜单项。

注意:如果你对钩子机制驱动函数感兴趣,请看module_invoke_all()函数,定义在includes/module.inc(http://api.drupal.org/api/function/module_invoke_all/7)中。
你应该看到现在问什么我们叫它hook_menu()或菜单钩子(the menu hook)。

Tip: Drupal的钩子的变造几乎涉及各个方面,一个Drupal提供的钩子的完整列表和使用方法能够在Drupal API文档站点(http://api.drupal.org/api/group/hooks/7)找到。

增加模块特定设置

Drupal有多种节点类型(在用户接口中叫内容类型),如articles和basic pages。我们想限定注释只能在默写节点类型使用,要做到这一点,我需要创建一个页面,在这个页面我们将告诉模块那些内容类型才是我们希望可注释的,在这个页面,我们将展现一组复选框,每个现存内容类型对应一个,这将让最终用户决定哪些内用类型可以注释(见图2-1)。这样的页面时管理页面,组成它的代码只有在需要时才载入和解析,因此,我们将代码放到一个单独的文件,而不放在每个web请求都要载入和运行的annotate.module文件中,随后,我们告诉Drupal去在文件annotate.admin.inc中寻找设置表单,我们建立sites/all/modules/annotate/annotate.admin.inc并加入如下代码:

<?php

/**
* @file
* Administration page callbacks for the annotate modules
*/

/**
* Form builder. Configure annotations
*
* @ingroup forms
* @see system_settings_form()
*/
function annote_admin_settings() {
  // Get an array of node types with internal names as key adn
  // "friendly names" as values. E.g.,
  // array('page' => 'Basic Page', 'article' => 'Articles')

  $types = node_type_get_types();
  foreach ($types as $node_type) {
    $options[$node_type->type] = $node_type->name;
  }
  $form['annotate_node_types'] = array(
    '#type' => 'checkboxes',
    '#title' => t('User 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['#submit'][] = 'annotate_admin_settings_submit';
  return system_settings_form($form);
}

表单在Drupal中表现为一个嵌套的树状结构 -- 就是一个数组的数组。这个结构告诉Drupal表单渲染引擎是怎样的一个表单将被展现,为了可读性,我们将数组的每个元素放到一行上,每个表单属性都用井字符号(#)标记,并且用作数组的键。开始我们声明表单元素类型为checkboxes,意味着复选框将被使用一个键化数组建立起来,我们已经在$options变量中得到了键化数组。
我们将选项设置为函数node_type_get_types()的输出,它返回一个对象数组,输出看起来如下:

[article] => stdClass Object (
  [type] => article
  [name] => Article
  [base] => node_content
  [description] => Use articles for time-sensitive content like news, press releasesor blog posts.
  [help] =>
  [has_title] => 1
  [title_label] => Title
  [has_body] => 1
  [body_label] => Body
  [custom] => 1
  [modified] => 1
  [locked] => 0
  [orig_type] => article
)

对象数组的键是节点类型的Drupal内部名称,友好的名称(那些显示给用户看的)包含在对象的那么属性中。
Drupal表单API需要#options要设置成键=>值对数组,foreach循环使用类型属性去创建键的部分,用名字属性去为我命名为$options的新数组创建值的部分,Drupal将为Basic page和article以及其它任何你站点的内容类型生成复选框。
通过定#title属性的值我们为这个表单元素提供标题。

注意:任何返回的文本将显示给用户(例如表单字段的#title和#description属性)。Drupal提供了字符串翻译功能函数,通过为所有字符串运行翻译函数,使你的模块本地化十分简单,我们没有为菜单项使用翻译函数,是因为菜单项自动翻译。

下一个指令#default_value,我们为表单元素定义默认值,因为复选框多个表单元素,#default_value应该是个数组。
#default_value的值值得讨论一下:

variable_get('annotate_node_types', array('page'))

Drupal允许程序员使用一对特殊的函数存取任何的值:variable_get()和variable_set()。值存储在数据库表variables中并且可以随时请求使用。因为这些值是在每次请求时都从数据库中抽取,用这种方式去存储巨量的数据不是一个好主意。注意我们传递给variable_get()的是一个描述我们的值的键(然后我们能取回)和一个默认值,在此,这个值是一个数组,就是允许注释的那个节点类型。我们是将允许注释Basic page内容类型作为默认。

Tip: 使用system_settings_form()时,表单元素的名字(在此是annotate_node_types)必须匹配variable_get()中的键名。

我们提供了一个描述来告诉管理员一点关于应该进入这个字段的信息,我将在第11章论述表单的细节。
下步我将增加代码来处理增加和删除内容类型的annotation字段。如果一个站点管理员选中一个内容类型,我将增加一个annotation字段到这个内容类型;如果管理员决定从一个内容类型中删除annotation字段,我将删除这个字段,我使用Drupal Field API去定义此字段并将此字段关联到一个内容类型。Field API处理所有设置一个字段的相关活动,包括在Drupal数据库建立一个表来存储内容作者提交的值、建立用来收集作者输入的信息的表单项、使字段和内容类型关联、在节点编辑表单和节点页面的字段显示。我将在第8章论述Field API细节。
我要做的第一件事是建立提交例程,它将在管理员提交表单时调用。在这个例程中,模块将检查内容类型的复选框是否被选中,如果没选中,就验证这个内容类型有没有关联的annotation字段,有的话,表示站点管理员想从内容类型中删除这个字段,并且从数据库中删除存在的annotations,如果复选框选中,模块查看这个内容类型上是否存在字段,如果没有,则模块在此内容类型上增加一个annotation字段。

/**
 * Process annotation settings submission
 */
function annotate_admin_settings_submit($form, $form_state) {
  // Loop through each of content type checkboxes shown on the form
  foreach ($form_state['values']['annotate+node_type'] as $key => $value)  {
    // If the check box for a content type is unchecked, loop to see whether
    // this content type has the annotation field attached to it using the
    // field_info_instance function. If it does then we need to remove the
    // annotation field as the adaministrator has unchecked the box
    if (!$value) {
      $instance = field_info_instance('node', 'annotation', $key);
      if (!empty($instance)) {
        field_delete_instance($instance);
        watchdog('Annotation', 'Delete annotation field from content type: %key', array('%key' => $key));
      }
    } else {
      // If the check box for a content type is checked, look to see whether
      // the field is associated with that content type. If not then ad the
      // annotation field to the content type.
      $instance = field_info_instance('node', 'annotation', $key);
      if (empty($instance)) {
        $instance = array(
          'field_name' => 'annotation',
          'entity_type' => 'node',
          'bundle' => $key,
          'label' => t('Annotation'),
          'widget_type' => 'text_textarea_with_summay',
          'settings' => array('display_summary' => TRUE),
          'display' => array(
            'default' => array(
              'type' => 'text_default',
            ),
            'teaser' => array(
              'type' => 'text_summary_or_trimmed',
            ),
          ),
        );
        $instance = field_create_instance($instance);
        watchdog('Annotation', 'Added annotation field t content type: %key', array('%key' => $key));
      }
    }
  } // End foreach loop
}

下一步就是为模块建立.install文件,此文件包含一个或多个在模块安装或卸载时调用的函数。多我们这个模块而言,在它安装时我们想建立annotation字段,它能通过管理员与内容类型关联,如果模块被卸载,我们想从所有内容类型中删除annotation字段并且从数据库中删除这个字段和它的内容。为做到这些,我们在模块目录中建一个名叫annotate.install的文件。
第一个调用的函数是hook_install(),我们命名它为annotate_install() -- 追随Drupal钩子函数命名约定标准,用模块的名字代替单词“hook”,在hook_install函数中,我们将用Field API检查字段是否存在,如果不存在,我们就创建annotation字段。

<?php
/**
 * Implements hook_install()
 */

function annotate_install() {
  // Check to see if annotation field exists
  $field = field_info_field('annotation');

  // if the annotation field does not exist then create it
  if (empty($field)) {
    $field = array(
      'field_name' => 'annotation',
      'type' => 'text_with_summary',
      'entity_type' => array('node'),
      'translatable' => TRUE,
    );
    $field = field_create_field($field);
  }
}

下一步是建立一个卸载函数,实现hook_uninstall。我们建立一个函数annotate_uninstall,使用watchdog去记录一个信息告诉管理员此模块将被卸载。我将使用node_get_types() API函数去获得一个站点所有存在的内容类型的列表,通过对此类型列表的循环,检查在内容类型上是否存在annotation字段,如果有,就删除它,最后,我们删除annotation字段本身。

/**
 * Implements hook_uninstall()
 */
function annotate_uninstall() {
  watchdog('Annotate Module', 'Uninstalling module and deleting field');

  $types = node_type_get_types();
  foreach ($types as $type) {
    annotate_delete_annotaion($type);
  }

  $field = field_info_field('annotation');
  if ($field) {
    field_delete_field('annotation');
  }
}

function annotate_delete_annotation($type) {
  $instance = field_info_instance('node', 'annotation', $type->type);
  if ($instance) {
    field_delete_instance($instance);
  }
}

处理过程的最后一步是更新.module文件,使之包含一个检查,看浏览节点的人是否是作者,如果不是,我们就对齐隐藏用户的注释。我采取一个简单的途径,使用hook_node_load(),这个钩子在节点载入是调用,在hook_node_load()函数中,我将检查,看浏览节点的人是否是作者,如果不是作者,我通过不设置的方式来对其隐藏注释。

/**
 * Implements hook_node_load()
 */

function annotate_node_load($nodes, $types) {
  global $user;

  // Check to see if the person viewing the node is the author, If not then
  // hide the annotation
  foreach ($nodes as $node) {
    if ($user->uid != $node->uid) {
      unset($node->annotation);
    }
  }
}

保存你创建的文件(.info .install .module .admin.inc),在管理菜单进入模块页面,你的模块应该显示在标题为Pro Drupal Development的组中(如果没有,在你的annotate.info和annotate.module文件中双击syntax;确保他们在sites/all/modules/custom目录中),激活你的模块。
现在annotate模块激活啦,导航到admin/config/annotate/settings你应该看到annotate.module的配置页面(图2-1)。

Figure 2-1. The configuration form for  annotate.module is generated for us.

只用了几行代码,我们现在有了功能齐全的模块配置页面,它将自动地保存和记住我们的设置,这让你感觉到Drupal手段的力量。
让我们先测试一下为所有内容类型激活annotations,选中所有复选框,点击保存按钮,下一步建一个basic page节点,滚动到下方就能看见Annotation字段啦(图2-2)。

Figure 2-2. The annotation form as it appears on a Drupal web page

建一个新的节点,输入标题、内容、注释,保存后你应该看到图下图2-3相似的结果。

Figure 2-3.  A node that has an annotation


自此,我们都没有哪怕是含蓄地进行任何数据库操作,你可能惊奇Drupal在哪存储和提取annotation字段的值。Field API做了幕后工作:建立了一个表以控制字段的值,在节点保存和载入时保存和提取这个值。当你调用FIled API函数field_create_field()时,它使用标准的命名规则在数据库中创建一个表field_data_<fieldname>。对于我们的annotations字段,表的名称为field_data_annotations,我们将在第4章论述更多的细节。

创建你自己的管理节

Drupal有许多管理设置的类别,如内容管理和用户管理,它们呈现在配置页面上,如果你需要一个自己的类别,你可以轻松地创建一个自己的类别,在这个例子中,我们建立一个新的叫“Node annotation”的类别。为此,我们使用模块的菜单钩子定义一个新的类别:

/**
 * Implementation of hook_menu()
 */
function annotate_menu() {
  $items['admin/config/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/config/annotate/settings'] = array(
    'title' => 'Annotation Settings',
    'description' => 'Change how annotations behave.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('annotate_admin_setings'),
    'access arguments' => array('administer site configration'),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'annotate.admin.inc',
  );
  return $items;
}

我们的模块的类别在配置页面上看起来如同下图2-4:

Figure 2-4. The link to the annotation module setti ngs now appears as a separate category.


如果你修改了菜单钩子的代码,你必须清楚菜单缓存,为此,你可以truncating数据库表cache_menu,或者点击模块devel.module提供的Rebuild Menu链接或者在卑职页面上点击Clear cache data按钮在它的页面上点击Performance链接。

Tip:开发模块(http://drupal.org/project/devel)是为Drupal开发者定制的模块,让你快速访问许多开发功能,如清除缓存、查看变量、跟踪查询等等,是每个开发者的必备。

我们用两步来建造我们的类别页,首先,我们增加一个描述类别头部的菜单项,这个菜单项有一个唯一的路径(admin/config/annotate),我们声明它应该放置在右列权重为-5,因为这个位置就在“Web Services”列别之上,如图2-3。
第二步告诉Drupal在“Node annotation”类别中建立实际的到annotation设置的链接,我们将原始菜单项的路径设置到admin/config/annotate/settings,Drupal重建菜单树时,他检视路径以去建立父项和子项之间的关系并决定它们,因为admin/config/annotate/settings是admin/config/annotate的子项,它就同样显示。
Drupal只载入需要的文件来完成一个请求,节省了内存的使用,因为我们的页面回调指向的函数不在我们模块空间之内(system_admin_menu_block_page()在system.module内),我需要告诉Drupal载入modules/system/system.admin.inc而不是去试图载入sites/all/modules/custom/annotate/system.admin.inc。我们告诉Drupal从system模块中获取路径并将结果加到我们菜单项的文件路径键上。
当然,这只是个例子,在实际中,你要建个类别要有足够好的原因,没有那个管理员愿意面对众多的类别。 

呈现设置表单

在annotate模块,我们使管理员有了选择哪个节点类型可以支持注释的能力,让我们研究一下它是如何工作的。
当一个管理员想改变annotate模块设置时,我们想显示一个表单给管理员,使他可以选择我们提供的选项,我们设置这个页面的回调指向drupal_get_form()函数,设置页面参数为一个包含于annotate_admin_settings的数组,这意味着当我们浏览http://example.com/?q=admin/config/annotate/settings时,对drupal_get_form('annotate_admin_settings')的调用将被执行,它实质上告诉Drupal去建立有函数annotate_admin_settings()定义的表单。
让我们看一下定义表单的函数,它为每个节点类型定义了一个复选框,现在增加两个选项,此函数在文件sites/all/modules/custom/annotate/annotate.admin.inc中:

/**
 * Form builder. Configure annotations
 *
 * @ingroup forms
 * @see system_settings_form()
 */
function annote_admin_settings() {
  // Get an array of node types with internal names as key adn
  // "friendly names" as values. E.g.,
  // array('page' => 'Basic Page', 'article' => 'Articles')

  $types = node_type_get_types();
  $foreach ($types as $node_type) {
    $options[$node_type->type] = $node_type->name;
  }
  $form['annotate_node_types'] = array(
    '#type' => 'checkboxes',
    '#title' => t('User may annotate these content types'),
    '#options' => $options,
    '#default_value' => variable_get('annotate_node_types', array('page')),
    '#description' => t('A text field will be available on these content types to make user-specific notes.'),
  );
 
  $form['annotate_deletion'] = array(
    '#type' => 'radios',
    '#title' => t('Annotations will be deleted'),
    '#description' => t('Select a method for deleting annotations.'),
    '#options' => array(
      t('Never'),
      t('Randomly'),
      t('After 30 days')
    ),
    '#default_value' => variable_get('annotate_deletion', 0) //default to Never
  );
  $form['annotate_limit_per_node'] = array(
    '#type' => 'textfield',
    '#title' => t('Annotations per node'),
    '#description' => t('Enter the maximum number of annotations allowed per node (0 for no limit).'),
    '#default_value' => variable_get('annotate_limit_per_node', 1),
    '#size' => 3
  );

  $form['#submit'][] = 'annotate_admin_settings_submit';
  return system_settings_form($form);
}

我们增加一个单选按钮去选择何时删除注释以及一个文本域去输入一个节点允许输入几个注释,(增强功能的实现留给你做练习),不是管理我们自己的表单的处理,我们调用system_settings_form()去让系统模块增加一些按钮到表单并校验和提交表单。图2-5展示了选项表单的样子:

 Figure 2-5. Enhanced options form using check box, radio button, and text field options

校验用户提交的设置


如果system_settings_form()为我们关心表单值的保存,我们怎么能检查“Annotations per node”域输入的是真正的数字?我们只需要增加一个检查看看这个值是否是数字的函数(annotate_admin_settings_validate($form, $form_state)到sites/all/modules/custom/annotate/annotate.admin.inc文件中并使用它在找到任何问题时设置一个错误。

/**
 * Valitate annotation settings submission.
 */
function annotate_admin_settings_validate($form, &$form_state) {
  $limit = $form_state['value']['annotate_limit_per_node'];
  if (!is_numric($limit)) {
    form_set_error('annotate_limit_per_node', t('Please enter number.'));
  }
}

现在,但Drupal处理表单时,它将回调到annotate_admin_settings_validate()以便校验,如果你判定输入了一个坏值,错误发生我们在出错字段的附近设置一个错误,映射到屏幕上就是一个警告信息,并且出错字段高亮显示。
Drupal怎么知道调用我们的函数?我们命名方式特殊,使用表单定义函数的名字(annotate_admin_settings)加上_validate。Drupal如何决定哪个表单校验函数被调用的全部解释请看第11章。

存储设置

前面的例子中,改变设置并按保存按钮是好使的,本节在下面描述这发生了什么。   

      使用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。

警告:为了这些设置在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')
    )

当我们为此字段设置默认值是,我们使用下面语句,这意味着,使用数组的0号项目就是t('Never')。
'#defual_value' => variable_get('annotate_deletion', 0) //default to Never
     

      用variable_get()抽取存储的值

当你的模块抽取已经存储的设置值是,就要用到variable_get()函数;
// Get stored setting of maximum number of annotations per node.
$max = variable_get('annotate_limit_per_node', 1);
注意这里为variable_get()使用了一个默认值,应对没有存储的值的情况(如管理员还从未浏览过这个设置页面)。

以后的步骤

以后我们要在开源社区共享这个模块,自然地,应该建一个README.txt文件,放到annotate目录,和annotate.info,annotate.module,annotate.admin.inc,and annotate.install等文件在一起,README.txt通常包含模块开发者、怎样安装模块等信息,版权信息不需要,因为所有上传到drupal.org的模块都遵循GPL,drupal.org的打包脚本能自动生成一个README.txt文件,再以后你能将其上传到drupal.org的贡献仓库,并建立一个project页面来保持跟踪社区其他人的反馈。

小结

读完本章后,你能完成下列任务

      + 零基础创建一个Drupal模块
      + 理解怎样钩进Drupal代码技巧
      + 存储和抽取模块设置参数
      + 使用Drupal表单API建立和处理简单的表单
      + 在Drupal的主管理页面内建立新的管理类别
      + 使用复选框、文本域、单选按钮定义一个供管理员选择的表单
      + 校验设置数据并呈现错误信息
      + 理解Drupal怎样利用内建的稳固的变量系统存储和抽取设置值

Comments

增加模块特定设置,内容中的代码错误。

  $foreach ($types as $node_type) {
    $options[$node_type->type] = $node_type->name;
  }

foreach前面 多打了个 $符号。
 

感谢woailuo的回复,已去掉$符号!

Drupal China http://drupalchina.cn

 

foreach ($form_state['values']['annotate+node_type'] as $key => $value)请更正 $form_state['values']['annotate_node_types']

运行时报错如下:

  • Notice: Undefined index: annotate_admin_setings in drupal_retrieve_form() (line 763 of E:\phpweb\drupal\includes\form.inc).
  • Warning: call_user_func_array() expects parameter 1 to be a valid callback, function 'annotate_admin_setings' not found or invalid function name in drupal_retrieve_form() (line 798 of E:\phpweb\drupal\includes\form.inc).

修改annotate.admin.inc中。
修改函数名
annotate_admin_settings => annotate_admin_setings 
annotate_admin_settings_submit => annotate_admin_setings_submit
之后运行正确。

 

越来越喜欢drupal了  也对它得机制非常感兴趣 ,感谢分析入门教程

 

   'entity_type => 'node',  

少了个单引号!!
     

 'entity_type' => 'node',

Drupal China http://drupalchina.cn

function annote_admin_settings(){}
修改为 
function annotate_admin_settings_submit(){}

没怎么明白variable_set()是在什么时候调用的?

我按着文档的指点写了个annotate模块,但是运行到“导航到admin/config/annotate/settings你应该看到annotate.module的配置页面”这一步时。页面是空白的。再回到模块下,发现有个这样的警告:

  • Warning: call_user_func_array() expects parameter 1 to be a valid callback, function 'drupal_get_from' not found or invalid function name in menu_execute_active_handler() (line 517 of D:\wamp\www\drupal7\includes\menu.inc).
  • Warning: call_user_func_array() expects parameter 1 to be a valid callback, function 'drupal_get_from' not found or invalid function name in menu_execute_active_handler() (line 517 of D:\wamp\www\drupal7\includes\menu.inc).

请问这是什么原因啊?

drupal_get_from 应该是 drupal_get_form

试试看,不要轻言放弃!

您好,谢谢您给的回复,我按着上面chapter2的教程些的模块,其中只有写到drupal_get_form,并没有写到drupal_get_from。就算我把drupal_get_form改成drupal_get_from,这个错误还是存在,请问这里的函数究竟是哪一个?而且这个用到的函数需要自己定义吗?谢啦

用来获取表单的,你要根据提示的错误信息进行更改。看您的回复没有正确理解错误提示。

试试看,不要轻言放弃!

 '#default_value' => variable_get('annotate_deletion', 0) //default to Never
 '#default_value' => variable_get('annotate_limit_per_node', 1),

annotate_delete_annotaion($type); => annotate_delete_annotation($type);

卸载函数第四行写错。

annotate_delete_annotaion($type); -》 annotate_delete_annotation($type);

提交处理的代码有些问题修改如下则可以正常运行了。

function annotate_admin_settings_submit($form, $form_state) {
  foreach ($form_state['values']['annotate_node_types'] as $key => $value) {
    if (!$value) {
      //如果用戶沒有勾選任何項目則查找節點內是否有annotation字段,如果有則刪除。
      $instance = field_info_instance('node', 'annotation', $key);
      if (!empty($instance)) {
        field_delete_instance($instance);
        watchdog('Annotate', 'Deleted annotation field from content type: %key', array('%key' => $key));
      }
    }
    else {
      $field = field_info_field('annotation');
      if(empty($field)) {
        $field = array(
          'field_name' => 'annotation',
          'type' => 'text_with_summary',
          'entity_types' => array('node'),
          'translatable' => true,
        );
        field_create_field($field);
      }
      //如果用戶勾選了項目則查找所有節點並檢測是否有annotation字段,如果沒有則添加。
      $instance = field_info_instance('node', 'annotation', $key);
      if (empty($instance)) {
        $instance = array(
          'field_name' => 'annotation',
          'entity_type' => 'node',
          'bundle' => $key,
          'label' => t('Annotation'),
          'widget_type' => 'text_textarea_with_summary',
          'settings' => array('display_summary' => TRUE),
          'display' => array(
            'default' => array(
              'type' => 'text_default',
            ),
            'teaser' => array(
              'type' => 'text_summary_or_trimmed',
            ),
          ),
        );
        field_create_instance($instance);
        watchdog('Annotate', 'Added annotation field from content type: %key', array('%key' => $key));
      }
    }
  }
}

annote_admin_setings方法名写错——>annotate_admin_setings

1,在annotate.admin.inc文件里函数名出线错误,function annote_admin_settings()修改为  function annotate_admin_setings().

2,且在函数function annotate_admin_settings_submit($form, $form_state) { // Loop through each of content type checkboxes shown on the form foreach ($form_state['values']['annotate+node_type'] as $key => $value)

里foreach里面的键值 annotate+node_type改为annotate_node_type,这全是书写错误。

有模块下载就好了,直接复制粘贴但结果还是出错

进入时admin/config/annotate/settings显示

  • Notice: Undefined index: annotate_admin_setings 在 drupal_retrieve_form() (行 806 在 /var/www/html/d7test/includes/form.inc).
  • Warning: call_user_func_array() expects parameter 1 to be a valid callback, function 'annotate_admin_setings' not found or invalid function name 在 drupal_retrieve_form() (行 841 在 /var/www/html/d7test/includes/form.inc).

直接访问http://localhost/drupal/admin/config/annotate/settings 得到如下错误:

( ! ) Fatal error: require_once(): Failed opening required 'C:\wamp\www\drupal/sites/all/modules/custom/annotate/annotate.admin.inc' (include_path='.;C:\php\pear') in C:\wamp\www\drupal\includes\menu.inc on line 525

====================分割线===================================

文件确实在,但让我在意的是上面的错误log里d额路径居然同时存在"/"和"\".求解答 

依旧是同样的错误,找不到路径上的文件,但奇怪的是路径居然还是"/""\"并存。

有尝试在drupal页面中configuration->performance->clear cache。 但没有什么变化

怀疑是没有找到正确的清缓存的方法。依据如下:

    1. 在module中把正在使用的勾打掉(annatate模块)

    2. 把整个annotate文件夹移除,drupal页面上准备清除缓存时出现提示标签有模块missing了

    3. clear cache后依旧会有提示