IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  Open source  >

研究 Drupal V6,第 3 部分: 管理 Drupal

构建自定义内容模块

developerWorks
文档选项

未显示需要 JavaScript 的文档选项

英文原文

英文原文


级别: 初级

Martin Streicher, 主编, Linux Magazine

2009 年 10 月 13 日

您已经学习了 Drupal V6 的基础知识,甚至还向一个 Drupal 站点添加了一些模块。在这个 “研究 Drupal V6” 系列的最后一篇文章中,您将学习如何编写和部署一个自定义模块以创建一个小说内容类型。

在当今的世界中,每个事业单位都需要一个 Web 站点。Web 站点同时也是名片、宣传册、公文包、目录、邀请函、年度报告和广告。实际上,如果一个公司没有 URL,在搜索引擎中没有足够的位置,那么它注定会默默无闻。

常用缩略词
  • API:应用程序编程接口
  • HTML:超文本标记语言
  • HTTP:超文本传输协议
  • SQL:结构化查询语言
  • URL:统一资源定位器

然而,并不是所有的 Web 站点都需要相同的特性,而且,运营商的专业技术也不同。因此,一些内容管理系统(CMS)旨在简单易用,而另一些则规模庞大、相当复杂。预算也不一样,某个网站可能资金雄厚,能够购买昂贵的商业应用程序;而另一个则囊中羞涩,对软件的期望值不高。

尽管没有 “正确的解决方案”,只有适合您的环境的解决方案,但许多网站还是选择了相同的软件:Drupal。Drupal 是开源、免费(的确完全免费)使用、易于启动且可以广泛扩展的软件,这使其成为小型、大型、新兴或有抱负的网站的理想选择。目前有数百个(也可能是数千个)模块 或扩展可用于定制 Drupal 站点,如果现有模块不适合,编写一个新模块也并不费事。

本系列的 研究 Drupal V6,第 1 部分:简介 展示了如何在一个类 UNIX® 的系统上安装和启动 Drupal V6 —— 这个流行包的最新版本。研究 Drupal V6,第 2 部分:管理 Drupal V6 展示了如何向一个 Drupal V6 站点添加模块以集成新特性。本文展示如何编写和部署一个自定义模块来创建一个小说内容类型。

与 Drupal 本身一样,自定义模块使用 PHP 语言编写,因此,熟悉这种编程语言、它的工具和库肯定有所裨益。通常,模块也会访问底层数据库,因此,关系数据库和 SQL 方面的经验也会对您有所帮助。

Drupal 模块开发简介

Drupal 内容模块通常扩展标准 Drupal 节点,将补充字段存储在一个单独的表中。例如,一个附件模块可能在它自己的表中记录一个文件的名称、位置和大小,然后将该信息关联到一个故事节点。内容模块还利用 Drupal 用户、角色和权限子系统来实现和执行特权和持久首选项。图 1 展示了内容模块的逻辑结构。


图 1. Drupal 内容模块
Drupal 内容模块

开发一个 Drupal 内容模块需要以下几个步骤:

  1. 命名模块。无论您是否计划分发模块,模块名称必须唯一,以免和其他模块冲突。选择一个简短但描述性强的名称。
  2. 创建一个单独的目录存储模块代码和相关资源。一个模块拥有元数据、一个安装程序、代码和几个模板。所有这些资源都要收集到一个独立的目录中,这也有利于简化分发和安装。
  3. 编写一个 .info 文件来识别模块和模块的目的。这个元数据定义先决条件、适当的模块名称和其他信息。
  4. 编写模块代码,包括模块安装程序,一个数据输入表单和一个呈现模板。
  5. 安装、启用和配置模块。使用 第 2 部分 中介绍的技术安装您的模块。

Drupal 提供一个良好定义的界面来扩展它的核心特性。要创建一个模块,必须执行 Drupal 称其为钩子(hook)的界面。例如,一个钩子提供帮助,另一组钩子使用您的模块需要的表扩大 Drupal 数据库,还有一组钩子将您的模块的特殊数据持久存储于数据库中。与上一版 Drupal(需要开发人员编写 SQL 和 HTML)不同,Drupal V6 提供一些富 API 来定义自定义模式和描述自定义表单。这些 API 使用 PHP 和简单的数据结构。但是,直接访问数据库的钩子仍然使用 SQL 编写。

