译者:老葛 Eskalate科技公司
注意: 在前面列表中提到的内部名称字段,是用来构造“创建内容(create content)”链接的URL的。例如,我们使用“joke”作为我们节点类型的内部名称(它是我们返回的数组的键),那么要创建一个新的笑话的话,用户要访问页面http://example.com/?q=node/add/joke。通常你不需要对此作出修改。内部名称存储在表node 和 node_revisions的“type”列中。
定义一个菜单回调函数
现在你已经定义了基本的节点属性,让我们为路径node/add/joke创建一个菜单回调函数,这样你就可以创建一个笑话表单并定义一些权限了。
/**
* Implementation of hook_menu().
*/
function joke_menu($may_cache) {
$items = array();
// Do not cache this menu item during the development of this module.
if (!$may_cache) {
$items[] = array(
'path' => 'node/add/joke',
'title' => t('Joke'),
'access' => user_access('create joke'),
);
}
return $items;
}
由于菜单系统的层次性,在这里 “callback”参数是可选的。你可以不对其进行设置,替代的你使用父路径node/add的回调函数,可以在node.module中的菜单钩子中找到该路径。在那里,它映射到了构建节点表单的函数node_add()上。通过使用已经定义好的回调函数,由于已经将它映射到了核心的节点验证和提交程序上了,所以你可以节省大量的工作。换句话说就是,现在你只需要关注于你自己定制的数据就可以了(在这里,你的模块将需要处理笑话妙语(punchline)字段),而其他的用户提交的数据比如标题和主体都不用你操心。这是由于node.module将会负责标题和主体以及其它一些核心节点属性的创建、验证、处理等操作。
使用hook_perm()定义特定于节点类型的权限
在你的菜单项目中你也添加了对“create joke”权限的检查,但是你尚未在模块中定义该权限。让我们使用hook_perm()来创建该权限:
/**
* Implementation of hook_perm().
*/
function joke_perm() {
return array('create joke', 'edit own joke');
}
现在你可以导航到Administer ➤ User management ➤ Access control,你就可以看到你在上面定义的权限了,并且可以将它分配给用户角色了。
使用hook_access()来限制对一个节点类型的访问
节点模块可以使用hook_access()来限制对它们定义的节点类型的访问。超级用户(用户ID 1)将绕过所有的访问权限检查,所以此时不会调用该钩子函数。如果没有为你的节点类型定义该钩子函数,那么所有的访问权限检查都会失败,这样就只有超级用户和具有“administer nodes”权限的用户才能够访问该类型的内容。
/**
* Implementation of hook_access().
*/
function joke_access($op, $node) {
global $user;
if ($op == 'create') {
return (user_access('create joke'));
}
if ($op == 'update' || $op == 'delete') {
return (user_access('edit own joke') && ($user->uid == $node->uid));
}
}
前面的函数允许具有“create joke”权限的用户创建一个笑话节点。如果用户还具有“edit own joke”权限并且它们是节点作者的话,那么他们还可以更新或者删除一个笑话。传递到钩子函数hook_access()的$op中的另一个值是“view”(查看),它允许你控制谁可以查看该节点。然而我们需要提醒一下:当查看的页面仅有一个节点时,才调用钩子hook_access()。当用户查看的页面是多节点列表页面,节点此时处于teaser状态,那么这种情况下就不能使用hook_access()来阻止用户对该页面的访问。你可以创建一些其它的钩子函数并直接操纵$node->teaser的值来控制对它的访问,但是这有点麻烦。一个好的解决方案是使用我们将在后面简短讨论到的函数hook_node_grants()和hook_db_rewrite_sql()。
为我们的节点类型定制节点表单
到目前为止,你已经为你的新节点类型定义了元数据、菜单回调函数和访问控制权限。接着,你需要构建节点表单,这样用户就可以输入笑话了。你可以通过实现hook_form()来完成这一工作:
/**
* Implementation of hook_form().
*/
function joke_form($node) {
// Get metadata for this node type
// (we use it for labeling title and body fields).
// We defined this in joke_node_info().
$type = node_get_types('type', $node);
$form['title'] = array(
'#type' => 'textfield',
'#title' => check_plain($type->title_label),
'#required' => TRUE,
'#default_value' => $node->title,
'#weight' => -5
);
$form['body_filter']['body'] = array(
'#type' => 'textarea',
'#title' => check_plain($type->body_label),
'#default_value' => $node->body,
'#rows' => 7,
'#required' => TRUE
);
$form['body_filter']['filter'] = filter_form($node->format);
$form['punchline'] = array(
'#type' => 'textfield',
'#title' => t('Punchline'),
'#required' => TRUE,
'#default_value' => $node->punchline,
'#weight' => 5
);
return $form;
}
注意:如果你不熟悉表单API的话,请参看第10章。
作为站点管理员,你现在可以导航到Create content ➤ Joke并查看新创建的表单了。在前面函数中的第一行代码返回了该节点类型的元数据信息。node_get_types()将检查$node->type以决定要返回哪种节点类型的元数据(在我们的例子中,$node->type的值将为“joke”)。这里再强调一遍,在钩子hook_node_info()中设置节点元数据,你已经在前面的joke_node_info()中设置了它。函数的其余部分包含了三个表单字段,用来收集标题、主题、笑话妙语。这里有一个重点,就是如何实现标题和主体的#title键的动态化的。它们的值来源于hook_node_info(),如果在hook_node_info()中“locked”属性为FALSE的话,站点管理员也可以在http://example.com/?q=admin/content/types/joke修改这些值。
添加过滤器格式支持
由于主体字段是一个textarea,而且节点主体字段可以使用过滤器格式,那么你可以使用下面的代码来包含Drupal的标准过滤器选择器(使用过滤器的更多信息,参看第11章):
$form['body_filter']['filter'] = filter_form($node->format);
如果你想让笑话妙语字段也可以使用过滤器格式,你需要在你的数据库表“joke”中再添加一列以为每个笑话妙语存储输入过滤器格式,如下所示:
ALTER TABLE 'joke' ADD 'punchline_format' INT UNSIGNED NOT NULL;
接着你要将你的最后一个表单字段定义修改成如下所示的形式:
$form['punchline']['field'] = array(
'#type' => 'textarea',
'#title' => t('Punchline'),
'#required' => TRUE,
'#default_value' => $node->punchline,
'#weight' => 5
);
$form['punchline']['filter'] = filter_form($node->punchline_format);
一般情况下,在Drupal中构建表单时的最后一行代码应该是如下的格式:
return drupal_get_form('joke_node_form', $node);
然而,由于你使用的是一个节点表单而不是一个普通的表单,node.module将处理大部分额外的工作。它负责验证和存储在表单中它能识别的所有默认字段,并为你(开发者)提供了验证和存储你定制数据的钩子。
接下来我们将讨论这些钩子函数。
使用hook_validate()来验证字段
当你提交一个你的节点类型的节点时,将会调用你模块中的钩子hook_validate()以验证你定制字段中的输入数据了。在验证中,你也可以使用form_set_value()做些修改。可以使用form_set_error()来设置错误消息,如下所示:
/**
* Implementation of hook_validate().
*/
function joke_validate($node) {
// Enforce a minimum word length of 3.
if (isset($node->punchline) && str_word_count($node->punchline) <= 3) {
$type = node_get_types('type', $node);
form_set_error('punchline', t('The punchline of your @type is too short. You
need at least three words.', array('@type' => $type->name)));
}
}
注意你已经在hook_node_info()中为主体字段定义了最小单词书目,而Drupal将自动对此进行验证。然而,笑话妙语字段是你添加到该节点类型表单的一个定制字段,所以你需要负责它的验证(加载、保存)。
使用hook_insert()来存储我们的数据
当保存一个新的节点时将会调用钩子insert(),在该钩子中你可以将定制的数据存储到相关的表中。只有节点为当前节点类型时才调用这一钩子。例如,如果节点类型是“joke”,那么将会调用joke_insert()。如果新添加了一个“book”类型的节点,那么就不会调用joke_insert()(在这里将调用book_insert())。
注意:如果你想在插入一个不同类型的节点时对其做些操作的话,你需要把它当作一个普通的节点提交,使用钩子hook_nodeapi()插入一些操作。参看“使用hook_nodeapi()操纵其它类型的节点”。
下面是为joke.module编写的hook_insert()函数:
/**
* Implementation of hook_insert().
*/
function joke_insert($node) {
db_query("INSERT INTO {joke} (nid, vid, punchline) VALUES (%d, %d, '%s')",
$node->nid, $node->vid, $node->punchline);
}
使用hook_update()更新定制数据
当编辑完一个节点并且核心节点数据已被写入到数据库中时,将调用钩子update()。再改钩子中编写对相关表的更新操作代码。而钩子hook_insert(),只有在节点为当前节点类型时才调用。例如,如果节点类型为”joke”,那么将调用joke_update()。
/**
* Implementation of hook_update().
*/
function joke_update($node) {
if ($node->revision) {
joke_insert($node);
}
else {
db_query("UPDATE {joke} SET punchline = '%s' WHERE vid = %d",
$node->punchline, $node->vid);
}
}
在这里,你首先检查看是否设置了节点修订标记,如果已经设置了,你创建一个笑话妙语的副本以保留旧的版本。(译者注:这句是对joke_insert($node);的解释,我没有看懂)。
使用hook_delete()清理数据
在从数据库中山出一个节点之后,Drupal将会立即调用所有实现了钩子hook_delete()的模块中的该钩子方法。该钩子一般用来从数据库中删除相关的信息。只有在删除当前节点类型的节点时,才调用本钩子。如果节点类型为”joke”,将会调用joke_delete()。
/**
* Implementation of hook_delete().
*/
function joke_delete(&$node) {
// Delete the related information we were saving for this node.
db_query('DELETE FROM {joke} WHERE nid = %d', $node->nid);
}
注意:当要删除的是一个修订本而不是整个节点时,Drupal将调用钩子hook_nodeapi(),其中将$op设为“delete revision”并将整个节点对象传进来。接着你的模块可以使用$node->vid作为键来删除它在修订本中的数据。
使用hook_load()来修改节点对象
在你的基本的joke模块中你最后一个需要实现的节点相关的钩子就是hook_load(),它可以在构建节点对象时向对象中添加你定制的节点属性。我们需要把笑话妙语字段注入到节点加载流程中,这样就可以在其他模块以及主题层使用它了。在构建完核心节点对象以后,且节点为当前节点类型时,将会立即调用该钩子。如果节点类型为“joke”,那么就调用joke_load()。
/**
* Implementation of hook_load().
*/
function joke_load($node) {
return db_fetch_object(db_query('SELECT punchline FROM {joke} WHERE vid = %d',
$node->vid));
}
使用hook_view()显示笑话妙语
现在你有了一个完整的系统,可以输入和编辑笑话。然而,尽管可以在节点提交表单中输入笑话妙语,但在查看笑话时,你却没有提供显示笑话妙语(包袱)字段的方式,你的用户将会对此感到很困惑。让我们使用hook_view()来显示笑话妙语(包袱):
/**
* Implementation of hook_view().
*/
function joke_view($node, $teaser = FALSE, $page = FALSE) {
if (!$teaser) {
// Use Drupal's default node view.
$node = node_prepare($node, $teaser);
// Add a random number of Ha's to simulate a laugh track.
$node->guffaw = str_repeat(t('Ha!'), mt_rand(0, 10));
// Now add the punchline.
$node->content['punchline'] = array(
'#value' => theme('joke_punchline', $node),
'#weight' => 2
);
}
if ($teaser) {
// Use Drupal's default node view.
$node = node_prepare($node, $teaser);
}
return $node;
}
你首先需要保证节点不是teaser形式显示的(也就是说,$teaser应该为FALSE,这样你就可以继续了)。你已将笑话秒的显示分解为一个单独的主题函数,这样就可以非常容易的覆写它了。如果你个站点管理员想使用你的模块但又想定制外观的话,这将会非常方便。你为主题函数提供了一个默认实现:
function theme_joke_punchline($node) {
$output = '<div class="joke-punchline">'.
check_plain($node->punchline). '</div><br />';
$output .= '<div class="joke-guffaw">'.
check_plain($node->guffaw). '</div>';
return $output;
}
你现在应该有了一个可以完全工作的笑话输入和查看系统。继续前进,输入一些笑话测试一下。你现在应该可以看到你的笑话了,它看起来有点朴素和简单,如图7-2所示:
图7-2 简单主题下的笑话节点
尽管这也可以工作,但还是存在一个更好的方式让用户立即看到笑话妙语。我们想要的是使用一个可伸缩的字段集,当用户点击时再展示笑话妙语。在Drupal内部已经存在了一个可伸缩的字段集功能,所以你只需要使用现有的就可以了而不是创建你自己的Javascript文件。把这一交互放到你的站点主题的模板文件中比放到主题函数中更好一些,因为它依赖于标识字体和CSS类。你的设计者喜欢你这样做,因为如果要修改笑话节点的外观的话,只需要简单的编辑模板文件就可以了。你需要创建一个名为node-joke.tpl.php的模板文件,并将其放到你当前使用的主题的目录下面,下面是该文件中的内容。如果你使用的主题为bluemarine,那么node-joke.tpl.php将被放到themes/bluemarine下面。由于我们将使用一个模板文件,那么就不再需要或者调用函数theme_joke_punchline()了,所以我们就可以把该函数注释掉了。
注意:主题系统将会自动发现node-joke.tpl.php,Drupal将使用该模板文件来修改笑话的外观,而不是默认的节点模板文件node.tpl.php。更多关于主题系统的知识,请参看第8章。
<div class="node<?php if ($sticky) { print " sticky"; } ?>
<?php if (!$status) { print " node-unpublished"; } ?>">
<?php if ($picture) {
print $picture;
}?>
<?php if ($page == 0) { ?><h2 class="title"><a href="<?php
print $node_url?>"><?php print $title?></a></h2><?php }; ?>
<span class="submitted"><?php print $submitted?></span>
<span class="taxonomy"><?php print $terms?></span>
<div class="content">
<?php print $content?>
<fieldset class="collapsible collapsed">
<legend>Punchline</legend>
<div class="form-item">
<label><?php print check_markup($node->punchline)?></label>
<label><?php print $node->guffaw?></label>
</div>
</legend>
</fieldset>
</div>
<?php if ($links) { ?><div class="links">» <?php print $links?></div>
<?php }; ?>
</div>
最后,你需要加载负责可伸缩字段集的JavaScript文件。这需对joke_load()做一简单修改:
function joke_load($node) {
drupal_add_js('misc/collapse.js');
return db_fetch_object(db_query('SELECT * FROM {joke} WHERE vid = %d',
$node->vid));
}
如果用户查看的是节点列表页面,那么每个页面将会多次调用drupal_add_js(),但是函数drupal_add_js()实际上会阻止重复加载文件。所以在这里加载JavaScript文件能够保证只有当需要它时(页面中包含笑话节点)才加载它,而不是将它放到菜单钩子里面对于所有页面都加载它。collapsible.js中的JavaScript将为字段集寻找可伸缩的CSS选择器,并且在找到以后知道如何处理它,如图7-3所示。这样,在node-joke.tpl.php中它将找到下面代码并激活它:
<fieldset class="collapsible collapsed">
这将得到我们想要的笑话的交互体验:
图7-3 使用Drupal内置的可伸缩CSS支持来隐藏笑话妙语
使用hook_nodeapi()操纵其它类型的节点
前面的钩子只有在基于节点类型时才调用。当Drupal查看一个blog(日志)节点时,调用blog_load()。如果你想为每个节点都添加一些信息,不管它是什么类型,那该怎么办呢?我们到目前为止看到的钩子函数都做不到这一点;对于这一工作我们需要一个更强大的钩子:hook_nodeapi()。
尽管创建节点不需要该钩子,但是还是值得在此对它进行说明,因为对于任何节点的生命周期期间,该钩子提供了一个使用模块对其进行修改的机会。一般在node.module调用完所有的特定节点类型的钩子函数以后调用钩子nodeapi()。下面是函数的签名:
hook_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL)
因为节点对象$node是通过引用传递的,所以对它的任何修改都将改变真正的节点。参数$op用来描述当前对节点所进行的操作,它可以有多种不同的值:
• delete:删除节点时。
• insert:刚刚将新节点插入到数据库中。
• load: 从数据库中加载了基本的节点对象,再加上有节点类型设置的额外节点属性(将会运行负责该项工作的hook_load()函数;参看本章前面的“使用hook_load()来修改节点对象”)。此时你可以添加新的属性或者操纵已有的节点属性。
• view: 正准备将节点展示给用户。将在钩子hook_view()之后调用该动作,所以模块可以假定节点已被过滤并且现在包含的为HTML。
• update:节点刚被更新到数据库中。
• validate:用户刚完成对节点的编辑并试图预览或者查看它。你可以使用该钩子来检查甚至修改数据,尽管在验证钩子中修改数据被认为是坏的开发实践。
• submit:节点已通过了验证并将被保存到数据库中。
• prepare: 将要展示节点表单。它应用于节点的“Add”(添加) 和“Edit”(编辑) 表单时。
• print:为打印准备一个节点试图。用在book.module中的打印机专用视图中。
• search result:节点将作为一个项目展示在搜索结果中时。
• update index:节点正在被搜索模块索引化。如果你想对使用nodeapi的“view”操作不能显示的额外信息进行索引的话,你可以在这里完成它(参看第12章)。
• rss item:节点将被作为RSS种子的一部分包含进来。
最后两个参数是根据当前操作可以改变其值的变量。当展示一个节点并且$op为view时,$a3 将为$teaser,而 $a4将为$page(参看node.module中的node_view())。参数的总结可参看表7-1 。
表7-1. 当$op为 view时,hook_nodeapi()中参数$a3 和 $a4的含义
参数 含义
$teaser 是否要仅仅展示teaser,比如在主页上。
$page 如果一个页面仅有一个节点时,$page为TRUE
当节点正被验证时,$a3参数为节点对象,而$a4参数为node_validate()中的$form参数(也就是,表单定义)。
如何存储节点
节点在数据库中被分成3个部分。表node包含了描述节点的大部分元数据。表node_revisions包含了节点的主体和teaser,以及一些修订特有的信息。正如你在joke.module例子中看到的,其他节点类型可以自由的在加载节点时向节点添加数据,同时可以向它们自己的表中存储他们想要的数据。
图7-4展示了一个包含了大部分常用属性的节点对象。注意你创建的用以存储笑话妙语的表被用来填充节点。根据启用模块的不同,在你的Drupal安装里面的节点对象包含的属性可能会有或多或少的变化。
图7-4 节点对象