第7章 Drupal节点

老葛的Drupal培训班 Think in Drupal

在本章中,我将介绍节点和节点类型。我将向大家展示使用两种不同方式创建一个节点类型。首先,我将向你介绍程序解决方案,也就是通过Drupal钩子函数编写模块来创建节点类型。这种方式,在定义节点可以做什么不可以做什么的时候,具有更高的自由度和灵活性。接着,我将向大家介绍如何通过Drupal后台管理接口来创建一个节点类型,并简单的讨论了内容创建工具集模块(CCK),Drupal社区正在逐步的将CCK的方式添加到Drupal核心中去。最后我们将研究一下Drupal的节点访问控制机制。
 
提示 开发者通常使用术语节点节点类型。而在Drupal的用户界面,分别将其称为posts(发布)和内容类型,这主要是为了让站点管理员能够更好的理解这些概念。
 

Drupal版本:

那么什么才是节点呢?

老葛的Drupal培训班 Think in Drupal

对于刚刚接触Drupal开发的新手来说,最先遇到的问题之一就是,什么是节点?一个节点就是一个内容片段。Drupal为每一片内容指定一个名为“节点 ID”(在代码中简写为$nid)的数字ID。一个每个节点还拥有一个标题,从而允许管理员通过标题来查看节点列表。
 