另外,每个 Drupal 模块都遵循一组严格的编码惯例,以确保一致性并促进代码共享和社区维护。为简便起见,这里故意省略了那些惯例。但有一个例外,不要使用一个结束 ?> 终止模块代码。如果您的 Drupal 日志表明,文本已经在标准 HTTP 头部前发送,则应从您的文件中删除任何前导空白和拖尾 ?>。参见 参考资料 获取完整的代码标准列表。





回页首


构建 Flitter — 一种类似于 Twitter 的内容类型

为方便展示,我们创建一个模块来记录非常短的消息,比如在 Twitter 上交换的那些消息。每条消息应该有一个标题(就像每个 Drupal 节点一样)和一个不超过 140 个字符的简洁的句子。让我们调用模块 Flitter。要实现 Flitter,必须扩展 Drupal 数据库,以便包含一个新的 Flitter 表;访问 Flitter 数据的权限,或 flit;编辑 flit 的表单;操作数据库的代码。

首先,为 Flitter 模块创建一个目录。根据惯例,该模块应该包含在 $DRUPAL_ROOT/sites/all/modules/flitter 中,其中 $DRUPAL_ROOT 是您的 Drupal 站点的根目录,比如 /var/www/drupal(目录 DRUPAL_ROOT/modules 是 Drupal 的核心模块的专用目录)。

$ export DRUPAL_ROOT=/var/www/drupal
$ mkdir -p $DRUPAL_ROOT/sites/all/modules/flitter

第一条命令设置一个帮助变量。mkdir -p 创建 Flitter 目录和所有绝对路径上缺少的中间目录。

其次,在 $DRUPAL_ROOT/sites/all/modules/flitter/flitter.info 中创建模块的元数据文件 flitter.info。这个文件的内容用于在模块管理页面中识别该模块。这个文件是一个标准的 PHP.ini 文件,必须包含以下条目:namedescriptioncorephp

name          = "Flitter" 
description   = "A custom content type to display very brief messages"
core          = 6.x 
php           = 5.1 

namedescription 是不言而喻的,确保用双引号包围它们的值。core 指定兼容的 Drupal 版本号。php 指定需要的 PHP 版本号。后两个值用于防止模块需求和系统功能不匹配。

下面,在 $DRUPAL_ROOT/sites/all/modules/flitter/flitter.module 中创建一个文件来存储模块代码主体。要实现的第一个钩子是 module_help(),其中 module 是您的模块的名称。在本例中,这个钩子是 flitter_help()。这个钩子提供关于 Flitter 模块和它的设计用途的附加信息。

<?php
function flitter_help($path, $arg) { 
  if ($path == 'admin/help#flitter') { 
    $txt = 'A flitter is a very short message. '
      . 'The title should clearly convey the topic, '
      . 'and the message should convey an opinion, status, '
      . 'event, or reference in 140 characters or less. ' ;
    $replace = array(); 
    return '<p>' . t($txt, $replace) . '</p>';
  }
}   

图 2 显示模块安装后这个钩子可能具有的外观。flitter_help() 为该模块的帮助页面提供信息。


图 2. module_help() 为模块的帮助页面提供信息
module_help() 钩子

代码最后一行中使用的函数 t() 由 Drupal 提供。具体而言,t()translate 的简写 — 试图将英语文本 $txt 翻译为用户偏好的语言。这里,t() 试图把英语帮助文本翻译为用户的母语。翻译通过您定义的词典执行。

t() 函数的第二个参数 — $replace — 用于用实际值替换文本中的占位符。本例中没有使用占位符。但是,$txt 也有可能包含一个占位符,比如一个 URL 缩写服务的地址。

$txt = 'A flitter is a very short message. The title should clearly '
  . 'convey the topic, and the message should convey an opinion, status, '
  . 'event, or reference in 140 characters or less. Use !url to shorten ' 
  . 'URLs.';

