使用开源软件设计、开发和部署协作型 Web 站点,..
时间:2007-05-25 来源:woaixiang
简介
在本文中,学习如何创建一个非常简单的定制模块,从而在 Web 站点上提供公告。这里提供的信息不应该解释为严格的开发规则,而是应该作为构建自己的定制模块时的起点。在许多情况下,我们会快速介绍主题,并指出提供了详细细节的 Drupal 文档。
我们的虚构企业 International Business Council(IBC)需要显示相关的公告,这些公告是自动发布的,经过指定的时间之后会自动地从 Web 站点中删除。公告需要几个新的数据字段:
* 一个不同于默认摘要的摘要,这个摘要由公告的作者控制(我们以此说明几个 Drupal 特性)
* 发布日期,即这个公告首次出现在 Web 站点上的时间
* 过期日期,即从 Web 站点中删除这个广告的时间
* 还需要让管理人员能够查看任何公告,即使是当前不处于发布日期和过期日期之间的公告。
主页将当前公告显示在主内容区域中,如 图 1 所示。在每个页面上使用一个边栏显示最近的公告。当前用户创建的公告用有颜色的背景突出显示。
图 1. IBC 主页
IBC 主页
开始
在开始之前,我们创建一个文件,其中将包含定义新模块所需的所有代码。然后,修改数据库,让它支持与新模块相关的信息。
创建文件
首先,在 ibc_site 下面创建 modules/announcement 目录。在这个目录中创建 announcement.module 文件,如 图 2 中的 Eclipse 环境所示。通常情况下,模块的名称与它创建的新节点类型相同。这个文件将包含本文描述的所有公告代码。
注意: Drupal 当前的最佳实践是,将所有非核心模块放在相关域的站点目录中。所以在我们的示例中,将 announcement 目录放在前面文章创建的 sites/drupal.development 目录中会更好。将定制模块与核心发布版分隔开可以简化对核心 Drupal 代码的更新过程。
另外,在 Drupal 4.7 中,可以使用正式的 .install 文件为模块准备所需的任何数据库表和数据。可以不使用 announcement.sql 文件,而是使用 PHP 在 announcement 模块目录中的 announcement.install 文件中执行 SQL 操作。可以在 http://drupal.org/node/51220 进一步了解这个特性。
感谢 Boris 指出了这一点。关于这种方法的更多细节,请参见 Boris 的 blog。
图 2. Eclipse 中的 Navigator 视图
Eclipse 中的 Navigator 视图
修改数据库
节点所表示的所有数据都存储在 node 表中,在 4.7 版本中还存储在 node_revisions 表中。在修改节点时,修订被存储在 node_revisions 表中,包括标题、摘要和主体。因为现有的 Drupal 表不支持新模块的需求,我们创建一个 announcement 表来存储公告特有的信息。为了进一步控制摘要,我们把自己的摘要包含在这个新表中。首先创建这个表,以后将编写在发生特定事件时访问这个表的函数。
清单 1 显示创建 announcement 表的命令,这个表通过节点 ID nid 链接到 node 表。这个表中新的列是 abstract、publish_date 和 expiration_date。可以在 SQL 命令行上或者通过 MySQL 查询浏览器在数据库中创建这个新表。我们将这个数据库命令保存在 announcement 目录中的 announcement.sql 文件中(图 2)。这个步骤记录了这个模块的数据库表的情况,而且如果需要的话,可以轻松地更新数据库。
清单 1. 创建 announcement 表的命令
CREATE TABLE announcement (
nid int(10) unsigned NOT NULL default '0',
abstract varchar(255) default '',
publish_date integer NOT NULL default '0',
expiration_date integer NOT NULL default '0',
PRIMARY KEY (nid)
);
开发模块
Drupal 为模块提供的接口是一套称为挂钩(hook) 的函数。在本节中,我们将讲解如何开发支持我们的定制模块所需的几个挂钩。这些是使我们的模块能够开始工作的基本函数;我们只实现了 Drupal 挂钩 中的一部分。
hook_settings
创建这个模块的属性,管理员可以修改这些属性。
hook_help
提供在界面中多个地方出现的文档。
hook_perm
定义信息访问的权限类别。
hook_access
定义不同操作和用户的访问权限。
hook_menu
设置在处理访问时要调用的 URL 路径和函数。
hook_link
定义可以添加到站点各个地方的链接。
hook_block
定义来自这个模块的一个信息区块。
hook_form
定义在添加和编辑这个节点时使用的界面组件。
hook_validate
在将输入存储到数据库中之前,对用户的输入进行检验。
hook_submit
在检验之后,但在更新数据库之前,修改节点。
hook_load
从数据库装载额外的节点信息。
hook_insert
首次保存额外的节点信息。
hook_update
在节点已经存在的情况下,保存额外的节点信息。
hook_delete
当删除节点时,删除额外的节点信息。
hook_cron
按照管理员的定义,执行预定的操作。
hook_search
定义对这个节点的信息的定制搜索。
hook_nodeapi
让其他模块对节点进行操作。
hook_node_info
确定模块的节点类型的名称和属性。
hook_settings
settings 挂钩可以在模块中添加属性,管理员可以控制这些属性并在显示这个模块时使用它们。对于我们的 announcement 模块,希望限制在边栏上显示的公告数量。block 挂钩 将生成显示的公告元素。但是,我们可以让管理员有能力通过管理员用户界面设置在边栏上显示的公告数量。可以用以下相对 URL 在界面中访问这些模块特有的属性:
admin/settings/<module_name>
清单 2 显示 announcement_settings 挂钩的实现。
清单 2. Announcement_settings 挂钩的实现
function announcement_settings() {
$form = array();
$form['announcement_block_max_list_count'] = array(
'#type' => 'textfield',
'#title' => t('Maximum number of block announcements'),
'#default_value' => variable_get('announcement_block_max_list_count', 3),
'#description' => t('The maximum number of items listed in the announcement block'),
'#required' => FALSE,
'#weight' => 0
);
return $form;
}
清单 2 所示的 announcement_settings 挂钩定义一个界面元素,管理员可以使用它指定这个属性的值。表单数组的索引是变量名;例如 announcement_block_max_list_count。在这个索引位置上存储的数组组件定义如何构造管理员的用户界面。
当构造要显示的公告边栏时,可以访问这个值。在构建边栏时,通过使用 清单 3 中的代码片段,获得在 settings 挂钩中定义的持久化变量。
清单 3. 获得在 settings 挂钩中设置的变量
$items = variable_get('announcement_block_max_list_count', 3);
hook_help
help 挂钩提供一个设置文档的位置,当管理员或用户与系统进行交互时将显示这些文档。显示文档的一种情况是当管理员在管理/模块页面中启用或禁用一个模块时。图 3 显示管理员屏幕上启用这个模块的行,其中突出显示了 announcement 模块。
图 3. 管理员页面上的帮助文档
管理员页面上的帮助文档
与之相似,当用户使用 create content 链接添加一个新公告时,他们会看到对要添加的节点的描述,如 图 4 所示。
图 4. 对可以添加的节点类型的帮助描述
对可以添加的节点类型的帮助描述
清单 4 显示 announcement 模块的 help 挂钩实现,这个实现会生成这两个描述。也可以生成其他帮助描述。
清单 4. announcement_help 挂钩实现
function announcement_help($section) {
switch ($section) {
case 'admin/modules#description':
return t('Enables the creation of announcement pages ' .
'that are presented on the home page.');
case 'node/add#announcement':
return t('An Announcement. Use this page to add an announcement page.');
}
}
hook_perm
perm 挂钩描述可以分配给每个角色的访问特权。可以使用任意字符串来描述与应用程序相关的操作。在这里,我们希望区分创建公告和编辑(或删除)公告操作,如 清单 5 所示。然后, hook_access 函数可以使用这些权限控制对内容的访问。
清单 5. Announcement_perm 挂钩实现
function announcement_perm() {
return array('create announcement', 'edit announcement');
}
默认的 Drupal 角色是 anonymous 用户和 authenticated 用户。管理员可以通过路径 admin/access/control 在界面中创建其他角色。这些角色以及上面定义的权限组成权限控制台的基础,如 图 5 所示。这个复选框阵列使管理员可以为系统中的每个角色启用或禁用权限。图中突出显示了 announcement 模块,在这里可以看到 清单 5 中的 hook_perm 中定义的两个字符串。只有具有 administrator 或 operations 角色的用户能够创建或编辑公告。
图 5. 管理员用来分配权限的界面
用来向角色分配模块权限的用户界面
hook_access
每个模块都可以限制对它表示的数据的访问。access 挂钩使模块作者能够控制访问。实际上,会调用 user_access 函数来检查当前用户是否具有给定的访问权限。在 清单 6 中,我们在 user_access 函数中引用 announcement_perm 函数中定义的权限。
传递给 access 挂钩函数的参数是要执行的操作(例如,创建、查看、更新或删除)和操作所针对的节点。函数返回一个布尔值,表示当前用户是否有权在指定节点上执行此操作。
在这个实现中,只有获得 “create announcement” 权限的用户才能创建公告。具有 “access content” 权限的用户(即经过身份验证的用户)可以查看公告。如果发出请求的用户是内容的所有者,或者被显式授予了编辑公告的权限,那么可以执行更新和删除操作。
在安装 Drupal 时创建的第一个注册用户(用户 ID 为 1)具有根特权,他可以编辑和修改系统中的任何数据。
清单 6. Announcement_access 挂钩实现
function announcement_access($op, $node) {
global $user;
if ($op == 'create') {
return user_access('create announcement');
}
else if ($op == 'view') {
return user_access('access content');
}
else if ($op == 'update' || $op == 'delete') {
if($user->uid == $node->uid || user_access('edit announcement')) {
return true;
}
else {
return false;
}
}
else {
return false;
}
}
hook_menu
menu 挂钩函数定义 Drupal 如何响应 URL。这个函数为特定的 URL 和菜单条目定义回调。在引用特定节点时,Drupal 构造 URL 的标准方法是将节点 ID 放在操作前面。按照这种格式,需要为我们的模块指定的 URL 包括:
/announcements
/announcements/add
/announcements/<id>/view
/announcements/<id>/edit
/announcements/<id>/delete
其中的 <id> 是要查看、编辑或删除的节点的 ID。我们的 announcement_menu 函数定义见 清单 7。
清单 7. Announcement_menu 挂钩实现
function announcement_menu($may_cache) {
$items = array();
if ($may_cache) {
$items[] = array('path' => 'announcements/add',
'title' => t('Add a new Announcement'),
'access' => node_access('create', 'announcement'),
'type' => MENU_CALLBACK,
'callback arguments' => array('announcement'),
'callback' => 'node_add');
$items[] = array('path' => 'announcements',
'title' => t('Announcements'),
'access' => user_access('access content'),
'type' => MENU_CALLBACK,
'callback' => 'announcement_all');
}
else {
if(is_numeric(arg(1))) {
$node = node_load(arg(1));
$items[] = array('path' => 'announcements/' . arg(1),
'title' => t('View an Announcement'),
'access' => node_access('view', $node),
'type' => MENU_CALLBACK,
'callback' => 'node_page');
$items[] = array('path' => 'announcements/' . arg(1) . '/view',
'title' => t('View an Announcement'),
'access' => node_access('view', $node),
'type' => MENU_CALLBACK,
'callback' => 'node_page');
$items[] = array('path' => 'announcements/' . arg(1) . '/edit',
'title' => t('Edit an Announcement'),
'access' => node_access('edit', $node),
'type' => MENU_CALLBACK,
'callback' => 'node_page');
$items[] = array('path' => 'announcements/' . arg(1) . '/delete',
'access' => node_access('delete', $node),
'type' => MENU_CALLBACK,
'callback' => 'node_delete_confirm');
}
}
return $items;
}
这个函数返回一个数组的数组,其中每个数组包含:
path
要匹配的 URL
title
这个菜单条目的标题,当鼠标停留在这个菜单条目上面时会显示这个标题
callback
当访问这个 URL 时调用的函数
type
菜单条目的类型
access
这个菜单条目的访问权限
可以使用缓存提高生产性 Web 站点的效率。在这里,可以使用 $may_cache 条件控制是否缓存菜单定义。但是,在开发期间,如果对信息进行缓存,它们就不能反映代码中的修改。可缓存条目的路径中不应该包含变量,比如 arg(1)。
hook_link
我们希望遵守的基本原则之一是,把操作放在接近被操作条目的地方。对于公告,我们将添加、编辑、删除和评论操作的链接放在公告标题的旁边。当然,只有在当前用户具有适当的访问特权的情况下,才应该显示这些操作链接。操作链接主题化为一种小的红色字体,如 图 6 所示。
图 6. 公告标题旁边的操作链接
公告标题旁边的操作链接
link 挂钩提供了处理链接的机制。清单 8 显示这个挂钩的实现。它允许我们根据访问特权创建适当的链接,然后用适当的标记对这些链接进行主题化。这就允许 CSS 将文本的样式设置为小的红色字体。comment 模块也可以通过提供 “Add Comment” 操作添加链接。
在 清单 8 中的 l(t('Edit')...) 序列中,我们使用两个实用程序函数支持链接的生成。t 函数首先将 'Edit' 字符串转换为当前地区,然后 l 函数将这个字符串格式化为适当的 Drupal 内部链接。通过调用 theme('links', $links) 在主题引擎中对链接进行主题化,然后将它映射到模板变量 $links。
清单 8. Announcement_link 挂钩实现
function announcement_link($type, $node = NULL, $teaser = FALSE) {
global $user;
$links = array();
if($type == 'node' && $node->type == 'announcement') {
if (node_access('create', 'announcement')) {
$links[] = l(t('Add'), "node/add/announcement",
array('title' => t('Add a new announcement')));
}
if (node_access('update', $node)) {
$links[] = l(t('Edit'), "announcements/$node->nid/edit",
array('title' => t('Edit Announcement ') . $node->title));
$links[] = l(t('Delete'), "announcements/$node->nid/delete",
array('title' => t('Delete Announcement ') . $node->title));
}
}
return $links;
}
hook_block
公告显示在主页的中间以及每个页面的导航边栏中,如 图 7 所示。边栏内容由 block 挂钩启用,这允许模块提供可以显示在页面上任何地方的内容。边栏常常显示完整内容的大纲。注意,当前用户的公告的突出显示在边栏和主内容区域中是一致的。 图 7 显示一位管理员看到的边栏,可以看到管理员登录之后可以使用的几个 Resources。对于一般用户,是不会显示这些的。
图 7. 使用 block 挂钩在边栏中显示的公告
在边栏中列出的公告
定义这个挂钩之后,管理员可以通过用户界面指定区块信息的位置,如 图 8 所示。这里启用了公告,并在右边栏中给予权值 -10。这个数字越小,内容在边栏中出现的位置就越高。-10 这个值确保公告是右边栏中出现的第一个信息区块。
图 8. 用来指定区块位置的管理员界面
用来指定区块位置的界面
清单 9 显示 announcement 模块的 block 挂钩实现。这个函数可以构造多个区块。Drupal 使用 $delta 参数为 $block 数组中定义的区块设置索引。函数测试的第一个条件是 list 操作。图 8 所示的管理员界面中要使用这些信息。
第二个操作是 view,它收集适当的公告以便显示在右边栏中使用的信息区块中。它使用 announcement_settings 挂钩中定义的公告设置 announcement_block_max_list_count 来决定要显示多少个公告。view 操作使用 announcement_block_list 模板文件(announcement_block_list.tpl.php)对公告进行主题化。(公告的主题化将在以后的一篇文章中讨论。)
清单 9. Announcement_block 挂钩实现
function announcement_block($op = 'list', $delta = 0, $edit = array()) {
global $user;
if ($op == 'list') {
$blocks[0]['info'] = t('Recently updated announcements');
return $blocks;
}
else if ($op == 'view') {
$block = array();
$output = '';
switch ($delta) {
case 0:
$now = time();
if (user_access('access content')) {
$q = 'SELECT N.uid,N.nid,N.title,A.publish_date,N.status '.
'FROM {node} N JOIN {announcement} A USING(nid) '.
"WHERE N.type='announcement' ".
'AND N.status = 1 '.
'AND A.publish_date < ' . $now . ' '.
'AND A.expiration_date > ' . $now . ' '.
'ORDER BY A.publish_date DESC ';
$items = variable_get('announcement_block_max_list_count', 3);
if ($items) { $q .= "LIMIT 0,$items"; }
$announcements = db_query($q);
$announcement_items = array();
while (db_num_rows($announcements) > 0 and $announcement =
db_fetch_object($announcements)) {
$announcement_items[] = $announcement;
}
}
$block['subject'] = t('Announcements');
$block['content'] = theme('announcement_block_list',$announcement_items);
break;
}
return $block;
}
}
hook_form
调用 form 挂钩来生成添加或编辑节点内容所需的用户界面。这个挂钩返回一个数组的数组,其中包含用户需要编辑的每段内容。公告节点通过 图 9 所示的界面进行编辑。我们为标题、发布日期和过期日期、摘要和主体提供了输入组件。在每个表单元素下面提供了简短的描述,向用户说明此信息是什么以及将会 如何使用它。
图 9. 公告节点的编辑表单
公告节点的编辑表单
为了生成对公告的摘要进行编辑的表单元素,我们创建一个数组,见 清单 10。
清单 10. 生成编辑公告摘要所需的文本区域
$form['abstract'] = array(
'#type' => 'textarea',
'#title' => t('Abstract'),
'#default_value' => $node->abstract,
'#rows' => 3,
'#description' => t('Short summary of the full announcement'),
'#required' => TRUE,
'#weight' => 9
);
数组的索引 abstract 是元素的名称。以 $node->abstract 的形式访问节点数据结构中的值。这个元素的细节包括:
type
组件的类型是 textarea(这会影响可用的其他属性)。
title
经过转换函数 “t” 转换的标题。
default_value
在组件最初显示时使用的值,例如当前值。
rows
文本区域显示的行数。
description
在界面中组件下面显示的文本。
required
这个输入字段是否是必须填写的。
weight
影响组件的排列次序:数字越小,在界面中出现的位置就越高。
Publication 定义为一个字段集,其中包含发布日期和过期日期。清单 11 显示完整的 announcement_form 函数,这个函数生成编辑公告节点所用的界面。如果没有提供发布日期和过期日期的话,前两个 if 子句设置合理的默认值。我们使用 publication 数组中的 #prefix 和 #suffix 元素插入额外的 HTML 标记,以便简化日期的 CSS 主题化。$form 数组的字符串索引用来间接引用节点中的特定值。
清单 11. 完整的 announcement_form
function announcement_form(&$node) {
if ($node->expiration_date == NULL) {
$node->expiration_date = time() + (365 * 86400);
}
if ($node->publish_date == NULL) {
$node->publish_date = time();
}
$form['title'] = array('#type' => 'textfield',
'#title' => t('Title'),
'#default_value' => $node->title,
'#description' => t('Title of the announcement'),
'#required' => TRUE,
'#weight' => 1
);
$form['publication'] = array('#type'=> 'fieldset',
'#collapsible' => FALSE,
'#title' => t('Publication dates'),
'#weight' => 5
);
$form['publication']['publish_date'] = array(
'#prefix' => '<div class="date_widget">',
'#suffix' => '</div>',
'#type' => 'date',
'#title' => t('Publication date'),
'#default_value' => _announcement_unixtime2drupaldate($node->publish_date)
);
$form['publication']['expiration_date'] = array(
'#prefix' => '<div class="date_widget">',
'#suffix' => '</div>',
'#type' => 'date',
'#title' => t('Expiration date'),
'#default_value' => _announcement_unixtime2drupaldate($node->expiration_date)
);
$form['abstract'] = array('#type' => 'textarea',
'#title' => t('Abstract'),
'#default_value' => $node->abstract,
'#rows' => 3,
'#description' => t('Short summary of the full announcement'),
'#required' => TRUE,
'#weight' => 9
);
$form['body'] = array('#type' => 'textarea',
'#title' => t('Body'),
'#default_value' => $node->body,
'#description' => t('Full content for the announcement which ' .
'is shown with the abstract on the details page'),
'#required' => TRUE,
'#weight' => 10
);
return $form;
}
从 Drupal 4.6 到 4.7 最大的变化之一是 form 挂钩的实现。Drupal Web 站点上有许多相关文档,包括:
* forms API Quickstart Guide
* forms API Reference
* detailed example
hook_validate
在编辑过程结束时,在存储节点之前触发 validate 挂钩。这可以用来在将数据存储进数据库中之前对数据进行检验。对于公告,我们利用这个挂钩确保发布日期在过期日期之前。更具交互性的实现可以使用客户端脚 本约束两个字段的关系,从而避免发生这种无效的情况。如果希望在存储节点之前对它进行额外的修改,那么使用 submit 挂钩。
清单 12 显示 validate 挂钩的实现。首先将日期转换为可以进行比较的整数。如果出现用户需要解决的问题,那么使用 form_set_error 函数,这个函数会使用 Drupal 的错误处理机制。在清单 12 中,这个函数的第一个参数是 publish_date,它引用 form 挂钩中使用的表单数组名称(清单 11),并在界面中突出显示这个元素。
清单 12. Announcement_validate 挂钩
function announcement_validate($node) {
if ($node) {
$publish_date =
_announcement_drupaldate2unixtime($node->publish_date);
$expiration_date =
_announcement_drupaldate2unixtime($node->expiration_date);
if ($publish_date >= $expiration_date) {
form_set_error('publish_date',
t('The publish date of an announcement must be before its expiration date.'));
}
}
}
hook_submit
节点经过检验阶段之后,调用 submit 挂钩,在实际更新数据库之前可以在这里对节点进行额外的修改。在 清单 13 中,submit 挂钩更新节点中的发布和过期日期,然后根据当前日期修改节点的状态。如果当前日期在发布和过期日期之间,就将状态设置为 1,否则将它设置为 0。
清单 13. Announcement_submit 挂钩
function announcement_submit(&$node) {
$node->publish_date =
_announcement_drupaldate2unixtime($node->publish_date);
$node->expiration_date =
_announcement_drupaldate2unixtime($node->expiration_date);
$now = time();
if ($now >= $node->publish_date &&
$now < $node->expiration_date) {
$node->status = 1;
}
else {
$node->status = 0;
}
}
数据库挂钩
在环境中发生与数据库进行交互的各种事件时,Drupal 会触发一个挂钩函数。重要的事件包括装载、插入、更新和删除。请查阅关于这些 挂钩事件 的更多信息。
在后面的一篇文章中,将详细讨论 MySQL 和数据库抽象层。
Drupal 有一个 数据库抽象层,在代码中的许多地方都要使用它。在数据库挂钩中,使用以 db_ 开头的函数访问这个数据库抽象层。
hook_load
当从数据库装载 announcement 类型的节点时,会自动地调用 load 挂钩。这个函数允许定制的模块从数据库装载任何额外的内容,从而使节点的内容完整。这个函数的返回值是包含额外内容的数组,这些内容会被合并进节点数据结 构中。在我们的示例中,需要从新表中装载三个数据条目 —— 摘要、发布日期和过期日期。清单 14 显示了为 announcement 节点装载额外信息的代码。
清单 14. 用于装载 announcement 类型的节点的 Announcement_load 挂钩
function announcement_load(&$node) {
$additions = db_fetch_object(db_query('SELECT * FROM {announcement} ' .
'WHERE nid = %d', $node->nid));
return $additions;
}
hook_insert
在 Web 站点上创建 announcement 节点时,会自动地调用 insert 挂钩,见 清单 15。这个挂钩使新模块有机会在创建一个节点时在数据库中存储额外的信息。对于 announcement 模块,我们要在 announcement 表创建一个新记录。传递给这个函数的节点对象包含来自输入表单的所有数据。发布日期和过期日期作为一个包含月、日和年的数组返回。本地函数 _announcement_drupaldate2unixtime 对这些日期进行转换。根据约定,所有本地模块函数的名称都以下划线(“_”)开头,后面跟着模块名称,比如 announcement。然后,调用数据库抽象层,将新行插入 announcement 表。$node->nid 是主键,它将 announcement 表链接到 node 表。
清单 15. Announcement_insert 挂钩以及用来将公告添加进数据库中的支持函数
function _announcement_drupaldate2unixtime($drupal_date) {
$year = $drupal_date["year"];
$month = $drupal_date["month"];
$day = $drupal_date["day"];
return mktime(0,0,0, (int)$month, (int)$day, (int)$year);
}
function announcement_insert($node) {
$publish_date = _announcement_drupaldate2unixtime($node->publish_date);
$expiration_date = _announcement_drupaldate2unixtime($node->expiration_date);
db_query("INSERT INTO {announcement} (nid, abstract, publish_date, expiration_date) ".
"VALUES (%d, '%s', '%d', '%d')",
$node->nid, $node->abstract, $publish_date, $expiration_date);
}
hook_update
当节点已经在数据库中存在而用户要编辑它时,会调用 update 挂钩(清单 16)。这个挂钩与 insert 挂钩相似,但发出的数据库命令是 UPDATE。
清单 16. 用来修改现有 announcement 节点的 Announcement_update 挂钩
function announcement_update($node) {
$publish_date = _announcement_drupaldate2unixtime($node->publish_date);
$expiration_date = _announcement_drupaldate2unixtime($node->expiration_date);
db_query("UPDATE {announcement} SET abstract='%s', publish_date = '%s', " .
"expiration_date = '%s' WHERE nid = %d",
$node->abstract, $publish_date, $expiration_date, $node->nid);
}
hook_delete
最后,当用户删除一个 announcement 节点时,会调用 delete 挂钩(清单 17)。这使模块有机会从数据库中的其他表中删除任何额外信息。在这里,我们要根据 nid 删除 announcement 表中与这个节点相关联的一行。
清单 17. 用来删除 announcement 节点的 Announcement_delete 挂钩
function announcement_delete($node) {
db_query('DELETE FROM {announcement} WHERE nid = %d', $node->nid);
}
hook_cron
cron 挂钩允许模块对任务进行调度,让任务以特定的时间间隔运行。站点管理员可以通过一个 cron 作业(对 http://<sitename.com>/cron.php 执行 HTTP GET)设置时间间隔。这会调用所有模块上定义的 cron 挂钩。在 Administer > Settings(例如 /admin/settings)下面的管理员界面的 cron 作业部分中,可以看到 cron 作业的状态。
announcement 模块依靠发布日期和过期日期来判断一个公告是否应该显示。但是,如果公告的节点状态没有设置为 0,就仍然可以通过标准的节点机制(/node/id/view)显示它。我们使用 cron 挂钩在所有已经过期的 announcement 节点上设置状态标志。清单 18 显示 announcement_cron 函数的实现,它首先在数据库中查询那些已经超过过期日期的公告,然后将这些节点的节点状态设置为 0。
清单 18. Announcement_cron 挂钩实现
function announcement_cron() {
$queryResult = db_query("UPDATE {node} AS n INNER JOIN {announcement} AS a " .
"ON n.nid = a.nid SET n.status = 0 WHERE n.type='announcement' " .
"AND n.status = 1 AND a.expiration_date < %d", time());
}
hook_search
search 挂钩使模块能够在它创建的节点上执行关键字搜索,从而扩展搜索页面的功能。首先,需要通过 administer > modules 页面启用 search 模块。这使我们能够使用 administer > block 页面将 search 区块包含在页眉中。通过使用 search 挂钩,当进行简单搜索时搜索页面上会出现另一个选项卡。可以使用这个搜索表单在自己的模块创建的节点中寻找关键字。
search 模块使用 cron 为节点中找到的数据建立索引表,这使 Drupal 能够提供针对节点内容的全文搜索。
对于 announcement 模块,我们希望搜索引擎能够搜索 announcement 表中新的摘要字段。还希望改变默认的搜索操作,从而显示这个摘要字段而不是默认内容,而且不希望用单独的选项卡显示搜索界面。幸运的是,search 挂钩的另一个替代品可以满足我们的需要。
hook_nodeapi
为了确保默认的搜索表单可以在 announcement 表中的摘要字段中寻找关键字,我们实现了 nodeapi 挂钩函数。这允许我们在更新索引期间包含这个字段。清单 19 显示了 nodeapi 挂钩的实现。
清单 19. Announcement_nodeapi 挂钩实现
function announcement_nodeapi(&$node, $op) {
switch ($op) {
case 'update index':
if ($node->type == 'announcement') {
$text = '';
$q = db_query('SELECT a.abstract FROM node n LEFT JOIN announcement a ' .
'ON n.nid = a.nid WHERE n.nid = %d', $node->nid);
if ($r = db_fetch_object($q)) {
$text = $r->abstract;
}
return $text;
}
}
}
在这个函数中,检查 update index 操作,这表示 Drupal 正在收集额外的数据,然后将在数据库中编制索引。如果要编制索引的节点是 announcement 类型的,就从 announcement 表中提取并返回相关联的摘要字段值。
既然 Drupal 可以对公告摘要编制索引了,就需要在默认的搜索页面结果上显示与关键字搜索匹配的信息。我们实现这一特性的方法是,使用 phptemplate_search_item 函数覆盖 search 模块中的 theme_search_item 函数,见 清单 20。因为这个函数是一种全局的主题修改,我们将它放在主题目录中的 template.php 文件中。
清单 20. phptemplate_search_item 函数
function phptemplate_search_item($item, $type) {
return _phptemplate_callback('search_item',
array('node' => $item), 'search_item-' . strtolower($item['type']));
}
在这个函数中,使用 _phptemplate_callback 函数将搜索条目的主题化与一个模板文件关联起来。phptemplate 引擎允许使用 node.tpl.php 和 node-<node-type>.tpl.php 模板文件来定制节点的显示方式,但是我们不这么做,而是使用这个函数连接 search_item.tpl.php 和 search_item-<node-type>.tpl.php 模板,见 清单 21。
现在,可以为搜索条目的默认外观提供一个模板,它实质上是 search.module 文件中的 theme_search_item 函数中原来的主题化搜索条目的修改版本。这种技术可以应用于任何主题化的实体。
清单 21. search_item.tpl.php 模板
<dt class="title search_item">
<a href="<?php print check_url($node['link']); ?>"><?php print
check_plain($node['title']) ?></a>
</dt>
<?php
$info = array();
if ($node['type']) $info[] = strtolower($node['type']);
if ($node['user']) $info[] = $node['user'];
if ($node['date']) $info[] = format_date($node['date'],'small');
if (is_array($node['extra'])) $info = array_merge($info, $node['extra']);
?>
<dd class="search_item">
<p><?php print $node['snippet']; ?></p>
<p class="search-info"><?php print implode(' - ', $info); ?></p>
</dd>
通过使用 search_item-announcement.tpl.php 模板,可以对 announcement 类型的搜索条目进行主题化,用我们自己构造的摘要字段替换默认的片段。在 清单 22 中,我们使用 search_excerpt 函数突出显示摘要中的关键字。
清单 22. search_item-announcement.tpl.php 模板
<dt class="title search_item_announcement">
<a href="<?php print check_url($node['link']); ?>">
<?php print check_plain($node['title']) ?></a>
</dt>
<?php
$info = array();
if ($node['type']) $info[] = strtolower($node['type']);
if ($node['user']) $info[] = $node['user'];
if ($node['date']) $info[] = format_date($node['date'],'small');
if (is_array($node['extra'])) $info = array_merge($info, $node['extra']);
?>
<dd class="search_item_announcement">
<p><?php print ($node['node']->abstract ? '<p>'.
search_excerpt(search_get_keys(),$node['node']->abstract) .
'</p>' : $node['snippet']); ?></p>
<p class="search-info"><?php print implode(' - ', $info); ?></p>
</dd>
图 10. 搜索结果页面在输出中突出显示搜索词
搜索结果页面在输出中突出显示搜索词
hook_node_info
清单 23 显示 node_info 挂钩函数,它允许节点模块定义多个定制的节点类型。我们通过这个函数让 Drupal 定义 announcement 节点类型。Drupal 需要两个与节点类型相关联的值:适合用户阅读的节点类型名称,这会用在用户界面中;以及基名称,这将作为与这个节点类型相关联的函数的前缀。如果希望添加 另一个节点类型,可以添加另一个数组条目。
清单 23. Announcement_node_info 决定模块的节点类型的名称和属性
function announcement_node_info() {
return array('announcement' => array('name' => 'Announcement',
'base' => 'announcement'));
}
对模块的输出进行主题化
模块文件提供几个主题函数,可以使用它们在不同的上下文中显示模块的信息。对于大多数情况,我们发现按照以下方式考虑这些上下文是有帮助的:
* 详细的布局,其中显示给定节点的所有信息
* 简略表示或总结,其中只显示重要部分,从而提供对节点的概述
* 区块,这提供比较小的节点信息形式,通常嵌入左或右边栏中
对于 announcement 模块,我们为每个上下文使用一个主题函数:
* 公告页面使用 theme_announcement 函数创建详细的布局。
* 站点的 front 页面或主页使用 theme_announcement_compact 函数创建公告的列表。
* 所有页面上的右边栏中的公告区块使用 theme_announcement_block_list 函数。
正如 第 5 部分:Drupal 入门 所解释的,主题函数可以用来创建 announcement 模块输出的默认外观,而不管选择的主题引擎是哪种。
因为我们使用 phptemplate 引擎,可以使用 phptemplate_announcement、phptemplate_announcement_compact 和 phptemplate_announcenemt_block_list 函数覆盖这些默认的主题函数,见 清单 24。我们发现,最好是将结构和样式定义与模块的逻辑分隔开,并使用 _phptemplate_callback 函数将模板文件与每个上下文关联起来。
清单 24. 默认主题函数和覆盖它们的 PHPTemplate 引擎函数
function theme_announcement($announcement) {
// Put your default theme for the announcement detail here
return '';
}
function theme_announcement_compact($announcement) {
// Put your default theme for the announcement summary here
return '';
}
function theme_announcement_block_list($announcement_list) {
// Put your default theme for the announcement block here
return '';
}
function phptemplate_announcement($announcement) {
return _theme_phptemplate_announcement($announcement, 'announcement');
}
function phptemplate_announcement_compact($announcement) {
return _theme_phptemplate_announcement($announcement, 'announcement_compact');
}
function phptemplate_announcement_block_list($announcement_list) {
global $user;
return _phptemplate_callback('announcement_block_list',
array('announcements' => $announcement_list,
'user' => $user));
}
function _theme_phptemplate_announcement($announcement, $announcement_template) {
$expired = FALSE;
if ($announcement->expiration_date < time()) {
$expired = TRUE;
}
$variables = array(
'title' => $announcement->title,
'body' => $announcement->body,
'links' => $announcement->links ?
theme('links', $announcement->links) : '',
'abstract' => $announcement->abstract,
'published' => format_date($announcement->publish_date,'custom','j M, Y'),
'expires' => format_date($announcement->expiration_date,'custom','j M, Y'),
'expired' => $expired,
'node' => $announcement
);
return _phptemplate_callback($announcement_template, $variables);
}
注意辅助函数 _theme_phptemplate_announcement,它提供一种对传递给模板文件的变量进行准备的通用方法。
我们已经描述了 announcement 定制模块的一个简单主题化示例。在后续的一篇文章中,将详细讨论节点的样式化。
结束语
在本文中,您了解了一个简单的定制模块(announcement 模块)的实现。这个模块提供的公告会根据发布日期和过期日期自动地显示在 Web 站点上或隐藏。公告显示在主页的主区域和所有其他页面的边栏中。我们使用许多核心函数(即挂钩)提供了一个有效的模块。
在本 系列 中的后续文章中,将详细讨论这个虚构的 Web 站点的样式,以及到 SQL 和数据库抽象层的接口。
在本文中,学习如何创建一个非常简单的定制模块,从而在 Web 站点上提供公告。这里提供的信息不应该解释为严格的开发规则,而是应该作为构建自己的定制模块时的起点。在许多情况下,我们会快速介绍主题,并指出提供了详细细节的 Drupal 文档。
我们的虚构企业 International Business Council(IBC)需要显示相关的公告,这些公告是自动发布的,经过指定的时间之后会自动地从 Web 站点中删除。公告需要几个新的数据字段:
* 一个不同于默认摘要的摘要,这个摘要由公告的作者控制(我们以此说明几个 Drupal 特性)
* 发布日期,即这个公告首次出现在 Web 站点上的时间
* 过期日期,即从 Web 站点中删除这个广告的时间
* 还需要让管理人员能够查看任何公告,即使是当前不处于发布日期和过期日期之间的公告。
主页将当前公告显示在主内容区域中,如 图 1 所示。在每个页面上使用一个边栏显示最近的公告。当前用户创建的公告用有颜色的背景突出显示。
图 1. IBC 主页
IBC 主页
开始
在开始之前,我们创建一个文件,其中将包含定义新模块所需的所有代码。然后,修改数据库,让它支持与新模块相关的信息。
创建文件
首先,在 ibc_site 下面创建 modules/announcement 目录。在这个目录中创建 announcement.module 文件,如 图 2 中的 Eclipse 环境所示。通常情况下,模块的名称与它创建的新节点类型相同。这个文件将包含本文描述的所有公告代码。
注意: Drupal 当前的最佳实践是,将所有非核心模块放在相关域的站点目录中。所以在我们的示例中,将 announcement 目录放在前面文章创建的 sites/drupal.development 目录中会更好。将定制模块与核心发布版分隔开可以简化对核心 Drupal 代码的更新过程。
另外,在 Drupal 4.7 中,可以使用正式的 .install 文件为模块准备所需的任何数据库表和数据。可以不使用 announcement.sql 文件,而是使用 PHP 在 announcement 模块目录中的 announcement.install 文件中执行 SQL 操作。可以在 http://drupal.org/node/51220 进一步了解这个特性。
感谢 Boris 指出了这一点。关于这种方法的更多细节,请参见 Boris 的 blog。
图 2. Eclipse 中的 Navigator 视图
Eclipse 中的 Navigator 视图
修改数据库
节点所表示的所有数据都存储在 node 表中,在 4.7 版本中还存储在 node_revisions 表中。在修改节点时,修订被存储在 node_revisions 表中,包括标题、摘要和主体。因为现有的 Drupal 表不支持新模块的需求,我们创建一个 announcement 表来存储公告特有的信息。为了进一步控制摘要,我们把自己的摘要包含在这个新表中。首先创建这个表,以后将编写在发生特定事件时访问这个表的函数。
清单 1 显示创建 announcement 表的命令,这个表通过节点 ID nid 链接到 node 表。这个表中新的列是 abstract、publish_date 和 expiration_date。可以在 SQL 命令行上或者通过 MySQL 查询浏览器在数据库中创建这个新表。我们将这个数据库命令保存在 announcement 目录中的 announcement.sql 文件中(图 2)。这个步骤记录了这个模块的数据库表的情况,而且如果需要的话,可以轻松地更新数据库。
清单 1. 创建 announcement 表的命令
CREATE TABLE announcement (
nid int(10) unsigned NOT NULL default '0',
abstract varchar(255) default '',
publish_date integer NOT NULL default '0',
expiration_date integer NOT NULL default '0',
PRIMARY KEY (nid)
);
开发模块
Drupal 为模块提供的接口是一套称为挂钩(hook) 的函数。在本节中,我们将讲解如何开发支持我们的定制模块所需的几个挂钩。这些是使我们的模块能够开始工作的基本函数;我们只实现了 Drupal 挂钩 中的一部分。
hook_settings
创建这个模块的属性,管理员可以修改这些属性。
hook_help
提供在界面中多个地方出现的文档。
hook_perm
定义信息访问的权限类别。
hook_access
定义不同操作和用户的访问权限。
hook_menu
设置在处理访问时要调用的 URL 路径和函数。
hook_link
定义可以添加到站点各个地方的链接。
hook_block
定义来自这个模块的一个信息区块。
hook_form
定义在添加和编辑这个节点时使用的界面组件。
hook_validate
在将输入存储到数据库中之前,对用户的输入进行检验。
hook_submit
在检验之后,但在更新数据库之前,修改节点。
hook_load
从数据库装载额外的节点信息。
hook_insert
首次保存额外的节点信息。
hook_update
在节点已经存在的情况下,保存额外的节点信息。
hook_delete
当删除节点时,删除额外的节点信息。
hook_cron
按照管理员的定义,执行预定的操作。
hook_search
定义对这个节点的信息的定制搜索。
hook_nodeapi
让其他模块对节点进行操作。
hook_node_info
确定模块的节点类型的名称和属性。
hook_settings
settings 挂钩可以在模块中添加属性,管理员可以控制这些属性并在显示这个模块时使用它们。对于我们的 announcement 模块,希望限制在边栏上显示的公告数量。block 挂钩 将生成显示的公告元素。但是,我们可以让管理员有能力通过管理员用户界面设置在边栏上显示的公告数量。可以用以下相对 URL 在界面中访问这些模块特有的属性:
admin/settings/<module_name>
清单 2 显示 announcement_settings 挂钩的实现。
清单 2. Announcement_settings 挂钩的实现
function announcement_settings() {
$form = array();
$form['announcement_block_max_list_count'] = array(
'#type' => 'textfield',
'#title' => t('Maximum number of block announcements'),
'#default_value' => variable_get('announcement_block_max_list_count', 3),
'#description' => t('The maximum number of items listed in the announcement block'),
'#required' => FALSE,
'#weight' => 0
);
return $form;
}
清单 2 所示的 announcement_settings 挂钩定义一个界面元素,管理员可以使用它指定这个属性的值。表单数组的索引是变量名;例如 announcement_block_max_list_count。在这个索引位置上存储的数组组件定义如何构造管理员的用户界面。
当构造要显示的公告边栏时,可以访问这个值。在构建边栏时,通过使用 清单 3 中的代码片段,获得在 settings 挂钩中定义的持久化变量。
清单 3. 获得在 settings 挂钩中设置的变量
$items = variable_get('announcement_block_max_list_count', 3);
hook_help
help 挂钩提供一个设置文档的位置,当管理员或用户与系统进行交互时将显示这些文档。显示文档的一种情况是当管理员在管理/模块页面中启用或禁用一个模块时。图 3 显示管理员屏幕上启用这个模块的行,其中突出显示了 announcement 模块。
图 3. 管理员页面上的帮助文档
管理员页面上的帮助文档
与之相似,当用户使用 create content 链接添加一个新公告时,他们会看到对要添加的节点的描述,如 图 4 所示。
图 4. 对可以添加的节点类型的帮助描述
对可以添加的节点类型的帮助描述
清单 4 显示 announcement 模块的 help 挂钩实现,这个实现会生成这两个描述。也可以生成其他帮助描述。
清单 4. announcement_help 挂钩实现
function announcement_help($section) {
switch ($section) {
case 'admin/modules#description':
return t('Enables the creation of announcement pages ' .
'that are presented on the home page.');
case 'node/add#announcement':
return t('An Announcement. Use this page to add an announcement page.');
}
}
hook_perm
perm 挂钩描述可以分配给每个角色的访问特权。可以使用任意字符串来描述与应用程序相关的操作。在这里,我们希望区分创建公告和编辑(或删除)公告操作,如 清单 5 所示。然后, hook_access 函数可以使用这些权限控制对内容的访问。
清单 5. Announcement_perm 挂钩实现
function announcement_perm() {
return array('create announcement', 'edit announcement');
}
默认的 Drupal 角色是 anonymous 用户和 authenticated 用户。管理员可以通过路径 admin/access/control 在界面中创建其他角色。这些角色以及上面定义的权限组成权限控制台的基础,如 图 5 所示。这个复选框阵列使管理员可以为系统中的每个角色启用或禁用权限。图中突出显示了 announcement 模块,在这里可以看到 清单 5 中的 hook_perm 中定义的两个字符串。只有具有 administrator 或 operations 角色的用户能够创建或编辑公告。
图 5. 管理员用来分配权限的界面
用来向角色分配模块权限的用户界面
hook_access
每个模块都可以限制对它表示的数据的访问。access 挂钩使模块作者能够控制访问。实际上,会调用 user_access 函数来检查当前用户是否具有给定的访问权限。在 清单 6 中,我们在 user_access 函数中引用 announcement_perm 函数中定义的权限。
传递给 access 挂钩函数的参数是要执行的操作(例如,创建、查看、更新或删除)和操作所针对的节点。函数返回一个布尔值,表示当前用户是否有权在指定节点上执行此操作。
在这个实现中,只有获得 “create announcement” 权限的用户才能创建公告。具有 “access content” 权限的用户(即经过身份验证的用户)可以查看公告。如果发出请求的用户是内容的所有者,或者被显式授予了编辑公告的权限,那么可以执行更新和删除操作。
在安装 Drupal 时创建的第一个注册用户(用户 ID 为 1)具有根特权,他可以编辑和修改系统中的任何数据。
清单 6. Announcement_access 挂钩实现
function announcement_access($op, $node) {
global $user;
if ($op == 'create') {
return user_access('create announcement');
}
else if ($op == 'view') {
return user_access('access content');
}
else if ($op == 'update' || $op == 'delete') {
if($user->uid == $node->uid || user_access('edit announcement')) {
return true;
}
else {
return false;
}
}
else {
return false;
}
}
hook_menu
menu 挂钩函数定义 Drupal 如何响应 URL。这个函数为特定的 URL 和菜单条目定义回调。在引用特定节点时,Drupal 构造 URL 的标准方法是将节点 ID 放在操作前面。按照这种格式,需要为我们的模块指定的 URL 包括:
/announcements
/announcements/add
/announcements/<id>/view
/announcements/<id>/edit
/announcements/<id>/delete
其中的 <id> 是要查看、编辑或删除的节点的 ID。我们的 announcement_menu 函数定义见 清单 7。
清单 7. Announcement_menu 挂钩实现
function announcement_menu($may_cache) {
$items = array();
if ($may_cache) {
$items[] = array('path' => 'announcements/add',
'title' => t('Add a new Announcement'),
'access' => node_access('create', 'announcement'),
'type' => MENU_CALLBACK,
'callback arguments' => array('announcement'),
'callback' => 'node_add');
$items[] = array('path' => 'announcements',
'title' => t('Announcements'),
'access' => user_access('access content'),
'type' => MENU_CALLBACK,
'callback' => 'announcement_all');
}
else {
if(is_numeric(arg(1))) {
$node = node_load(arg(1));
$items[] = array('path' => 'announcements/' . arg(1),
'title' => t('View an Announcement'),
'access' => node_access('view', $node),
'type' => MENU_CALLBACK,
'callback' => 'node_page');
$items[] = array('path' => 'announcements/' . arg(1) . '/view',
'title' => t('View an Announcement'),
'access' => node_access('view', $node),
'type' => MENU_CALLBACK,
'callback' => 'node_page');
$items[] = array('path' => 'announcements/' . arg(1) . '/edit',
'title' => t('Edit an Announcement'),
'access' => node_access('edit', $node),
'type' => MENU_CALLBACK,
'callback' => 'node_page');
$items[] = array('path' => 'announcements/' . arg(1) . '/delete',
'access' => node_access('delete', $node),
'type' => MENU_CALLBACK,
'callback' => 'node_delete_confirm');
}
}
return $items;
}
这个函数返回一个数组的数组,其中每个数组包含:
path
要匹配的 URL
title
这个菜单条目的标题,当鼠标停留在这个菜单条目上面时会显示这个标题
callback
当访问这个 URL 时调用的函数
type
菜单条目的类型
access
这个菜单条目的访问权限
可以使用缓存提高生产性 Web 站点的效率。在这里,可以使用 $may_cache 条件控制是否缓存菜单定义。但是,在开发期间,如果对信息进行缓存,它们就不能反映代码中的修改。可缓存条目的路径中不应该包含变量,比如 arg(1)。
hook_link
我们希望遵守的基本原则之一是,把操作放在接近被操作条目的地方。对于公告,我们将添加、编辑、删除和评论操作的链接放在公告标题的旁边。当然,只有在当前用户具有适当的访问特权的情况下,才应该显示这些操作链接。操作链接主题化为一种小的红色字体,如 图 6 所示。
图 6. 公告标题旁边的操作链接
公告标题旁边的操作链接
link 挂钩提供了处理链接的机制。清单 8 显示这个挂钩的实现。它允许我们根据访问特权创建适当的链接,然后用适当的标记对这些链接进行主题化。这就允许 CSS 将文本的样式设置为小的红色字体。comment 模块也可以通过提供 “Add Comment” 操作添加链接。
在 清单 8 中的 l(t('Edit')...) 序列中,我们使用两个实用程序函数支持链接的生成。t 函数首先将 'Edit' 字符串转换为当前地区,然后 l 函数将这个字符串格式化为适当的 Drupal 内部链接。通过调用 theme('links', $links) 在主题引擎中对链接进行主题化,然后将它映射到模板变量 $links。
清单 8. Announcement_link 挂钩实现
function announcement_link($type, $node = NULL, $teaser = FALSE) {
global $user;
$links = array();
if($type == 'node' && $node->type == 'announcement') {
if (node_access('create', 'announcement')) {
$links[] = l(t('Add'), "node/add/announcement",
array('title' => t('Add a new announcement')));
}
if (node_access('update', $node)) {
$links[] = l(t('Edit'), "announcements/$node->nid/edit",
array('title' => t('Edit Announcement ') . $node->title));
$links[] = l(t('Delete'), "announcements/$node->nid/delete",
array('title' => t('Delete Announcement ') . $node->title));
}
}
return $links;
}
hook_block
公告显示在主页的中间以及每个页面的导航边栏中,如 图 7 所示。边栏内容由 block 挂钩启用,这允许模块提供可以显示在页面上任何地方的内容。边栏常常显示完整内容的大纲。注意,当前用户的公告的突出显示在边栏和主内容区域中是一致的。 图 7 显示一位管理员看到的边栏,可以看到管理员登录之后可以使用的几个 Resources。对于一般用户,是不会显示这些的。
图 7. 使用 block 挂钩在边栏中显示的公告
在边栏中列出的公告
定义这个挂钩之后,管理员可以通过用户界面指定区块信息的位置,如 图 8 所示。这里启用了公告,并在右边栏中给予权值 -10。这个数字越小,内容在边栏中出现的位置就越高。-10 这个值确保公告是右边栏中出现的第一个信息区块。
图 8. 用来指定区块位置的管理员界面
用来指定区块位置的界面
清单 9 显示 announcement 模块的 block 挂钩实现。这个函数可以构造多个区块。Drupal 使用 $delta 参数为 $block 数组中定义的区块设置索引。函数测试的第一个条件是 list 操作。图 8 所示的管理员界面中要使用这些信息。
第二个操作是 view,它收集适当的公告以便显示在右边栏中使用的信息区块中。它使用 announcement_settings 挂钩中定义的公告设置 announcement_block_max_list_count 来决定要显示多少个公告。view 操作使用 announcement_block_list 模板文件(announcement_block_list.tpl.php)对公告进行主题化。(公告的主题化将在以后的一篇文章中讨论。)
清单 9. Announcement_block 挂钩实现
function announcement_block($op = 'list', $delta = 0, $edit = array()) {
global $user;
if ($op == 'list') {
$blocks[0]['info'] = t('Recently updated announcements');
return $blocks;
}
else if ($op == 'view') {
$block = array();
$output = '';
switch ($delta) {
case 0:
$now = time();
if (user_access('access content')) {
$q = 'SELECT N.uid,N.nid,N.title,A.publish_date,N.status '.
'FROM {node} N JOIN {announcement} A USING(nid) '.
"WHERE N.type='announcement' ".
'AND N.status = 1 '.
'AND A.publish_date < ' . $now . ' '.
'AND A.expiration_date > ' . $now . ' '.
'ORDER BY A.publish_date DESC ';
$items = variable_get('announcement_block_max_list_count', 3);
if ($items) { $q .= "LIMIT 0,$items"; }
$announcements = db_query($q);
$announcement_items = array();
while (db_num_rows($announcements) > 0 and $announcement =
db_fetch_object($announcements)) {
$announcement_items[] = $announcement;
}
}
$block['subject'] = t('Announcements');
$block['content'] = theme('announcement_block_list',$announcement_items);
break;
}
return $block;
}
}
hook_form
调用 form 挂钩来生成添加或编辑节点内容所需的用户界面。这个挂钩返回一个数组的数组,其中包含用户需要编辑的每段内容。公告节点通过 图 9 所示的界面进行编辑。我们为标题、发布日期和过期日期、摘要和主体提供了输入组件。在每个表单元素下面提供了简短的描述,向用户说明此信息是什么以及将会 如何使用它。
图 9. 公告节点的编辑表单
公告节点的编辑表单
为了生成对公告的摘要进行编辑的表单元素,我们创建一个数组,见 清单 10。
清单 10. 生成编辑公告摘要所需的文本区域
$form['abstract'] = array(
'#type' => 'textarea',
'#title' => t('Abstract'),
'#default_value' => $node->abstract,
'#rows' => 3,
'#description' => t('Short summary of the full announcement'),
'#required' => TRUE,
'#weight' => 9
);
数组的索引 abstract 是元素的名称。以 $node->abstract 的形式访问节点数据结构中的值。这个元素的细节包括:
type
组件的类型是 textarea(这会影响可用的其他属性)。
title
经过转换函数 “t” 转换的标题。
default_value
在组件最初显示时使用的值,例如当前值。
rows
文本区域显示的行数。
description
在界面中组件下面显示的文本。
required
这个输入字段是否是必须填写的。
weight
影响组件的排列次序:数字越小,在界面中出现的位置就越高。
Publication 定义为一个字段集,其中包含发布日期和过期日期。清单 11 显示完整的 announcement_form 函数,这个函数生成编辑公告节点所用的界面。如果没有提供发布日期和过期日期的话,前两个 if 子句设置合理的默认值。我们使用 publication 数组中的 #prefix 和 #suffix 元素插入额外的 HTML 标记,以便简化日期的 CSS 主题化。$form 数组的字符串索引用来间接引用节点中的特定值。
清单 11. 完整的 announcement_form
function announcement_form(&$node) {
if ($node->expiration_date == NULL) {
$node->expiration_date = time() + (365 * 86400);
}
if ($node->publish_date == NULL) {
$node->publish_date = time();
}
$form['title'] = array('#type' => 'textfield',
'#title' => t('Title'),
'#default_value' => $node->title,
'#description' => t('Title of the announcement'),
'#required' => TRUE,
'#weight' => 1
);
$form['publication'] = array('#type'=> 'fieldset',
'#collapsible' => FALSE,
'#title' => t('Publication dates'),
'#weight' => 5
);
$form['publication']['publish_date'] = array(
'#prefix' => '<div class="date_widget">',
'#suffix' => '</div>',
'#type' => 'date',
'#title' => t('Publication date'),
'#default_value' => _announcement_unixtime2drupaldate($node->publish_date)
);
$form['publication']['expiration_date'] = array(
'#prefix' => '<div class="date_widget">',
'#suffix' => '</div>',
'#type' => 'date',
'#title' => t('Expiration date'),
'#default_value' => _announcement_unixtime2drupaldate($node->expiration_date)
);
$form['abstract'] = array('#type' => 'textarea',
'#title' => t('Abstract'),
'#default_value' => $node->abstract,
'#rows' => 3,
'#description' => t('Short summary of the full announcement'),
'#required' => TRUE,
'#weight' => 9
);
$form['body'] = array('#type' => 'textarea',
'#title' => t('Body'),
'#default_value' => $node->body,
'#description' => t('Full content for the announcement which ' .
'is shown with the abstract on the details page'),
'#required' => TRUE,
'#weight' => 10
);
return $form;
}
从 Drupal 4.6 到 4.7 最大的变化之一是 form 挂钩的实现。Drupal Web 站点上有许多相关文档,包括:
* forms API Quickstart Guide
* forms API Reference
* detailed example
hook_validate
在编辑过程结束时,在存储节点之前触发 validate 挂钩。这可以用来在将数据存储进数据库中之前对数据进行检验。对于公告,我们利用这个挂钩确保发布日期在过期日期之前。更具交互性的实现可以使用客户端脚 本约束两个字段的关系,从而避免发生这种无效的情况。如果希望在存储节点之前对它进行额外的修改,那么使用 submit 挂钩。
清单 12 显示 validate 挂钩的实现。首先将日期转换为可以进行比较的整数。如果出现用户需要解决的问题,那么使用 form_set_error 函数,这个函数会使用 Drupal 的错误处理机制。在清单 12 中,这个函数的第一个参数是 publish_date,它引用 form 挂钩中使用的表单数组名称(清单 11),并在界面中突出显示这个元素。
清单 12. Announcement_validate 挂钩
function announcement_validate($node) {
if ($node) {
$publish_date =
_announcement_drupaldate2unixtime($node->publish_date);
$expiration_date =
_announcement_drupaldate2unixtime($node->expiration_date);
if ($publish_date >= $expiration_date) {
form_set_error('publish_date',
t('The publish date of an announcement must be before its expiration date.'));
}
}
}
hook_submit
节点经过检验阶段之后,调用 submit 挂钩,在实际更新数据库之前可以在这里对节点进行额外的修改。在 清单 13 中,submit 挂钩更新节点中的发布和过期日期,然后根据当前日期修改节点的状态。如果当前日期在发布和过期日期之间,就将状态设置为 1,否则将它设置为 0。
清单 13. Announcement_submit 挂钩
function announcement_submit(&$node) {
$node->publish_date =
_announcement_drupaldate2unixtime($node->publish_date);
$node->expiration_date =
_announcement_drupaldate2unixtime($node->expiration_date);
$now = time();
if ($now >= $node->publish_date &&
$now < $node->expiration_date) {
$node->status = 1;
}
else {
$node->status = 0;
}
}
数据库挂钩
在环境中发生与数据库进行交互的各种事件时,Drupal 会触发一个挂钩函数。重要的事件包括装载、插入、更新和删除。请查阅关于这些 挂钩事件 的更多信息。
在后面的一篇文章中,将详细讨论 MySQL 和数据库抽象层。
Drupal 有一个 数据库抽象层,在代码中的许多地方都要使用它。在数据库挂钩中,使用以 db_ 开头的函数访问这个数据库抽象层。
hook_load
当从数据库装载 announcement 类型的节点时,会自动地调用 load 挂钩。这个函数允许定制的模块从数据库装载任何额外的内容,从而使节点的内容完整。这个函数的返回值是包含额外内容的数组,这些内容会被合并进节点数据结 构中。在我们的示例中,需要从新表中装载三个数据条目 —— 摘要、发布日期和过期日期。清单 14 显示了为 announcement 节点装载额外信息的代码。
清单 14. 用于装载 announcement 类型的节点的 Announcement_load 挂钩
function announcement_load(&$node) {
$additions = db_fetch_object(db_query('SELECT * FROM {announcement} ' .
'WHERE nid = %d', $node->nid));
return $additions;
}
hook_insert
在 Web 站点上创建 announcement 节点时,会自动地调用 insert 挂钩,见 清单 15。这个挂钩使新模块有机会在创建一个节点时在数据库中存储额外的信息。对于 announcement 模块,我们要在 announcement 表创建一个新记录。传递给这个函数的节点对象包含来自输入表单的所有数据。发布日期和过期日期作为一个包含月、日和年的数组返回。本地函数 _announcement_drupaldate2unixtime 对这些日期进行转换。根据约定,所有本地模块函数的名称都以下划线(“_”)开头,后面跟着模块名称,比如 announcement。然后,调用数据库抽象层,将新行插入 announcement 表。$node->nid 是主键,它将 announcement 表链接到 node 表。
清单 15. Announcement_insert 挂钩以及用来将公告添加进数据库中的支持函数
function _announcement_drupaldate2unixtime($drupal_date) {
$year = $drupal_date["year"];
$month = $drupal_date["month"];
$day = $drupal_date["day"];
return mktime(0,0,0, (int)$month, (int)$day, (int)$year);
}
function announcement_insert($node) {
$publish_date = _announcement_drupaldate2unixtime($node->publish_date);
$expiration_date = _announcement_drupaldate2unixtime($node->expiration_date);
db_query("INSERT INTO {announcement} (nid, abstract, publish_date, expiration_date) ".
"VALUES (%d, '%s', '%d', '%d')",
$node->nid, $node->abstract, $publish_date, $expiration_date);
}
hook_update
当节点已经在数据库中存在而用户要编辑它时,会调用 update 挂钩(清单 16)。这个挂钩与 insert 挂钩相似,但发出的数据库命令是 UPDATE。
清单 16. 用来修改现有 announcement 节点的 Announcement_update 挂钩
function announcement_update($node) {
$publish_date = _announcement_drupaldate2unixtime($node->publish_date);
$expiration_date = _announcement_drupaldate2unixtime($node->expiration_date);
db_query("UPDATE {announcement} SET abstract='%s', publish_date = '%s', " .
"expiration_date = '%s' WHERE nid = %d",
$node->abstract, $publish_date, $expiration_date, $node->nid);
}
hook_delete
最后,当用户删除一个 announcement 节点时,会调用 delete 挂钩(清单 17)。这使模块有机会从数据库中的其他表中删除任何额外信息。在这里,我们要根据 nid 删除 announcement 表中与这个节点相关联的一行。
清单 17. 用来删除 announcement 节点的 Announcement_delete 挂钩
function announcement_delete($node) {
db_query('DELETE FROM {announcement} WHERE nid = %d', $node->nid);
}
hook_cron
cron 挂钩允许模块对任务进行调度,让任务以特定的时间间隔运行。站点管理员可以通过一个 cron 作业(对 http://<sitename.com>/cron.php 执行 HTTP GET)设置时间间隔。这会调用所有模块上定义的 cron 挂钩。在 Administer > Settings(例如 /admin/settings)下面的管理员界面的 cron 作业部分中,可以看到 cron 作业的状态。
announcement 模块依靠发布日期和过期日期来判断一个公告是否应该显示。但是,如果公告的节点状态没有设置为 0,就仍然可以通过标准的节点机制(/node/id/view)显示它。我们使用 cron 挂钩在所有已经过期的 announcement 节点上设置状态标志。清单 18 显示 announcement_cron 函数的实现,它首先在数据库中查询那些已经超过过期日期的公告,然后将这些节点的节点状态设置为 0。
清单 18. Announcement_cron 挂钩实现
function announcement_cron() {
$queryResult = db_query("UPDATE {node} AS n INNER JOIN {announcement} AS a " .
"ON n.nid = a.nid SET n.status = 0 WHERE n.type='announcement' " .
"AND n.status = 1 AND a.expiration_date < %d", time());
}
hook_search
search 挂钩使模块能够在它创建的节点上执行关键字搜索,从而扩展搜索页面的功能。首先,需要通过 administer > modules 页面启用 search 模块。这使我们能够使用 administer > block 页面将 search 区块包含在页眉中。通过使用 search 挂钩,当进行简单搜索时搜索页面上会出现另一个选项卡。可以使用这个搜索表单在自己的模块创建的节点中寻找关键字。
search 模块使用 cron 为节点中找到的数据建立索引表,这使 Drupal 能够提供针对节点内容的全文搜索。
对于 announcement 模块,我们希望搜索引擎能够搜索 announcement 表中新的摘要字段。还希望改变默认的搜索操作,从而显示这个摘要字段而不是默认内容,而且不希望用单独的选项卡显示搜索界面。幸运的是,search 挂钩的另一个替代品可以满足我们的需要。
hook_nodeapi
为了确保默认的搜索表单可以在 announcement 表中的摘要字段中寻找关键字,我们实现了 nodeapi 挂钩函数。这允许我们在更新索引期间包含这个字段。清单 19 显示了 nodeapi 挂钩的实现。
清单 19. Announcement_nodeapi 挂钩实现
function announcement_nodeapi(&$node, $op) {
switch ($op) {
case 'update index':
if ($node->type == 'announcement') {
$text = '';
$q = db_query('SELECT a.abstract FROM node n LEFT JOIN announcement a ' .
'ON n.nid = a.nid WHERE n.nid = %d', $node->nid);
if ($r = db_fetch_object($q)) {
$text = $r->abstract;
}
return $text;
}
}
}
在这个函数中,检查 update index 操作,这表示 Drupal 正在收集额外的数据,然后将在数据库中编制索引。如果要编制索引的节点是 announcement 类型的,就从 announcement 表中提取并返回相关联的摘要字段值。
既然 Drupal 可以对公告摘要编制索引了,就需要在默认的搜索页面结果上显示与关键字搜索匹配的信息。我们实现这一特性的方法是,使用 phptemplate_search_item 函数覆盖 search 模块中的 theme_search_item 函数,见 清单 20。因为这个函数是一种全局的主题修改,我们将它放在主题目录中的 template.php 文件中。
清单 20. phptemplate_search_item 函数
function phptemplate_search_item($item, $type) {
return _phptemplate_callback('search_item',
array('node' => $item), 'search_item-' . strtolower($item['type']));
}
在这个函数中,使用 _phptemplate_callback 函数将搜索条目的主题化与一个模板文件关联起来。phptemplate 引擎允许使用 node.tpl.php 和 node-<node-type>.tpl.php 模板文件来定制节点的显示方式,但是我们不这么做,而是使用这个函数连接 search_item.tpl.php 和 search_item-<node-type>.tpl.php 模板,见 清单 21。
现在,可以为搜索条目的默认外观提供一个模板,它实质上是 search.module 文件中的 theme_search_item 函数中原来的主题化搜索条目的修改版本。这种技术可以应用于任何主题化的实体。
清单 21. search_item.tpl.php 模板
<dt class="title search_item">
<a href="<?php print check_url($node['link']); ?>"><?php print
check_plain($node['title']) ?></a>
</dt>
<?php
$info = array();
if ($node['type']) $info[] = strtolower($node['type']);
if ($node['user']) $info[] = $node['user'];
if ($node['date']) $info[] = format_date($node['date'],'small');
if (is_array($node['extra'])) $info = array_merge($info, $node['extra']);
?>
<dd class="search_item">
<p><?php print $node['snippet']; ?></p>
<p class="search-info"><?php print implode(' - ', $info); ?></p>
</dd>
通过使用 search_item-announcement.tpl.php 模板,可以对 announcement 类型的搜索条目进行主题化,用我们自己构造的摘要字段替换默认的片段。在 清单 22 中,我们使用 search_excerpt 函数突出显示摘要中的关键字。
清单 22. search_item-announcement.tpl.php 模板
<dt class="title search_item_announcement">
<a href="<?php print check_url($node['link']); ?>">
<?php print check_plain($node['title']) ?></a>
</dt>
<?php
$info = array();
if ($node['type']) $info[] = strtolower($node['type']);
if ($node['user']) $info[] = $node['user'];
if ($node['date']) $info[] = format_date($node['date'],'small');
if (is_array($node['extra'])) $info = array_merge($info, $node['extra']);
?>
<dd class="search_item_announcement">
<p><?php print ($node['node']->abstract ? '<p>'.
search_excerpt(search_get_keys(),$node['node']->abstract) .
'</p>' : $node['snippet']); ?></p>
<p class="search-info"><?php print implode(' - ', $info); ?></p>
</dd>
图 10. 搜索结果页面在输出中突出显示搜索词
搜索结果页面在输出中突出显示搜索词
hook_node_info
清单 23 显示 node_info 挂钩函数,它允许节点模块定义多个定制的节点类型。我们通过这个函数让 Drupal 定义 announcement 节点类型。Drupal 需要两个与节点类型相关联的值:适合用户阅读的节点类型名称,这会用在用户界面中;以及基名称,这将作为与这个节点类型相关联的函数的前缀。如果希望添加 另一个节点类型,可以添加另一个数组条目。
清单 23. Announcement_node_info 决定模块的节点类型的名称和属性
function announcement_node_info() {
return array('announcement' => array('name' => 'Announcement',
'base' => 'announcement'));
}
对模块的输出进行主题化
模块文件提供几个主题函数,可以使用它们在不同的上下文中显示模块的信息。对于大多数情况,我们发现按照以下方式考虑这些上下文是有帮助的:
* 详细的布局,其中显示给定节点的所有信息
* 简略表示或总结,其中只显示重要部分,从而提供对节点的概述
* 区块,这提供比较小的节点信息形式,通常嵌入左或右边栏中
对于 announcement 模块,我们为每个上下文使用一个主题函数:
* 公告页面使用 theme_announcement 函数创建详细的布局。
* 站点的 front 页面或主页使用 theme_announcement_compact 函数创建公告的列表。
* 所有页面上的右边栏中的公告区块使用 theme_announcement_block_list 函数。
正如 第 5 部分:Drupal 入门 所解释的,主题函数可以用来创建 announcement 模块输出的默认外观,而不管选择的主题引擎是哪种。
因为我们使用 phptemplate 引擎,可以使用 phptemplate_announcement、phptemplate_announcement_compact 和 phptemplate_announcenemt_block_list 函数覆盖这些默认的主题函数,见 清单 24。我们发现,最好是将结构和样式定义与模块的逻辑分隔开,并使用 _phptemplate_callback 函数将模板文件与每个上下文关联起来。
清单 24. 默认主题函数和覆盖它们的 PHPTemplate 引擎函数
function theme_announcement($announcement) {
// Put your default theme for the announcement detail here
return '';
}
function theme_announcement_compact($announcement) {
// Put your default theme for the announcement summary here
return '';
}
function theme_announcement_block_list($announcement_list) {
// Put your default theme for the announcement block here
return '';
}
function phptemplate_announcement($announcement) {
return _theme_phptemplate_announcement($announcement, 'announcement');
}
function phptemplate_announcement_compact($announcement) {
return _theme_phptemplate_announcement($announcement, 'announcement_compact');
}
function phptemplate_announcement_block_list($announcement_list) {
global $user;
return _phptemplate_callback('announcement_block_list',
array('announcements' => $announcement_list,
'user' => $user));
}
function _theme_phptemplate_announcement($announcement, $announcement_template) {
$expired = FALSE;
if ($announcement->expiration_date < time()) {
$expired = TRUE;
}
$variables = array(
'title' => $announcement->title,
'body' => $announcement->body,
'links' => $announcement->links ?
theme('links', $announcement->links) : '',
'abstract' => $announcement->abstract,
'published' => format_date($announcement->publish_date,'custom','j M, Y'),
'expires' => format_date($announcement->expiration_date,'custom','j M, Y'),
'expired' => $expired,
'node' => $announcement
);
return _phptemplate_callback($announcement_template, $variables);
}
注意辅助函数 _theme_phptemplate_announcement,它提供一种对传递给模板文件的变量进行准备的通用方法。
我们已经描述了 announcement 定制模块的一个简单主题化示例。在后续的一篇文章中,将详细讨论节点的样式化。
结束语
在本文中,您了解了一个简单的定制模块(announcement 模块)的实现。这个模块提供的公告会根据发布日期和过期日期自动地显示在 Web 站点上或隐藏。公告显示在主页的主区域和所有其他页面的边栏中。我们使用许多核心函数(即挂钩)提供了一个有效的模块。
在本 系列 中的后续文章中,将详细讨论这个虚构的 Web 站点的样式,以及到 SQL 和数据库抽象层的接口。
相关阅读 更多 +