注意 如果你熟悉面向对象的话,那么你可以把每个节点类型看作一个类,把每个节点看做一个对象实例。然而,Drupal的代码不是100%面向对象的,为什么这样呢?这有一个很好的解释。(参看http://api.drupal.org/api/HEAD/file/developer/topics/oop.html)。在Drupal的将来版本中,如果需求合理的话,将会越来越倾向于使用面向对象技术,因为将来不再支持PHP4(它对面向对象的支持很弱)。
 
    有许多不同的节点或节点类型。常见的节点类型有“blog entry”(博客),“poll”(投票),和“book page”(书籍页面)。一般情况下(在本书中),术语“内容类型”和“节点类型”是同义的,尽管节点类型是一个更抽象的概念并且你可以把它看作基节点的派生,如图7-1所展示的。
 
 
    把所有的内容类型当作节点的好处是,这样就可以为它们使用相同的底层数据结构了。对于开发者来说,这意味着你可以对所有的内容以同样的代码方式进行多种操作。对于节点可以非常容易的进行一批操作,并且你还可以为自定义的节点类型添加许多额外的功能。由于所有的内容都是节点,其底层的数据结构和行为是一样的,所以Drupal内置的支持了对内容的搜索、创建、编辑和管理等操作。显然,该一致性对于终端用户也同样有用。由于创建、编辑和删除节点的表单拥有一个类似的外观,这样就既保持了一致性,并且用户界面更易于使用。
7-1 源于基本节点的节点类型和可能添加的字段
 

Drupal版本:

那么什么才是节点呢?(1)

老葛的Drupal培训班 Think in Drupal

通常通过为节点类型添加它们自己的属性,来扩展基本节点。节点类型poll存储了投票相关条目,如投票的有效期,投票当前是否可用,以及用户是否允许投票。节点类型forum为每个节点加载了分类术语,这样它就知道了它位于管理员定义的哪个论坛下面。节点类型blog,则与前面二者不同,它没有添加任何的其它的数据;替代的,通过为每个用户创建日志和为每个日志创建RSS种子,从而为数据添加了不同的视图。所有的节点都包括了下列属性,它们存储在表node和node_revisions中:
 
• nid:节点的唯一标识ID。
 
• vid:节点的唯一修订本ID,由于Drupal需要为每个节点存储内容修订本,所以该字段是必须的。在所有的节点和节点修订本中,vid是唯一的。
 
• type:每个节点都有一个节点类型;例如,blog, story, article, image等等。
 
• language:节点的语言。如果此列为空的话,那么就意味着该节点是语言中立的。
 
• title:节点的标题,一个简短的255位字符的字符串。如果通过代码将表node_type中的字段has_title设置为0的话,那么节点就没有标题了。
 
• uid:作者的用户ID。默认情况,每个节点都有一个唯一的作者。
 
• status: 0表示未发布;就是说,不具有 “管理节点” 权限的用户看不到它的内容。1意味着已发布,并且具有“管理节点”权限的用户可以看见它的内容。Drupal的节点级别的访问控制机制(可参看本章中的后面两节,“使用hook_access()来限制对一节点类型的访问”和“限制对节点的访问”)可以禁止已发布节点的显示。如果启用了搜索模块,那么可以使用搜索模块来对内容建立索引。
 
• created:节点创建时的Unix时间戳。
 
• changed:节点最后被修改的Unix时间戳。如果你是使用了节点修订本系统,那么它的值与表node_revisions中字段timestamp的值相同。
 
• comment:一个整数字段,用来描述节点的评论状态,它有3个可能值:
• 0:对当前节点禁用了评论。这是评论模块禁用时已有节点的默认值。在节点编辑表单的“评论设置”部分里的用户界面中,它对应于“已禁用”选项。
• 1:不能再向当前节点添加评论了。在节点编辑表单的“评论设置”部分里的用户界面中,它对应于“只读”选项。
• 2:可以查看评论,并且用户可以创建新的评论。评论模块负责控制着谁可以创建评论以及评论显示的外观。在节点编辑表单的“评论设置”部分里的用户界面中,它对应于“读/写”选项。
 
• promote:另一个整数字段,用来决定是否将节点显示在首页上,有两个值可用:
• 1:推到首页。节点将被推到你站点的默认首页上。该节点仍然会显示在它的普通页面上,例如http://example.com/?q=node/3。这里需要注意的是,由于你可以在“管理>>站点设置>>站点信息”中将首页改成你想要的那个页面,所以这里可能有点用词不当。更准确一点的说,页面http://example.com/?q=node将包含所有的promote字段为1的节点,而该页面在默认情况下为站点的首页。
• 0: 不将节点显示在http://example.com/?q=node中。
 
• moderate:一个整数字段,其中0表示禁用了审核,1表示启用了审核。下面是该字段的警告说明:在核心的Drupal安装中没有为该字段留下接口。换句话说就是,你可以反复的改变该字段的值,而默认情况下它不起任何作用。所以开发者可以根据它们的需要,将该字段用在各种功能中去。第3方模块,比如http://drupal.org/project/modr8http://drupal.org/project/revision_moderation,使用了该字段。
 
• sticky:当Drupal在一个页面中显示一列节点时,默认情况是将标记为“置顶”的节点列在前面,接着按照创建日期列出剩下的“不置顶”节点。换句话说就是“置顶”的节点位于节点列表的顶部。1表示“置顶”,0表示“不置顶”。你可以在同一列表中包含多个“置顶”节点。
 
• tnid:当一个节点作为另一个节点的翻译版本时,被翻译的源节点的nid将被存储在这里。例如,如果节点3的语言为英语,节点5是节点3的瑞典语翻译,那么节点5的tnid字段就为3。
 
• translate:有两个可选的值,1意味着翻译需要被更新;0意味着翻译是最新的。
 
如果你使用了Drupal的修订本系统,Drupal将创建一个内容的修订本,同时又追踪了谁在最后修改了节点。
 

Drupal版本:

不是所有的东西都是节点

用户、区块和评论不是节点。在这些特定的数据结构中,为了适应它们各自的特定目的,它们每一个都拥有自己的钩子系统。节点一般有“标题”和“正文”两部分,而在表示用户的数据结构中则不需要这些。用户需要的是,e-mail地址、用户名称、一种安全的存储密码的方式。当要存储的内容片段更小一些时,比如存的是导航菜单、搜索框、最新评论列表等等,我们此时可以使用轻量级的存储解决方案---区块。评论也不是节点,它们也属于轻量级的内容。一个页面可能会有100或者更多的评论,试想,如果所有的这些评论在被加载时都使用节点钩子系统的话,那么会给系统带来多大的负担呢.

    在过去,经常争论,用户或评论到底应不应该归结为节点,而一些第3方模块实际上实现了这一点。如果现在还对这个问题进行争论的话,那么就好比在编程风格上高呼“Emacs更好一些”一样。(译者注:我不知道Emacs什么意思^_^)。
 
老葛的Drupal培训班 Think in Drupal

Drupal版本:

创建一个节点模块

老葛的Drupal培训班 Think in Drupal

传统上,当你想在Drupal中创建一个新的内容类型时,你应该编写一个节点模块,由它来负责提供你的内容类型所需的新的且有趣的东西。我们之所说这是传统方式,这是因为Drupal框架最近常用的方式是,让你通过后台管理界面来创建内容类型,使用第3方模块来扩展这些内容类型的功能,而不是从头开始编写一个节点模块。在本章中,我们将讨论这两种方式。
    让我们编写一个节点模块,从而让用户可以为站点添加笑话。每一个笑话都包括一个标题,笑话本身,接着是一个笑话妙语(punchline)。你应该可以非常容易的使用内置的节点属性title来存储笑话的标题,用节点属性body来存储笑话内容,但是你还需要创建一个新的数据库表来存储笑话妙语。我们将通过使用.install文件来实现它。
 
    首先,让我们在目录sites/all/modules/custom下面创建一个名为joke的文件夹。

Drupal版本:

创建.install文件

老葛的Drupal培训班 Think in Drupal

你将需要在你的数据库表中存储一些信息。首先,你需要节点的ID,这样你就可以引用node_revisions表中的对应节点了,node_revisions表存储了节点的标题和主体。其次,你需要存储节点的修订本ID,这样你的模块就可以使用Drupal内置的修订本控制了。当然,你还需要存储笑话妙语。由于你已经知道了数据库的模式,让我们继续,创建joke.install文件并将其放到目录sites/all/modules/custom/joke下面。关于创建安装文件的更多信息,可参看第2章。
 
<?php
// $Id$
 
/**
 * Implementation of hook_install().
 */
function joke_install() {
    drupal_install_schema('joke');
}
 
/**
 * Implementation of hook_uninstall().
 */
function joke_uninstall() {
    drupal_uninstall_schema('joke');
}
 
/**
 * Implementation of hook_schema().
 */
function joke_schema() {
    $schema['joke'] = array(
        'description' => t("Stores punch lines for nodes of type 'joke'."),
        'fields' => array(
            'nid' => array(
                'type' => 'int',
                'unsigned' => TRUE,
                'not null' => TRUE,
                'default' => 0,
                'description' => t("The joke's {node}.nid."),
            ),
            'vid' => array(
                'type' => 'int',
                'unsigned' => TRUE,
                'not null' => TRUE,
                'default' => 0,
                'description' => t("The joke's {node_revisions}.vid."),
            ),
            'punchline' => array(
                'type' => 'text',
                'not null' => TRUE,
                'description' => t('Text of the punchline.'),
            ),
        ),
        'primary key' => array('nid', 'vid'),
        'unique keys' => array(
            'vid' => array('vid')
        ),
        'indexes' => array(
            'nid' => array('nid')
        ),
    );
 
    return $schema;
}
 

Drupal版本:

创建.info文件

老葛的Drupal培训班 Think in Drupal

让我们再创建一个joke.info文件并将其添加到joke文件夹下。
 
; $Id$
name = Joke
description = A content type for jokes.
package = Pro Drupal Development
core = 6.x
 

Drupal版本:

创建.module文件

老葛的Drupal培训班 Think in Drupal

最后,你需要创建模块文件本身。创建一个名为joke.module的文件并将其放到sites/all/modules/custom/joke下面。在你完成了这个模块以后,你可以在模块列表页面(管理➤站点构建 ➤模块)启用这一模块。你首先添加如下代码:
<?php
// $Id$
 
/**
 * @file
 * Provides a "joke" node type.
 */
 

Drupal版本:

提供我们节点类型的相关信息

老葛的Drupal培训班 Think in Drupal

现在你该向joke.module中添加钩子函数了。你想要实现的第一个钩子函数是hook_node_info().当Drupal发现有那些节点类型可用时,调用这个钩子函数。你将为你的自定义节点提供一些元数据。
/**
 * Implementation of hook_node_info().
 */
function joke_node_info() {
// We return an array since a module can define multiple node types.
// We're only defining one node type, type 'joke'.
return array(
'joke' => array(
'name' => t('Joke'), // Required.
'module' => 'joke', // Required.
'description' => t('Tell us your favorite joke!'), // Required.
'has_title' => TRUE,
'title_label' => t('Title'),
'has_body' => TRUE,
'body_label' => t('Joke'),
'min_word_count' => 2,
'locked' => TRUE
)
);
}
 
    由于在单个模块中可以定义多个节点类型,所以返回值应该是一个数组。下面是钩子hook_node_info()中可以提供的元数据的总结:
 
• name (必须的):显示在站点上的节点名称。例如,如果它的值为’Joke’,那么Drupal将使用该值,作为本节点提交表单的标题。
 
• module (必须的):Drupal要查找的回调函数的前缀名。我们这里使用了’joke’,所以Drupal将寻找以’joke’为前缀的回调函数,比如 joke_validate(), joke_insert(), joke_delete()等等。
 
• description:一般用于添加一个简短描述,以用来说明此内容类型可以干什么。该文本将会显示在“创建内容”页面的列表中(http://example.com/?q=node/add)。
 
• has_title:布尔值,用以说明此内容类型是否使用标题字段。默认为TRUE。
 
• title_label:在节点编辑表单中标题字段对应的文本标签。只有当has_title为TRUE时它才可见。默认为Title。
 
• has_body: 布尔值,用以说明此内容类型是否使用主体字段。默认为TRUE。
 
• body_label: 表单中body字段对应的文本标签。只有当has_body为TRUE时它才可见。默认为Body。
 
• min_word_count: body字段想要通过验证所需的最小单词个数。默认为0.(在我们的模块中,我们将其设置为2,以阻止一字笑话。)
 
• locked:布尔值,用以指示此内容类型的内部名称是否被锁定了;如果锁定了,那么站点管理员将不能修改它了;否则,管理员可以在“管理➤内容管理➤内容类型”中该内容类型的相应选项。默认为TRUE,这意味着名字被锁定,因此不可编辑。
 
注意: 在前面列表中提到的内部名称字段,是用来构造“创建内容”页面链接URL的。例如,我们使用“joke”作为我们节点类型的内部名称(它是我们返回的数组的键),如果要创建一个新的笑话的话,那么用户要访问页面http://example.com/?q=node/add/joke。通常你不需要通过将locked设置为FALSE,来对此作出修改。内部名称存储在表nodenode_revisions的“type”列中。
 

Drupal版本:

修改菜单回调

老葛的Drupal培训班 Think in Drupal

现在不需要实现钩子hook_menu()了(译者注:在Drupal5中,是必须的),在“创建内容”页面已经有了相应的链接。Drupal可以自动的发现你新创建的内容类型,并将它的条目添加到http://example.com/?q=node/add页面,如图7-2所示。而URL http://example.com/?q=node/add/joke就是指向节点提交表单的直接链接。这里的名字和描述都来源于你在joke_node_info()中所给的定义。
 
7-2.该内容类型出现在了http://example.com/node/add页面。
 
    如果你不想添加这个直接的链接,那么你可以使用hook_menu_alter()来移除它。例如,下面的代码,就可以为不具有“管理节点”权限的用户移除该页面。
 
/**
 * Implementation of hook_menu_alter().
 */
function joke_menu_alter(&$callbacks) {
    // If the user does not have 'administer nodes' permission,
    // disable the joke menu item by setting its access callback to FALSE.
    if (!user_access('administer nodes')) {
        $callbacks['node/add/joke']['access callback'] = FALSE;
        // Must unset access arguments or Drupal will use user_access()
        // as a default access callback.
        unset($callbacks['node/add/joke']['access arguments']);
    }
}
 

Drupal版本:

使用hook_perm()定义特定于节点类型的权限

老葛的Drupal培训班 Think in Drupal

一般情况下,由模块创建的节点类型的权限包括:创建该类型的一个节点,编辑你自己创建的节点,编辑该类型的任意节点。可以在hook_perm()中将它们定义为create joke, edit own joke, 和edit any joke,等等。你仍然需要在模块中定义这些权限。现在,让我们使用hook_perm()来创建这些权限:
 
/**
 * Implementation of hook_perm().
 */
function joke_perm() {
    return array('create joke', 'edit own joke', 'edit any joke', 'delete own joke','delete any joke');
}
    现在你可以导航到“管理➤用户管理 ➤访问控制”,你就可以看到你在上面定义的权限了,并且可以将它们分配给用户角色了。
 

Drupal版本:

使用hook_access()来限制对一个节点类型的访问

你在hook_perm()中定义了权限,但是它们是如何起作用的呢?节点模块可以使用hook_access()来限制对它们定义的节点类型的访问。超级用户(用户ID 1)将绕过所有的访问权限检查,所以对于超级用户则不会调这个钩子函数。如果没有为你的节点类型定义这个钩子函数,那么所有的访问权限检查都会失败,这样就只有超级用户和具有“管理节点”权限的用户,才能够创建、编辑、或删除该类型的内容。

 
/**
 * Implementation of hook_access().
 */
function joke_access($op, $node, $account) {
    $is_author = $account->uid == $node->uid;
    switch ($op) {
        case 'create':
            // Allow if user's role has 'create joke' permission.
            return user_access('create joke', $account);
 
        case 'update':
            // Allow if user's role has 'edit own joke' permission and user is
            // the author; or if the user's role has 'edit any joke' permission.
            return user_access('edit own joke', $account) && $is_author ||
                user_access('edit any joke', $account);
 
        case 'delete':
            // Allow if user's role has 'delete own joke' permission and user is
            // the author; or if the user's role has 'delete any joke' permission.
            return user_access('delete own joke', $account) && $is_author ||
                user_access('delete any joke', $account);
    }
}
 
    前面的函数允许具有“create joke”权限的用户创建一个笑话节点。如果用户还具有“edit own joke”权限并且他们是节点作者,或者如果他们具有'edit any joke'权限,那么他们还可以更新一个笑话。那些具有'delete own joke'权限的用户,他们可以删除自己创建的笑话;而具有'delete any joke'权限的用户,则可以删除joke类型的所有节点。
 

    在钩子函数hook_access()中, $op另一个可用的值是“view”(查看),它允许你控制谁可以查看该节点。然而我们需要提醒一下:当查看的页面仅有一个节点时,才调用钩子hook_access()。当节点处于摘要视图状态时,比如位于一个多节点列表页面,在这种情况下, hook_access()就无法阻止用户对该节点的访问。你可以创建一些其它的钩子函数,并直接操纵$node->teaser的值来控制对它的访问,但是这有点黑客的味道了。一个比较好的解决方案是,使用我们在后面即将讨论的函数hook_node_grants()hook_db_rewrite_sql()。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

为我们的节点类型定制节点表单

老葛的Drupal培训班 Think in Drupal

到目前为止,你已经为你的新节点类型定义了元数,还有定义了访问控制权限。接着,你需要构建节点表单,这样用户就可以输入笑话了。你可以通过实现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,
        '#maxlength' => 255,
    );
    $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' => isset($node->punchline) ? $node->punchline : '',
        '#weight' => 5
    );
    return $form;
}
 