在上面的代码中,!url 是一个已命名占位符。! 占位符类型用一个字符串直接替换另一个字符串。(还有其他占位符类型可用)。以下调用执行一个实际替换,使用 http://www.bit.ly 替换 !url t($txt, array('!url' => 'http://www.bit.ly/'));

下面继续构建模块。由于 Flitter 创建了一个新的内容类型,下一步必须定义一个新的数据库表来持久存储自定义 Flitter 数据,并将它的字段和一个 Drupal 节点关联起来。让我们将一段 Flitter 数据称为一个 flit

为抽象化底层数据库的具体数据并简化模块开发,Drupal V6 引入了一个 Schema API(参见 参考资料),以便使用纯 PHP 关联数组定义表。根据惯例,新的 Flitter 表的创建和删除(卸载模块时将删除表)使用一组特别命名的钩子在一个特殊的文件 flitter.install 中定义。清单 1 显示了完整的 flitter.install。


清单 1. flitter.install
				
<?php
function flitter_install() {
  drupal_install_schema('flitter');
}

function flitter_uninstall() {
  drupal_uninstall_schema('flitter'); 
}

function flitter_schema() {
  $schema['flitter'] = array(
    'fields'      => array(
      'vid'       => array(
        'type'    => 'int',
        'unsigned'=> TRUE,
        'not null'=> TRUE,
        'default' => 0),
      'nid'       => array(
        'type'    => 'int',
        'unsigned'=> TRUE,
        'not null'=> TRUE,
        'default' => 0),
      'message'   => array(
        'type'    => 'varchar',
        'length'  => 140,
        'not null'=> TRUE,
        'default' => '')),
    'indexes'     => array(
      'nid'       => array('nid')),
    'primary key' => array('vid'));
  
  return $schema;
}

flitter_schema() 的代码应该看起来似曾相识:它与您为定义字段(但这里称为参数)而使用 SQL 编写的代码类似,因此这段代码很容易转换为支持您的 Drupal 网站的任意数据库引擎需要的代码。关联数组 $schema['flitter'] 必须定义 fieldsindexesprimary_key 的值,它们分别对应表的列,加速查询的索引和表的主键。

回想一下,一个核心 Drupal 节点记录一些关键信息,比如一段内容的标题,创建日期,处理情况和状态(发表、草稿等),因此,那些字段在一个 flit 中不需要重复。一个 flit 需要的字段包括它的版本,vid;与该 flit 关联的节点 ID,nid;以及 flit message 本身,定义为一个 140 个字符的字段。

$schema['flitter'] 中的 indexes 定义的值也值得注意:这个索引引用元组 (version ID, node ID),这加速了对 flit 的最近版本的查询。

前一个钩子定义了新的数据结构,但没有描述它与一个节点的关系。要将这个新的 flit 关联到一个节点,必须实现另一个钩子:flitter_node_info()。这个钩子返回一个数组以描述这个 Flitter 内容类型(或者在其他情况下描述模块实现的所有内容类型)。清单 2 展示了 flitter_node_info(),它应该作为在 flitter.module 文件中的另一个函数出现。


清单 2. flitter_node_info() 描述由 Flitter 定义的内容类型
				
function flitter_node_info() {
  return array(
    'flitter' => array(
      'name'        => t('Flitter'),
      'module'      => 'flitter',
      'description' => t('A very brief message to acquaintances.'),
      'has_title'   => TRUE,
      'title_label' => t('What are you doing?'),
      'has_body'    => FALSE));
}

flitter_node_info() 传递的信息出现在多个位置。namedescription 字段出现在 Create Content 页面中,如图 3 所示。


图 3. module_node_info() 描述内容类型的用途
module_node_info() 钩子

has_titlehas_body 控制节点提供的 title 和 body 字段是否分别出现在表单中,以便创建和编辑一个 flit。图 4 显示了生成的表单。由于 has_title 为 true,title 字段显示,其标签由 title_label 定义。因为 has_body 为 false,所以 body 字段隐藏。


图 4. 创建一个新 flit 的表单
创建一个 flit 的表单

