级别: 初级 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 内容模块需要以下几个步骤:
- 命名模块。无论您是否计划分发模块,模块名称必须唯一,以免和其他模块冲突。选择一个简短但描述性强的名称。
- 创建一个单独的目录存储模块代码和相关资源。一个模块拥有元数据、一个安装程序、代码和几个模板。所有这些资源都要收集到一个独立的目录中,这也有利于简化分发和安装。
- 编写一个 .info 文件来识别模块和模块的目的。这个元数据定义先决条件、适当的模块名称和其他信息。
- 编写模块代码,包括模块安装程序,一个数据输入表单和一个呈现模板。
- 安装、启用和配置模块。使用 第 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 文件,必须包含以下条目:name、description、core 和 php。
name = "Flitter"
description = "A custom content type to display very brief messages"
core = 6.x
php = 5.1
|
name 和 description 是不言而喻的,确保用双引号包围它们的值。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() 为模块的帮助页面提供信息
代码最后一行中使用的函数 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'] 必须定义 fields、indexes 和 primary_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() 传递的信息出现在多个位置。name 和 description 字段出现在 Create Content 页面中,如图 3 所示。
图 3. module_node_info() 描述内容类型的用途
has_title 和 has_body 控制节点提供的 title 和 body 字段是否分别出现在表单中,以便创建和编辑一个 flit。图 4 显示了生成的表单。由于 has_title 为 true,title 字段显示,其标签由 title_label 定义。因为 has_body 为 false,所以 body 字段隐藏。
图 4. 创建一个新 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 模块定义自己的权限
清单 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
在结束时,您的模块目录应该(至少)有以下 4 个文件:
$ ls flitter
flitter.info flitter.module
flitter.install flitter_info.tpl.php
|
现在可以启用这个模块,如图 7 所示。当您启用模块时,它的安装程序添加 Flitter 表,使这个内容类型可用于合格的用户,并将 flit 呈现到规范。如果您选择禁用并卸载模块,所有 flit 和关联节点将被清除。
图 7. 像其他模块一样运行和打开 Flitter
结束语
希望您现在对 Drupal 有了充分了解,能够评估它对您的价值。如果您有许多需求而某些需求不能直接通过 Drupal 内核满足,那么可以浏览众多可用模块的列表,查看您想要的特性是否已经被实现。很可能某人已经构建了一个解决方案。如果没有合适的模块可用,您可以使用 Drupal 模块 API 创建一个新特性,这只需很少时间。您甚至可以考虑将 Drupal 作为可扩展的新开发的基础。
参考资料 学习
获得产品和技术
讨论
关于作者  | |  | Martin Streicher 是 Linux Magazine 的主编。Martin 获得 Purdue University 的计算机科学硕士学位,从 1986 年起他一直从事 UNIX 类系统的编程工作,使用的编程语言包括 Pascal、C、Perl、Java 和 Ruby(最近)。
|
对本文的评价
|