注意:如果你不熟悉表单API的话,请参看第10章。
 
    作为站点管理员,如果你已经启用了你的模块,现在你就可以导航到“创建内容➤笑话”来查看新创建的表单了。在前面函数中,第一行代码返回了该节点类型的元数据信息。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修改这些值。
 
7-3.笑话的提交表单
 

Drupal版本:

添加过滤器格式支持

由于主体字段是一个textarea,并且对于节点主体字段可以使用过滤器格式,所以上面的表单中包含Drupal的标准内容过滤器,代码如下(过滤转换文本;使用过滤器的更多信息,可参看第11章):

$form['body_filter']['filter'] = filter_form($node->format);
 
    $node->format属性,指的是本节点body字段所用的过滤器格式的ID.这个属性存储在node_revisions表中。 如果你想让笑话妙语字段也可以使用过滤器格式,那么你就需要找个地方来存储该字段所用过滤器的信息.一个比较好的解决方案是, 在你的数据库表joke中再添加一个名为punchline_format的整数列, 来为每个笑话妙语存储过滤器格式。
 
    接着,将你的最后一个表单字段的定义修改成如下所示的形式:
 
$form['punchline']['field'] = array(
    '#type' => 'textarea',
    '#title' => t('Punchline'),
    '#required' => TRUE,
    '#default_value' => $node->punchline,
    '#weight' => 5
);
// Add filter support.
$form['punchline']['filter'] = filter_form($node->punchline_format);
 
    当你使用的是一个节点表单而不是一个普通表单时,node.module将处理节点表单中它所知道的默认字段的验证和存储工作(比如title和body字段---我们把后者改名为了Joke,但是节点模块仍然会把它作为节点主体字段进行处理);节点模块为你(开发者)提供了多个钩子,用来验证和存储你的自定义字段。