flitter_node_info() 只描述标准节点字段在表单中的显示方式。要为 flit 自定义消息添加字段,必须实现另一个钩子:flitter_form()。由于数据输入是任何 CMS 中的基本功能,Drupal 提供一个 Form API(参见 参考资料)来描述表单字段。清单 3 显示了 flitter_form() 钩子。


清单 3. flitter_form() 描述 Flitter 需要的表单
				
function flitter_form(&$node) {
  $type = node_get_types('type', $node);
  if ($type->has_title) {
    $form['title'] = array(
      '#type'         => 'textfield',
      '#title'        => check_plain($type->title_label),
      '#required'     => TRUE,
      '#default_value'=> $node->title);
  }
  
  $form['message'] = array(
    '#type'           => 'textfield',
    '#size'           => 70,
    '#maxlength'      => 140,
    '#title'          => t('Tell us more'),
    '#description'    => t('A short description or elaboration'),
    '#required'       => TRUE,
    '#default_value'  => isset($node->message) ? $node->message : '');
    
  return($form);
}

flitter_form 中的 if 语句可能会让你感到疑惑。难道 title 不是一个 flit 的必要字段吗?根据这个钩子的定义,答案是肯定的。但是,网站管理员可以通过管理工具禁用 title 字段。因此,flit 的表单必须检查 title 字段是否启用,如果没有,就省略它。对于 message 字段则没有这样的测试,因为它是一个 flit 的核心。





回页首


权限和数据库

至此,所有数据收集钩子都已完成,但还需创建 3 组模块钩子:权限、数据库访问和呈现。这些钩子分别在 清单 4清单 5清单 6 中顺序显示。

清单 4 定义 flitter_perm(),它列举模块授予的权限。但是,如果没有配套 flitter_access(),这些权限没有实际意义。flitter_access() 将每个已命名权限映射到一个创建更新删除 操作。


清单 4. flitter_perm() 定义模块中的可用权限
				
function flitter_perm() {
  return array(
    'create flit',
    'edit flits',
    'delete flits' );
}

function flitter_access($op, $node, $account) {
  switch ($op) {
    case 'create':
      return user_access('create flit', $account);
    case 'update':
      return user_access('edit flits', $account);
    case 'delete':
      return user_access('delete flits', $account);
  }
}

图 5 显示权限管理页面上的 Flitter 的权限。回想一下,我们曾在 第 1 部分 的安装和配置过程中创建了两个组:Management 和 Staff。


图 5. Flitter 模块定义自己的权限
Flitter 模块定义自己的权限

清单 5 包括与 Drupal 数据库交互需要的所有钩子。这些钩子 —— flitter_insert()flitter_update()flitter_delete()flitter_nodeap1() —— 使用 SQL 影响数据库,SQL 的语法特定于您使用的数据库引擎。在本例中,SQL 的语法是 MySQL。针对其他关系数据库的语法也类似。


清单 5:添加、修改和删除 flit 的钩子
				
function flitter_insert($node) {
  db_query(
    'INSERT INTO {flitter} (vid, nid, message) '
      ."VALUES ('%d', '%d', '%s')",
      $node->vid, $node->nid, $node->message);
}

function flitter_update($node) {
  if ($node->revision) {
    flitter_insert($node);
  } else {
    db_query(
      "UPDATE {flitter} SET message = '%s' WHERE vid=%d", 
        $node->message,
        $node->vid);
  }
}

function flitter_delete($node) {
  db_query("DELETE FROM {flitter} WHERE nid=%d", $node->nid);
}

function flitter_nodeap1(&$node, $op, $note, $page) {
  if ($op == 'delete revision') {
    db_query("DELETE FROM {flitter} WHERE vid=%d", $node->vid);
  }
}

前三个钩子几乎不需要解释,但有一个例外:flitter_update() 中的 if 语句确定版本控制是否启用。如果启用,该方法将创建一个新记录来捕获修订,而不是更新一个现有记录,那会覆盖该信息。flitter_nodeap1() 的名称有些奇怪,它是一个特殊的钩子,用于删除一个特定修订。钩子 flitter_delete() 用于删除一个特定节点的所有版本。