接下来我们将讨论这些钩子函数。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

使用hook_validate()来验证字段

老葛的Drupal培训班 Think in Drupal

当用户提交一个你节点类型的节点时,将会调用你模块中的钩子hook_validate()。因此,当用户提交该表单,来创建或者编辑一个笑话时,钩子hook_validate()将会查找joke_validate()函数,这样你就可以验证你的自定义字段中的输入了。在提交后,你也可以使用form_set_value()对数据做些修改。还可以使用form_set_error()来设置错误消息,如下所示:
/**
 * Implementation of hook_validate().
 */
function joke_validate($node) {
    // Enforce a minimum word length of 3 on punch lines.
    if (isset($node->punchline) && str_word_count($node->punchline) < 3) {
        $type = node_get_types('type', $node);
        form_set_error('punchline', t('The punch line of your @type is too short. You need at least three words.', array('@type' => $type->name)));
    }
}
 
    注意,你已经在hook_node_info()中为body字段定义了最小单词书目,而Drupal将自动对此进行验证。然而,punchline字段是你添加到该节点类型表单中的一个额外字段,所以你需要负责它的验证(加载、保存)。
 

Drupal版本:

使用hook_insert()来存储我们的数据

当一个新节点被保存时,会调用钩子insert()。在这个钩子中你可以将自定义数据存储到相关的表中。只有对于在节点类型元数据中定义的模块,才为其调用这一钩子。该信息定义在hook_node_info()的“module”键中(参看“提供我们节点类型的相关信息”一节)。例如,如果“module”键的值为joke,那么就会调用joke_insert()。如果你启用了书籍模块,并且新加了一个书籍类型的节点,此时就不会调用joke_insert();这里将调用的是book_insert(),这是因为book.module使用“module”键为book来定义了它的节点类型。

 
注意 如果你想在插入一个不同类型的节点时,对其做些操作的话,你需要把它当作一个普通的节点提交,使用钩子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);
}
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

使用hook_update()保持数据同步

老葛的Drupal培训班 Think in Drupal

当编辑完一个节点,并且节点核心数据已被写入到数据库中时,将会调用钩子update()。在这一钩子中可以编写对相关表的更新操作。和钩子hook_insert()一样,只有在节点为当前节点类型时才调用这个钩子。例如,如果节点类型在hook_node_info()中的“module”键为“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);
}
}
 
/**
 * Implementation of hook_update().
 */
function joke_update($node) {
    if ($node->revision) {
        // New revision; treat it as a new record.
        joke_insert($node);
    }
    else {
        db_query("UPDATE {joke} SET punchline = '%s' WHERE vid = %d",
        $node->punchline, $node->vid);
    }
}
 
在这里,你首先检查看是否设置了节点修订本标记,如果设置了,你为笑话妙语创建一个新的拷贝来替代旧的版本。
 

Drupal版本:

使用hook_delete()清理数据

老葛的Drupal培训班 Think in Drupal

在从数据库中删除一个节点以后,Drupal将会立即调用钩子hook_delete(),所有实现了这个钩子的模块都会被调用。该钩子一般用来从数据库中删除相关的信息。只有在删除当前节点类型的节点时,才调用这个钩子。如果节点类型在hook_node_info()中的“module”键为“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为键,来删除该修订本的数据。
 

Drupal版本:

使用hook_load()来修改节点对象

老葛的Drupal培训班 Think in Drupal

在你的joke模块中,另一个需要实现的钩子就是hook_load(),它可以在构建节点对象时向对象中添加你自定义的节点属性。我们需要把笑话妙语字段注入到节点加载流程中,这样就可以在其它模块中以及主题层中使用它了。此时使用hook_load()。
在构建完核心节点对象以后,并且加载的节点属于当前节点类型时,才会调用该钩子。如果节点类型在hook_node_info()中的“module”键为“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));
}
 

Drupal版本:

使用hook_view()显示笑话妙语

老葛的Drupal培训班 Think in Drupal

现在你有了一个完整的系统,可以用来输入和编辑笑话。然而,尽管可以在节点提交表单中输入笑话妙语,但在查看笑话时,你却没有提供显示笑话妙语字段的方式,你的用户将会为此感到很困惑。让我们使用hook_view()来显示笑话妙语:
 
/**
 * Implementation of hook_view().
 */