清单 6 定义检索、准备和显示一个 flit 需要的 3 个钩子。flitter_load() 从数据库检索一个特定修订。flitter_view() 主要用作样板文件,将节点转变为可查看的 HTML。flitter_theme() 命名一个模板来呈现这个 flit 并指定呈现被发送的 message 字段。同样,这个 flit 的关联节点中的标题字段和其他字段将被自动发送。


清单 6. 提取、准备并显示 flit 的钩子
				
function flitter_load($node) {
  $r = db_query("SELECT message FROM {flitter} WHERE vid=%d", $node->vid);
  return db_fetch_object($r);
}

function flitter_view($node, $note = FALSE, $page = FALSE) {
  $node = node_prepare($node, $note);
  $message = check_markup($node->message);
  $node->content['flitter_info'] = array(
    '#value' => theme('flitter_info', $message));
  
  return($node);
}

function flitter_theme() {
  return array( 
    'flitter_info' => array( 
      'template' => 'flitter_info', 
      'arguments' => array( 
        'message' => NULL)));
}

清单 7 是这个迷局的最后一部分。它是 flitter_info.tpl.php 的代码清单,这是用于呈现每个 flit 的模板。与这里显示的其他文件一样,这个模板也存储在模块目录中。


清单 7. flitter_info.tpl.php
				
<div class="biography_info"> 
	<h2>
	<?php print t('Message'); ?>:</h2> 
	<?php print $message; ?>
</div>

图 6 展示了一个在主页上呈现的 flit。该 flit 的正下方是一个传统 Drupal 故事,该故事展示了如何在相同的页面上呈现异构内容。


图 6. 在主页上呈现并带有一个故事的 Flit
在主页上呈现并带有一个故事的 Flit

在结束时,您的模块目录应该(至少)有以下 4 个文件:

$ ls flitter
flitter.info		flitter.module
flitter.install		flitter_info.tpl.php

现在可以启用这个模块,如图 7 所示。当您启用模块时,它的安装程序添加 Flitter 表,使这个内容类型可用于合格的用户,并将 flit 呈现到规范。如果您选择禁用并卸载模块,所有 flit 和关联节点将被清除。


图 7. 像其他模块一样运行和打开 Flitter
运行 Flitter





回页首


结束语

希望您现在对 Drupal 有了充分了解,能够评估它对您的价值。如果您有许多需求而某些需求不能直接通过 Drupal 内核满足,那么可以浏览众多可用模块的列表,查看您想要的特性是否已经被实现。很可能某人已经构建了一个解决方案。如果没有合适的模块可用,您可以使用 Drupal 模块 API 创建一个新特性,这只需很少时间。您甚至可以考虑将 Drupal 作为可扩展的新开发的基础。



参考资料

学习
  • 访问 Drupal.org 查找文档、插件模块和其他资源,并在 Drupal 社区中与其他用户交流。

  • 查看 Theme Garden,它提供一些 Drupal 主题。另外,Drupal Modules 是一个可搜索的数据库,您可以从中查找可用的 Drupal 模块。

  • 查看 Drupal 编码标准 的完整列表。

  • 要收听针对软件开发人员的有趣访谈和讨论,请查看 developerWorks podcasts

  • 随时关注 developerWorks 技术活动网络广播

  • 查看最近将在全球各地举办的面向 IBM 开源开发人员的会议、展览、网络广播和其他 活动

  • 访问 developerWorks Open source 专区,获得丰富的 how-to 信息、工具和项目更新,帮助您用开放源码技术进行开发,并与 IBM 产品结合使用。

  • 查看免费的 developerWorks 演示中心,观看并了解 IBM 及开源技术和产品功能。


获得产品和技术

讨论


关于作者

Martin Streicher 是 Linux Magazine 的主编。Martin 获得 Purdue University 的计算机科学硕士学位,从 1986 年起他一直从事 UNIX 类系统的编程工作,使用的编程语言包括 Pascal、C、Perl、Java 和 Ruby(最近)。




对本文的评价










回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款