function joke_view($node, $teaser = FALSE, $page = FALSE) {
    // If $teaser is FALSE, the entire node is being displayed.
    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 punch line.
        $node->content['punchline'] = array(
            '#value' => theme('joke_punchline', $node),
            '#weight' => 2
        );
    }
 
    // If $teaser is TRUE, node is being displayed as a teaser,
    // such as on a node listing page. We omit the punch line in this case.
    if ($teaser) {
        // Use Drupal's default node view.
        $node = node_prepare($node, $teaser);
    }
 
    return $node;
}
 
    在这段代码中,如果节点没有被显示为摘要形式(也就是说,$teaser为FALSE),那么笑话中就会包含笑话妙语。你已将笑话妙语的显示分解为一个单独的主题函数,这样就可以方便的进行覆写了。如果一个系统管理员想使用你的模块,但又想自定义外观的话,那么这将会非常方便。通过实现hook_theme(),并提供一个joke_punchline主题函数的默认实现,这样你就告诉了Drupal,你要使用这个主题函数了。
 
/**
 * Implementation of hook_theme().
 * We declare joke_punchline so Drupal will look for a function
 * named theme_joke_punchline().
 */
function joke_theme() {
    return array(
        'joke_punchline' => array(
            'arguments' => array('node'),
        ),
    );
}
 
function theme_joke_punchline($node) {
    $output = '<div class="joke-punchline">'.
        check_markup($node->punchline). '</div><br />';
    $output .= '<div class="joke-guffaw">'.
        $node->guffaw .'</div>';
    return $output;
}
 

Drupal版本:

使用hook_view()显示笑话妙语(1)

老葛的Drupal培训班 Think in Drupal

你需要清除主题注册表的缓存,这样Drupal就能找到你的主题钩子了。清除缓存有多种方式,一种是使用devel.module,还有一种是简单的访问“管理➤站点构建 ➤模块”页面。现在你的笑话输入和查看系统,应该可以完整工作了。继续前进,输入一些笑话来测试一下。现在你应该可以看到你的笑话了,它看起来外观有点朴素,如图7-4和7-5所示:
 
7-4 笑话节点的简单主题
 
 
7-5.节点以摘要形式显示时,没有添加笑话妙语
 
    尽管这也可以工作,但还存在一个更好的方式,让用户可以在查看完整节点页面时能够立即看到笑话妙语。我们想要的是,使用一个可伸缩的字段集,当用户点击时再展示笑话妙语。在Drupa中,可伸缩字段集的功能已经存在了,所以你只需要使用现有的就可以了,而不需要创建你自己的Javascript文件了。把这个交互放到你站点主题的模板文件中,比放到主题函数中更好一些,因为它依赖于标识字体和CSS类。你的设计者很乐意看到你这样做,因为如果要修改笑话节点的外观的话,只需要简单的编辑模板文件就可以了。
    你需要创建一个名为node-joke.tpl.php的模板文件,并将其放到你当前使用的主题的目录下面,下面是该文件中的内容。如果你使用的主题为bluemarine,那么node-joke.tpl.php将被放到themes/bluemarine下面。由于我们将会使用一个模板文件,那么就不再需要实现钩子hook_theme()和函数theme_joke_punchline()了,所以我们就可以把它们注释掉了。记住,要像前面所讲的一样,再次清除主题注册表缓存,这样Drupal就不再查找函数theme_joke_punchline()了。由于模板文件将负责笑话妙语的输出,所以在joke_view()中,我们还可以将笑话妙语指定到$node->content中的那段代码注释掉(否则,笑话妙语会被显示两次)。
 
注意:访问“管理➤站点构建 ➤模块”页面以后(将会自动重构主题注册表),主题系统将会自动发现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 if (isset($node->punchline)) print check_markup($node->punchline)?></label>
                    <label><?php if (isset($node->guffaw)) print $node->guffaw?></label>
                </div>
                </legend>
            </fieldset>
        </div>
    <?php if ($links) { ?><div class="links">&raquo; <?php print $links?></div>
        <?php }; ?>
</div>
 
    Drupal将会自动地包含进来启用可伸缩功能的JavaScript文件。misc/collapsible.js中的JavaScript将为字段集查找可伸缩的CSS选择器,并且在找到以后知道如何处理它,如图7-6所示。这样,在node-joke.tpl.php中它将找到下面代码并激活它自己:
 
<fieldset class="collapsible collapsed">
 
    这就可以得到我们想要的笑话交互体验了:
 
7-6 使用Drupal内置的可伸缩CSS支持,来隐藏笑话妙语
 

Drupal版本:

使用hook_nodeapi()操纵其它类型的节点

老葛的Drupal培训班 Think in Drupal

前面的钩子只有在基于模块的hook_node_info()实现的“module”键时才调用。当Drupal看到一个blog节点类型时,那么将调用blog_load()。如果你想为每个节点都添加一些信息,不管它是什么类型,那该怎么办呢?我们到目前为止看到的钩子函数都做不到这一点;对于这一工作,我们需要一个更强大的钩子:hook_nodeapi()。
    这个钩子为模块提供了一个机会,来响应任意节点的生命周期期间的不同操作。node.module一般在调用完所有的特定节点类型的钩子函数以后,再调用钩子nodeapi()。下面是这个函数的签名:
 
hook_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL)
 
    因为节点对象$node是通过引用传递的,所以对它的任何修改都将改变真正的节点。参数$op用来描述对节点所进行的当前操作,它可以有多种不同的值:
 
• prepare: 即将显示节点表单。它适用于节点的添加和编辑表单。
 
• validate:用户刚刚完成对节点的编辑并试图预览或者提交它。在这里,你的代码应该进行检查,以确保数据是你期望;如果有地方出错了,那么就调用form_set_error(),它将为用户返回一个错误消息。你可以使用该钩子来检查甚至修改数据,当然,在验证钩子中修改数据通常被认为是坏的实践。
 
• presave:节点通过了验证,即将被保存到数据库中。
 
• insert:新节点刚被插入到了数据库中。
 
• update:节点刚被更新到了数据库中。
 
• delete:节点已被删除。
 
• delete revision:一个节点的修订本已被删除。如果模块中保存了与该修订本有关的数据,那么它需要对此作出反应。使用$node->nid可得到节点ID,使用$node->vid可得到该修订本的ID。
 
• load: 从数据库中加载了基本的节点对象,再加上由节点类型设置的额外的节点属性(为了响应已经运行的hook_load();参看本章前面的“使用hook_load()来修改节点对象”一节)。此时你可以添加新的属性或者操作已有的节点属性。
 
• alter:节点的内容已经通过了drupal_render(),并被保存到了$node->body(如果节点以完整的形式显示)或者node->teaser(如果节点以摘要的形式显示)中,并且节点即将被传递给主题层。模块可以修改这个完整构建的节点。但是对$node->content中字段的修改,应该放到查看操作中,而不是这个操作中。
 
• view: 节点即将被显示给用户。这个动作将在钩子hook_view()之后调用,所以模块可以假定节点已被过滤并且现在包含了HTML。其它项目也可被添加到$node->content(示例可参看,前面我们是如何添加笑话妙语的)。
 
• search result:节点即将作为一个搜索结果项目显示出来。
 
• update index:节点正在被搜索模块索引化。有些额外信息,使用nodeapi的“view”操作不能将其显示出来,如果你想对其进行索引的话,你可以在这里将它返回(参看第12章)。
 
• prepare translation:翻译模块正在准备对节点进行翻译。模块可以添加自定义的翻译了的字段。
 
• rss item:节点正在被作为RSS种子的一部分而包含进来。
 
    函数hook_nodeapi()的最后两个参数,它们的值将根据当前操作的不同而改变。当一个节点被显示并且$op为alter或者view时,$a3 将为$teaser,而 $a4将为$page(参看node.module中的node_view())。参数的总结可参看表7-1 。
 
7-1. $op为alter或者 view时,hook_nodeapi()中参数$a3 和 $a4的含义
参数       含义
$teaser     是否要仅仅展示teaser,比如在http://example.com/?q=node上。
$page       如果一个节点自己作为一个页面显示时,$pageTRUE(比如,                                          http://example.com/?q=node/2))
 
    当节点正被验证时, 参数$a3为node_validate()中的参数$form(也就是,表单定义数组)。
    显示一个节点页面时,比如http://example.com/?q=node/3,钩子的调用次序,如图7-7所示:
7-7. 显示一个节点页面的执行路径
 

Drupal版本:

如何存储节点

老葛的Drupal培训班 Think in Drupal

节点在数据库中被分成了3个部分。表node包含了描述节点的大部分元数据。表node_revisions包含了节点的主体和摘要,以及一些修订本特有的信息。正如你在joke.module例子中看到的,其它节点类型可以自由的在加载节点时向节点添加数据,同时可以在它们自己的表中存储它们想要的数据。
    图7-8展示了一个包含了大部分常用属性的节点对象。注意,你创建的用以存储笑话妙语的表,被用来填充节点。根据启用模块的不同,在你的Drupal安装里面,节点对象包含的属性可能会有或多或少的变化。
 
 
7-4 节点对象

Drupal版本:

使用CCK创建节点类型

老葛的Drupal培训班 Think in Drupal

在前面joke.module中,我们给大家展示了使用模块的方式来创建一个节点类型,尽管这种方式具有较高的自由度并且具有较高的性能,但是它却有点枯燥无味。如果不做任何编程工作,就可以组装一个新的节点类型的话,难道这样的方式不会更好么?这就是CCK提供的方式。
 
     注意:关于CCK的更多信息,访问CCK项目页面 http://drupal.org/project/cck
 
 
    现在,你可以导航到“管理内容管理内容类型”,通过后台管理界面页面添加一个新的内容类型(比如一个笑话(joke)内容类型)。如果你已经启用了joke.module,你要为节点类型起一个不同的名字以避免命名冲突。CCK中的其它部分尚未被添加到Drupal核心中,比如为新节点类型添加除标题和主体以外的其它字段的能力。在joke.module的例子中,你需要三个字段:标题,笑话本身,和笑话妙语。你使用Drupal的hook_node_info()将主体(body)字段改名为笑话(Joke);通过实现一些钩子函数,并创建你自己的数据库表来存储笑话妙语,从而提供了笑话妙语(punchline)字段。在CCK中,你可以简单的创建一个名为punchline的文本输入字段,并将其添加到你的内容类型中。CCK替你负责数据的存储、取回、和删除。
 
注意:Drupal第3方模块库中包含了大量的CCK字段模块,用来添加图片、日期、电子邮件、地址等字段。CCK相关第3方模块位于:http://drupal.org/project/Modules/category/88
 
       由于在编写本书时,CCK的许多地方还在开发和完善中,所以我们在这里不讨论更多细节了。然而,可以清晰的看到,在将来,使用编写模块的方式会越来越少,而使用CCK的方式(通过管理界面来组装一个新的节点类型)则会越来越流行。
 

Drupal版本:

限制对节点的访问

老葛的Drupal培训班 Think in Drupal

有多种方式可用于限制对节点的访问。你已经看到了,如何使用hook_access()来限制对一个节点类型的访问,以及使用hook_perm()定义权限。但是Drupal提供了用于控制访问的更丰富的工具集:使用表node_access以及两个访问钩子函数hook_node_grants()和hook_node_access_records()。
 
    当Drupal初次安装时,将会向node_access表中写入一条记录,它将有效的关闭节点访问机制。只有当使用了节点访问机制的模块被启用时,才会用到Drupal的这一部分。位于modules/node/node.module中的函数node_access_rebuild()可用来追踪启用了哪些节点访问模块,如果这些模块都被禁用了,那么这个函数还可以恢复默认记录,如表7-2所示。
 
7-2. node_access表的默认记录
nid gid realm grant_view grant_update grant_delete
0   0   all      1           0            0
 
    一般情况下,如果一个节点访问模块正被使用(也就是说,它修改了标node_access),如果它没有向表node_access插入一行记录,用来定义如何处理访问,那么Drupal将拒绝对节点的访问。
 

Drupal版本:

定义节点授权(Grants)

老葛的Drupal培训班 Think in Drupal

有三个基本的权限,对应于节点之上的三种操作:查看、更新、删除。当这些操作中的一个将要发生时,如果一个模块实现该节点类型,将首先使用这个模块里面的函数node_access()。如果该模块没有定义是否允许访问的话(也就是说,它返回了NULL,而不是TRUE或FALSE),Drupal将向所有应用于节点访问控制的模块询问,这个操作是否应该被允许进行。通过使用hook_node_grants(),为每个领域(realm)每个用户得到一个授权(grant)ID列表,来完成这一工作。

Drupal版本:

什么是领域(Realm)?

 

领域就是一个任意的字符串,它用于允许多个节点访问控制模块共享数据库表node_access。例如,acl.module是一个使用访问控制列表(ACLs)来管理节点访问的第三方模块,它的领域就是acl。taxonomy_access.module是另一个第3方模块,它基于分类术语来限制对节点的访问,它使用term_access作为领域。所以,领域就是在表node_access中标识你的模块空间的东西;它有点像命名空间。当需要你的模块返回许可ID时,你将从你模块定义的领域中返回它。
老葛的Drupal培训班 Think in Drupal

Drupal版本:

什么是授权(Grant)ID

老葛的Drupal培训班 Think in Drupal

一个授权ID是一个标识符,用于为一个给定领域,提供节点访问控制权限方面的信息。例如,一个节点访问控制模块----比如forum_access.module,它根据用户角色,来管理对论坛类型节点的访问----可以使用角色ID作为授权ID。一个使用US ZIP代码来管理对节点访问的模块,可以使用ZI P代码作为授权ID。在每种情况下,都是用与用户有关的东西作为授权ID,比如该用户拥有这个角色么?这个用户的ZIP是12345么?用户是在这个访问控制列表中么?或者,这个用户定阅的时间超过1年了么?
    节点访问模块为包含授权ID的领域提供了授权ID,尽管每个授权ID都特定于它的节点访问模块,但是它们的底层原理是一样的,那就是在node_access表中,如果存在一条包含了授权ID的记录的话,那么就启用访问,访问的类型则可以通过grant_view, grant_update, 或者grant_delete列中,值为1的那个进行判定.
    在一个节点正在保存时,会将授权ID插入到表node_access中。将这个节点对象传递给实现了钩子hook_node_access_records()的每个模块。模块将检查节点,或者简单的返回(如果它不用为该节点处理访问控制),或者返回一个数组,里面包含了用于插入到表node_access中的授权。使用node_access_acquire_grants()可以批量的插入授权。下面是一个来自于forum_access.module的例子。
 
/**
 * Implementation of hook_node_access_records().
 *
 * Returns a list of grant records for the passed in node object.
 */
function forum_access_node_access_records($node) {
...
if ($node->type == 'forum') {
$result = db_query('SELECT * FROM {forum_access} WHERE tid = %d', $node->tid);
while ($grant = db_fetch_object($result)) {
$grants[] = array(
'realm' => 'forum_access',
'gid' => $grant->rid,
'grant_view' => $grant->grant_view,
'grant_update' => $grant->grant_update,
'grant_delete' => $grant->grant_delete
);
}
return $grants;
}
}

Drupal版本:

节点访问流程

老葛的Drupal培训班 Think in Drupal

当要对节点进行某一操作时,Drupal将进入如图7-9所示的流程。
 
7-9 判定是否允许对给定节点的访问
 

Drupal版本:

总结

读完本章后,你应该可以

老葛的Drupal培训班 Think in Drupal

 

• 理解什么是节点,什么是节点类型
• 编写模块来创建节点类型
• 理解如何在节点创建、保存、加载等等操作时插入钩子函数
• 理解如何判定对节点的访问控制
 

Drupal版本: