第3章 drupal 钩子,动作,和触发器

使用Drupal时,一个常见的目标就是,当一个事件发生时需要做些东西。例如,站点管理员可能希望在一个消息发布以后收到一封电子邮件。或者当用户在评论中使用了违禁词语,那么就会被自动封号。本章将描述如何使用Drupal的事件钩子,从而当那些事件发生时,能够运行自己的代码。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

理解事件和触发器

老葛的Drupal培训班 Think in Drupal

Drupal在运行自己的业务时,需要处理一系列的事件。而这些内部事件实际就是一些时机,模块在此时能够与Drupal的处理流程进行交互。表3-1给出了一些Drupal事件。
 
3-1. Drupal事件示例
事件                  类型
创建一个节点            节点
删除一个节点            节点
查看一个节点            节点
创建一个用户帐号        用户
更新用户个人资料        用户
登录                    用户
登出                    用户
 
    Drupal开发者将这些内部事件称为钩子,这是因为当一个事件发生时,Drupal允许模块从该点钩进Drupal的执行路径。我们在前面的章节中,应该也见到了一些钩子。在模块开发中,一般都会涉及到这种情况----判定对哪个Drupal事件做出反应,也就是说,你需要在你的模块中实现哪些钩子。
 
    假定你有一个刚刚起步的网站,而网站所在的主机则被你放到了你的地下室中,开始都有点简陋。一旦你的网站有了人气,你可能就会打算把它卖给一个大公司,继而一夜暴富。在网站迈向成功的期间,你可能想监督用户的每次登录,用户的每次登录,都能给你带来一点希望。你决定,当有一个用户登录时,你的计算机就会发出嘟嘟嘟嘟的声音。不过你的小猫也住在地下室,为了避免嘟嘟声打扰了小猫的美梦,你决定使用一个简单的日志条目来模拟嘟嘟声。你快速的编写了一个.info文件,并将其放在了sites/all/modules/custom/beep/beep.info:
 
; $Id$
name = Beep
description = Simulates a system beep.
package = Pro Drupal Development
core = 6.x
 
    接着,该编写sites/all/modules/custom/beep/beep.module了:
<?php
// $Id$
/**
* @file
* Provide a simulated beep.
*/
function beep_beep() {
    watchdog('beep', 'Beep!');
}
 
    这将向Drupal的日志中写入一条消息“Beep!”(“嘟嘟!”)。现在已经不错了。接着,我们需要告诉Drupal当用户登录时发出嘟嘟声。通过在我们的模块中实现hook_user(),并将逻辑添加到login操作中,我们就可以完成目标了:
 
/**
* Implementation of hook_user().
*/
function beep_user($op, &$edit, &$account, $category = NULL) {
    if ($op == 'login') {
        beep_beep();
    }
}
    简单吧!如果添加了新内容时,也要发出嘟嘟声,那该怎么办呢?通过在我们的模块中实现hook_nodeapi(),并将逻辑添加到insert操作中,这样也就完成目标了:
/**
* Implementation of hook_nodeapi().
*/
function hook_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
    if ($op == 'insert') {
        beep_beep();
    }
}
 
    如果我们想在添加评论时,也让Drupal发出嘟嘟声,那又该怎么办呢?好的,我们可以实现hook_comment(),并将逻辑添加到insert操作中,但是让我们暂停一下,并好好的思考一下。其实我们在重复的做着同样的一件事情。如果有那么一个图形化的用户界面,在那里我们可以将嘟嘟这个动作关联到我们想要的钩子和操作上,那该多好啊?这就是Drupal内置的触发器模块所要实现的功能。它允许你将一些动作与特定事件关联起来。在代码中,事件就是一个唯一的钩子操作联合体,比如“用户钩子, 登录操作”或者“nodeapi钩子, 插入操作”。当这些操作发生时,trigger.module就会让你触发一个动作。
 
    为了避免概念的混淆,让我们明确给出我们所用的术语:
事件:一个一般的编程概念,这个术语一般理解为:从系统的一个构件向其它构件发送一个消息。
钩子:这个编程技术,用在Drupal中,就是允许模块“钩进”执行流程。
操作:它指的是在钩子内部运行的一个具体的流程。例如,登录操作就是用户钩子中的一个操作。

触发器:它指的是,一个具体的钩子操作联合体与一个或多个动作的关联。例如,嘟嘟这个动作可以与用户钩子的登录操作关联起来。

理解动作

    一个动作就是Drupal要做的一些事情。下面是一些例子:

• 将节点推到首页
• 将节点从未发布状态改为发布状态
• 删除一个用户
• 发送一封电子邮件
    这里面的每一种情况,都包含了一个定义明确的任务。程序员可能会注意到,前面列表中所给的这些动作与PHP函数有点类似。例如,你可以调用includes/mail.inc中的drupal_mail()函数来发送一封电子邮件。动作听起来与函数类似,其实动作就是函数。它们是一些特殊的函数:Drupal可以通过自省将其与事件关联起来(我们一会儿将对此详细介绍)。现在,让我们看看触发器模块。
 
老葛的Drupal培训班 Think in Drupal
 

Drupal版本:

触发器用户界面

老葛的Drupal培训班 Think in Drupal

导航到“管理➤站点构建 ➤模块”,并启用触发器模块。接着导航到“管理➤站点构建 ➤触发器”。你将看到的界面应该与图3-1所示的类似。
3-1.触发器分配界面
    注意顶部横向的标签。它们对应于Drupal钩子!在图3-1中,我们查看的是nodeapi钩子的各种操作。它们的命名都很容易理解;比如,nodeapi钩子的delete操作就标注为“在删除文章之后”。对于钩子中的每个操作,在操作发生时,都可以为其分配一个动作,比如“将文章推到首页”。而每个可用的动作都列在了名为“选择一个动作”的下拉选择框中。
 
注意 不是所有的动作对所有的触发器都可用,这是因为有些动作在特定的上下文中没有任何意义。例如,在触发器“在删除文章之后”中,你就不能使用“将文章推到首页”这个动作。根据你的安装,有些触发器可能会显示“没有为该触发器可用的动作”。
 
    表3-2给出了一些触发器名字和它们对应的钩子和操作。
 
3-2. Drupal 6中,钩子,操作,触发器的对应关系
钩子       操作       触发器名字
comment     insert      在保存新的评论之后
comment     update      在更新评论之后
comment     delete      在删除评论后
comment     view        当评论正在被注册用户查看时
cron        run         cron 运行时
nodeapi     presave     当保存新文章或更新文章时
nodeapi     insert      在保存新文章之后
nodeapi     update      在更新文章之后
nodeapi     delete      在删除文章之后
nodeapi     view        在内容被注册用户查看时
taxonomy    insert      在将新术语存储到数据库之后
taxonomy    update      在将更新过的术语存储到数据库之后
taxonomy    delete      在删除一个术语后
user        insert      在用户帐户创建之后
user        update      在用户资料更新之后
user        delete      在用户被删除之后
user        login       在用户登录之后
user        logout      在用户退出之后
user        view        当用户资料被浏览时
 

Drupal版本:

你的第一个动作

如果将我们的嘟嘟函数转化为一个完整的动作,那么我们需要做哪些工作呢?这有两个步骤:

1. 通知Drupal该动作所支持的触发器。
2. 创建你自己的动作函数。
 
    第一步就是实现hook_action_info()。下面给出beep模块中该钩子的实现:
/**
* Implementation of hook_action_info().
*/
function beep_action_info() {
    $info['beep_beep_action'] = array(
        'type' => 'system',
        'description' => t('Beep annoyingly'),
        'configurable' => FALSE,
        'hooks' => array(
            'nodeapi' => array('view', 'insert', 'update', 'delete'),
            'comment' => array('view', 'insert', 'update', 'delete'),
            'user' => array('view', 'insert', 'update', 'delete', 'login'),
            'taxonomy' => array('insert', 'update', 'delete'),
        ),
    );
    return $info;
}
 
    该函数的名字为beep_action_info(),在这里,和其它的钩子实现一样,我们使用了:模块名(beep)+钩子名(action_info)。我们将返回一个数组,数组中的一个条目就对应我们的模块中的一个动作。由于我们只编写了一个动作,所以只有一个条目,它的键就是执行动作的函数的名字:beep_beep_action()。为了在阅读代码时,方便的识别哪个函数是个动作,我们在我们的beep_beep()函数的名字后面追加了_action,这样就成了beep_beep_action()。
 
让我们仔细的看一下数组中的键:
• type: 这是你编写的动作的类型。Drupal使用该信息,将动作归类到触发器分配界面的下拉选择框中。可能的类型包括system, node, user, comment, 和taxonomy。在判定你编写的动作的类型时,你需要好好的想一想,“这个动作作用于什么对象呢?”(如果答案不确定,或者是“各种不同的对象!”,那么可以使用system类型)。
• description:这是该动作的描述性名字,它显示在触发器分配界面的下拉选择框中。
• configurable:这个是用来判定该动作是否带有参数的。
• hooks: 在这个钩子数组中,每个条目都是用来列举该动作所支持的操作的。Drupal使用这一信息,来判定该动作在触发器分配界面中的位置。
 
    我们已经向Drupal描述了我们的动作,所以让我们继续:
/**
* Simulate a beep. A Drupal action.
*/
function beep_beep_action() {
    beep_beep();
}
 
    这也不是太难吧,不是么?在继续往下以前,由于我们将使用触发器和动作来取代直接的钩子实现,所以让我们回过头来将beep_user()和beep_nodeapi()删除。
 
老葛的Drupal培训班 Think in Drupal

Drupal版本:

分配该动作

现在,让我们重新回到“管理➤站点构建 ➤触发器”。如果你正确的完成了前面所给的这些东西,那么你的动作将会出现在这个用户界面,如图3-2所示。

3-2.该动作出现在了触发器用户界面的下拉选择框中
老葛的Drupal培训班 Think in Drupal

Drupal版本:

修改动作所支持的触发器

老葛的Drupal培训班 Think in Drupal

如果你修改了该动作所支持的触发器,那么你就能够在用户界面中看到相应的变化。例如,如果你将beep_action_info()改成如下的内容,那么你的“Beep”动作只能用于触发器“在删除文章之后”:

/**
* Implementation of hook_action_info().
*/
function beep_action_info() {
    $info['beep_beep_action'] = array(
        'type' => 'system',
        'description' => t('Beep annoyingly'),
        'configurable' => FALSE,
       'hooks' => array(
           'nodeapi' => array('delete'),
        ),
    );
    return $info;
}

Drupal版本:

支持所有触发器的动作

 

如果你不想将你的动作限定在特定的触发器或者触发器集上,那么通过以下声明,你的动作就支持所有的触发器了:
/**
* Implementation of hook_action_info().
*/
function beep_action_info() {
    $info['beep_beep_action'] = array(
        'type' => 'system',
        'description' => t('Beep annoyingly'),
        'configurable' => FALSE,
        'hooks' => array(
           'any' => TRUE,
        ),
    );
    return $info;
}
老葛的Drupal培训班 Think in Drupal
 

 

Drupal版本:

高级动作(1)

动作主要有两种类型:带有参数的动作和不带参数的动作。我们前面所写的“嘟嘟”动作就是不带参数的动作。当动作执行时,它嘟嘟一下,这就完事了。但是许多时候,动作可能需要更多一点的上下文。例如,一个“发送电子邮件”动作,需要知道将电子邮件的发收件人以及邮件的标题和正文。这种需要为其在配置表单中做些设定的动作,就是高级动作,也称为可配置动作

 
    简单的动作不带参数,也不需要配置表单,并且能够自动出现在触发器分配界面(访问“管理➤站点构建 ➤模块”以后)。如果你想告诉Drupal你的动作是一个高级动作的话,你需要进行以下步骤:在你模块的hook_action_info()实现中将configurable键设置为TRUE;提供一个表单用来配置该动作;提供一个可选的验证处理器和一个必须的提交处理器来处理配置表单。表3-3对简单动作和高级动作的区别进行了总结。
 
3-3.简单和高级动作的不同之处的总结
                     简单动作          高级动作
参数                    无*                 必须的
配置表单                无                  必须的
可用性                  自动                需使用动作管理界面创建动作实例
hook_action_info()      FALSE               TRUE
configure的值
* 如果需要的话,可以使用$object和$context参数。
 
    让我们创建一个可以嘟嘟多次的高级动作。我们可以使用一个配置表单来指定该动作嘟嘟的次数。首先,我们需要告诉Drupal这个动作是可配置的。让我们在beep.module的action_info钩子实现中,为我们的新动作添加一个条目:
/**
* Implementation of hook_action_info().
*/
function beep_action_info() {
    $info['beep_beep_action'] = array(
        'type' => 'system',
        'description' => t('Beep annoyingly'),
        'configurable' => FALSE,
        'hooks' => array(
            'nodeapi' => array('delete'),
        ),
    );
    $info['beep_multiple_beep_action'] = array(
       'type' => 'system',
       'description' => t('Beep multiple times'),
       'configurable' => TRUE,
       'hooks' => array(
           'any' => TRUE,
       ),
    );
    return $info;
}
老葛的Drupal培训班 Think in Drupal

Drupal版本:

高级动作(2)

让我们快速的检查一下,我们的实现是否正确,导航到“管理➤站点配置➤动作”。不错,动作出现在了高级动作的下拉选择框中了,如图3-3所示。

 
3-3.新动作显示在了高级动作的下拉选择框中
 
    现在,我们需要提供一个表单,这样管理员就可以选择嘟嘟多少次了。通过使用Drupal的表单API,来定义一个或多个字段,我们就可以实现这一点。我们还需要编写表单的验证函数和提交函数。它们的名字是基于hook_action_info()中所定义的动作ID的。我们当前讨论的动作的动作ID为beep_multiple_beep_action,所以按照约定,我们在后面追加_form,这样就得到了表单定义的函数名字beep_multiple_beep_action_form。Drupal期望的验证函数名字为:动作ID+ _validate(beep_multiple_beep_action_validate);提交函数的名字为:动作ID+ _submit(beep_multiple_beep_action_submit)。
 
/**
* Form for configurable Drupal action to beep multiple times.
*/
function beep_multiple_beep_action_form($context) {
    $form['beeps'] = array(
        '#type' => 'textfield',
        '#title' => t('Number of beeps'),
        '#description' => t('Enter the number of times to beep when this action
            executes.'),
        '#default_value' => isset($context['beeps']) ? $context['beeps'] : '1',
        '#required' => TRUE,
    );
    return $form;
}
 
function beep_multiple_beep_action_validate($form, $form_state) {
    $beeps = $form_state['values']['beeps'];
    if (!is_numeric($beeps)) {
        form_set_error('beeps', t('Please enter a numeric value.'));
    }
    else if ((int) $beeps > 10) {
        form_set_error('beeps', t('That would be too annoying. Please choose fewer
            than 10 beeps.'));
    }
}
 
function beep_multiple_beep_action_submit($form, $form_state) {
    return array(
        'beeps' => (int) $form_state['values']['beeps']
    );
}
老葛的Drupal培训班 Think in Drupal

Drupal版本:

高级动作(3)

老葛的Drupal培训班 Think in Drupal

第一个函数向Drupal描述了表单。我们只定义了一个文本输入框字段,这样管理员就可以输入嘟嘟的次数了。当管理员选择添加一个高级动作“嘟嘟多次”时,如图3-3所示,Drupal将会使用我们的表单字段来呈现一个完整的动作配置表单,如图3-4所示。

3-4.动作“嘟嘟多次”的动作配置表单
 
    Drupal向动作配置表单添加了一个描述字段。该字段的值是可编辑的,它将用来替代我们在action_info钩子中定义的默认描述。这是有意义的,因为我们可以创建一个高级动作用来嘟嘟两次并将其描述为“嘟嘟两次”,然后创建另一个用来嘟嘟五次并将其描述为“嘟嘟五次”。这样,在将动作分配给一个触发器时,我们就指出了这两个高级动作之间的区别。这样,高级动作的描述对于管理员来说就是很有意义的。
 
提示 这两个动作,“嘟嘟两次”和“嘟嘟五次”,可以看作是“嘟嘟多次”动作的实例。
 
    验证函数和Drupal中其它的表单验证函数一样(关于表单验证的更多详细,请参看第10章)。在这里,我们对用户的输入作了检查,以确保用户输入的是一个数字并且该数字不是特别大。
 
    提交函数的返回值是特定于动作配置表单的。它应该是一个数组,其中以我们关心的字段为键。这个数组中的值在动作运行时可供动作使用。描述是由系统自动处理的,所以我们只需要返回我们提供的字段就可以了,在这里也就是,嘟嘟的次数。
 
    最后,该编写高级动作本身了:
/**
* Configurable action. Beeps a specified number of times.
*/
function beep_multiple_beep_action($object, $context) {
    for ($i = 1; $i < $context['beeps']; $i++) {
        beep_beep();
    }
}
    你会注意到这个动作有两个参数,$object和$context。而我们前面所写的简单动作中,就没有带参数,二者在这一点上有点不同。
 
注意 简单动作也可以和高级动作一样,带有参数。由于PHP会忽略掉传递给函数但是没有出现在函数签名中的参数,如果我们需要了解当前的上下文信息,那么可以简单的将我们的简单动作的函数签名从beep_beep_action()改为beep_beep_action($object, $context)。所有的动作都可以使用$object和$context参数。
 

Drupal版本:

在动作中使用上下文

老葛的Drupal培训班 Think in Drupal

我们在前面已经看到,动作的函数签名的一般形式为example_action($object,$context)。下面让我们学习一下这些参数的具体含义。

• $object: 许多动作都是作用于Drupal的一个内置对象的:节点、用户、分类术语、等等。当trigger.module执行动作时,被作用的对象就会通过参数$object传递给动作。例如,如果一个动作被设置为在新节点创建时执行的话,那么$object参数包含的就是节点对象。
• $context: 一个动作可以在许多不同的上下文中被调用。通过在hook_action_info()中定义hooks键,动作就可以声明它们所支持的触发器。但是支持多个触发器的动作,需要使用一些方式来判定它们被执行时所处的上下文。这样,根据上下文的不同,动作会做出不同的反应。
 

Drupal版本:

触发器模块是如何准备上下文的

老葛的Drupal培训班 Think in Drupal

让我们设定一个场景。假定你有一个网站,是用来呈现争议性问题的。下面是它的业务模型:用户通过付费注册进来,并只能在网站上发布一条评论。一旦他们发布了评论,他们就会被封号,直到再次付费后才被解封。我们不关心这样的网站是否有经济前景,这里主要考虑的是:如何使用触发器和动作来实现它。我们需要一个动作来阻止当前用户。检查一下user.module,我们看到Drupal已经为我们提供了这个动作:
 
/**
* Implementation of hook_action_info().
*/
function user_action_info() {
    return array(
        'user_block_user_action' => array(
            'description' => t('Block current user'),
            'type' => 'user',
            'configurable' => FALSE,
            'hooks' => array(),
        ),
        'user_block_ip_action' => array(
            'description' => t('Ban IP address of current user'),
            'type' => 'user',
            'configurable' => FALSE,
            'hooks' => array(),
        ),
    );
}
 
    然而,这些动作却没有显示在触发器分配页面,为什么呢?这是因为它们的hooks键是一个空数组,也就是它们不支持任何钩子。如果我们能只改一下hooks键,那不就可以了?不错,可以这样做,让我们往下看。

Drupal版本:

使用drupal_alter()修改已有的动作

Drupal运行action_info钩子时,每个模块都可以声明它所提供动作,Drupal还给了模块一个机会,让它们修改该信息----包括其它模块提供的信息。下面让我们修改“阻止当前用户”这个动作,让它可以用于评论插入这个触发器:

 
/**
* Implementation of hook_drupal_alter(). Called by Drupal after
* hook_action_info() so modules may modify the action_info array.
*
* @param array $info
* The result of calling hook_action_info() on all modules.
*/
function beep_action_info_alter(&$info) {
    // Make the "Block current user" action available to the
    // comment insert trigger. If other modules have modified the
    // array already, we don't stomp on their changes; we just make sure
    // the 'insert' operation is present. Otherwise, we assign the
    // 'insert' operation.
    if (isset($info['user_block_user_action']['hooks']['comment'])) {
        array_merge($info['user_block_user_action']['hooks']['comment'],
            array('insert'));
    }
    else {
        $info['user_block_user_action']['hooks']['comment'] = array('insert');
    }
}
 
    最终的结果就是,“阻止当前用户”这个动作现在可被分配了,如图3-5所示。
3-5.将动作“阻止当前用户”分配给评论插入触发器
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

建立上下文

由于我们已经分配了动作,所以当一个新评论被发布时,当前用户将被阻止。让我们仔细的看一下,这里都发生了什么。我们已经知道,Drupal是通过触发钩子来向模块通知特定事件的。在这里触发的就是评论钩子。由于当前是一个新评论正被添加进来,所以当前的特定操作就是insert操作。触发器模块实现了评论钩子。在这个钩子内部,它对数据库进行查询,来获取分配到这个特定触发器上的所有动作。数据库就会将我们分配的动作“阻止当前用户”返回给该钩子。现在,触发器模块就可以执行该动作了,它符合标准的动作函数签名example_action($object, $context)。

 
    但是我们又有了一个问题。当前要被执行的动作是一个用户类型的动作,而不是评论类型的。所以它期望接收到的对象是一个用户对象!但是在这里,一个用户动作在一个评论钩子的上下文中被调用了。与评论相关的信息被传递给了钩子,而传递的不是与用户相关的信息。那么我们该怎么办呢?实际上发生的是,触发器模块会判定我们的动作是一个用户动作,并加载用户动作所需的$user对象。下面是来自modules/trigger/trigger.module的代码,它给出了这是如何实现的:
 
/**
* When an action is called in a context that does not match its type,
* the object that the action expects must be retrieved. For example, when
* an action that works on nodes is called during the comment hook, the
* node object is not available since the comment hook doesn't pass it.
* So here we load the object the action expects.
*
* @param $type
* The type of action that is about to be called.
* @param $comment
* The comment that was passed via the comment hook.
* @return
* The object expected by the action that is about to be called.
*/
function _trigger_normalize_comment_context($type, $comment) {
    switch ($type) {
    // An action that works with nodes is being called in a comment context.
    case 'node':
        return node_load($comment['nid']);
 
    // An action that works on users is being called in a comment context.
    case 'user':
        return user_load(array('uid' => $comment['uid']));
    }
}
 
    当为我们的用户动作执行前面的代码时,匹配的是第2种情况,所以将会加载用户对象并接着执行我们的用户钩子。评论钩子所知道的信息(比如,评论的标题)将会通过$context参数传递给动作。注意,动作是如何查找用户ID的----首先在对象中查找,其次在上下文中查找,最后使用全局变量$user:
/**
* Implementation of a Drupal action.
* Blocks the current user.
*/
function user_block_user_action(&$object, $context = array()) {
    if (isset($object->uid)) {
        $uid = $object->uid;
    }
    elseif (isset($context['uid'])) {
        $uid = $context['uid'];
    }
    else {
        global $user;
        $uid = $user->uid;
    }
    db_query("UPDATE {users} SET status = 0 WHERE uid = %d", $uid);
    sess_destroy_uid($uid);
    watchdog('action', 'Blocked user %name.', array('%name' =>
        check_plain($user->name)));
}
 
    动作必须要聪明一点,因为当它们被调用时它们并不知道发生了什么。这就是为什么,动作最好是直接的,甚至是原子的。触发器模块总是将当前的钩子和操作放在上下文中,通过上下文将其传递过来。它们的值存储在$context['hook'] 和$context['op']中。这种方式是向动作传递信息的标准方式。
 
老葛的Drupal培训班 Think in Drupal

Drupal版本:

检查上下文

将钩子和操作放在上下文中,这一点非常有用。我们举个例子,动作“发送电子邮件”就大量的利用了这一点。这个动作的类型为system,它可以被分配给许多不同的触发器。

 
    动作“发送电子邮件”在合成电子邮件期间,允许将特定的令牌替换掉。例如,你可能想在邮件的正文中包含一个节点的标题,或者想把节点的作者作为电子邮件的收件人。但是根据该动作分配给的触发器的不同,该收件人可能并不可用。例如,如果是在用户钩子中发送的电子邮件,由于没有节点可用,所以更谈不上让节点作者作为收件人了。modules/system/system.module中的动作“发送电子邮件”,它首先会花点时间来检查上下文从而判定有什么可用。下面,它将确保当前有一个节点,这样就可以利用节点相关的各种属性了:
 
/**
* Implementation of a configurable Drupal action. Sends an e-mail.
*/
function system_send_email_action($object, $context) {
    global $user;
    switch ($context['hook']) {
        case 'nodeapi':
            // Because this is not an action of type 'node' (it's an action
            // of type 'system') the node will not be passed as $object,
            // but it will still be available in $context.
            $node = $context['node'];
            break;
        case 'comment':
            // The comment hook provides nid, in $context.
            $comment = $context['comment'];
            $node = node_load($comment->nid);
        case 'user':
            // Because this is not an action of type 'user' the user
            // object is not passed as $object, but it will still be
            // available in $context.
            $account = $context['account'];
            if (isset($context['node'])) {
                $node = $context['node'];
            }
            elseif ($context['recipient'] == '%author') {
                // If we don't have a node, we don't have a node author.
                watchdog('error', 'Cannot use %author token in this context.');
                return;
            }
            break;
        default:
            // We are being called directly.
            $node = $object;
    } ...
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

动作的存储

老葛的Drupal培训班 Think in Drupal

动作就是在给定时间运行的函数。简单的动作不带有可配置的参数。例如,我们创建的动作“嘟嘟”只会简单的嘟嘟一下。它不需要任何其它的信息(当然,如果需要的话还是可以使用$object和$context。)将这个动作与我们创建的高级动作相比,那么动作“嘟嘟多次”就需要知道嘟嘟的次数了。而其它的高级动作,比如动作“发送电子邮件”,可能需要更多的信息:电子邮件的收件人,电子邮件的主题,等等。这些参数都需要存储在数据库中。
 
表actions
    当管理员创建一个高级动作的实例时,在配置表单输入的信息将被序列化并保存到actions表的parameters字段中。简单动作“嘟嘟”的数据库记录应该是这样的:
aid: 'beep_beep_action'
type: 'system'
callback: 'beep_beep_action'
parameters:
description: Beep
 
    相反,动作“嘟嘟多次”的一个实例对应的数据库记录应该是这样的:
aid: 2
type: 'system'
callback: 'beep_beep_action'
parameters: (serialized array containing the beeps parameter with its value, i.e.,
the number of times to beep)
description: Beep three times
 
    在一个高级动作被执行前,parameters字段中的内容将被反序列化,并被包含在$context参数中,从而传递给该动作。所以,在我们的动作“嘟嘟多次”的实例中,在beep_multiple_beep_action()中通过$context['beeps'] 就可以取得嘟嘟的次数了。
 
动作ID
    注意,在前面的部分中,两条记录的动作ID之间的不同。简单动作的动作ID就是实际的函数名字。但是,很明显,对于高级动作,因为可能会存储一个动作的多个实例,所以我们在这里不能为其使用函数名作为标识。因此在这里使用了一个数字动作ID(存放在数据库表actions_aid中的)。
 
    动作执行引擎,会基于动作ID是不是数字,来判定是否需要为其取出存储的参数。如果它不是数字,那么动作就被简单的执行了,这样就不需要再查询数据库了。这是一个非常迅速的判定;Drupal在index.php中就使用了同样的方式,来区分内容和菜单常量。
 

Drupal版本:

直接使用actions_do()来调用一个动作

触发器模块仅是调用动作的一种方式。你可能想写一个单独的模块,它需要自己负责动作的调用和参数的准备。如果是这样的话,那么推荐使用actions_do()来调用动作。函数的签名如下:

actions_do($action_ids, &$object, $context = array(), $a1 = NULL, $a2 = NULL)
 
让我们学习一下里面的参数:
• $action_ids: 要执行的动作,既可以是单个动作ID,也可以是一个包含动作ID的数组。
• $object: 该动作要作用的对象,如果存在的话。
• $context:一个关联数组,里面包含了动作可能想要使用的信息,对于高级动作里面会包含配置参数。
• $a1 and $a2:可选的额外参数,如果传递给了actions_do(),那么也将会传递给该动作。
 
    下面是我们如何使用actions_do()来调用我们的简单动作“嘟嘟”的:
$object = NULL; // $object is a required parameter but unused in this case
actions_do('beep_beep_action', $object);
 
    而下面则是我们如何调用高级动作“嘟嘟多次”的:
$object = NULL;
actions_do(2, $object);
 
    或者,我们还可以绕过获取存储的参数这一步,从而这样调用它:
$object = NULL;
$context['beeps'] = 5;
actions_do('beep_multiple_beep_action', $object, $context);
 
注意 一些中坚的PHP开发者可能会疑惑,“有必要使用动作么?为什么不直接调用该函数,或者仅仅实现一个钩子?为什么需要把参数隐藏在上下文中,直接使用传统的PHP参数不也能实现吗?”答案是,通过编写一个带有非常一般的函数签名的动作,那么就可以实现代码的重用,这样就方便了站点管理员。站点管理员,可能并不懂得PHP,如果他想在添加节点时实现发送电子邮件的功能,那么它就不需要雇佣一个PHP程序员了。他只需要简单的将动作“发送电子邮件”分配到触发器“在保存新文章之后”上,就能实现想要的功能了,这样就不再需要麻烦他人了。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

使用hook_hook_info()定义你自己的触发器

Drupal是怎么知道,有哪些触发器是可以显示在触发器用户界面的?按照典型的方式,它能够让模块通过钩子声明该模块所实现的钩子。例如,这里是来自comment.module的hook_hook_info()实现。定义触发器描述的地方就是hook_hook_info()的实现:

 
/**
* Implementation of hook_hook_info().
*/
 
function comment_hook_info() {
    return array(
        'comment' => array(
            'comment' => array(
                'insert' => array(
                    'runs when' => t('After saving a new comment'),
                ),
                'update' => array(
                    'runs when' => t('After saving an updated comment'),
                ),
                'delete' => array(
                    'runs when' => t('After deleting a comment')
                ),
                'view' => array(
                    'runs when' => t('When a comment is being viewed by an
                        authenticated user')
                ),
            ),
        ),
    );
}
 
    如果我们安装了一个名为monitoring.module的模块,它向Drupal引入了一个新的名为monitoring(监控)的钩子,它可以这样描述该钩子下面的两个操作(overheating(过热)和freezing(过冷)):
/**
* Implementation of hook_hook_info().
*/
function monitoring_hook_info() {
    return array(
        'monitoring' => array(
            'monitoring' => array(
                'overheating' => array(
                    'runs when' => t('When hardware is about to melt down'),
                ),
                'freezing' => array(
                    'runs when' => t('When hardware is about to freeze up'),
                ),
            ),
        ),
    );
}
    在启用了监控模块以后,Drupal就能够看到新的hook_hook_info()实现,并修改触发器页面,为新钩子包含一个单独的标签,如图3-6所示。当然,模块本身仍然需要使用module_invoke()或者module_invoke_all()来触发钩子,以及负责触发相应的动作。在这个例子中,该模块需要调用module_invoke_all('monitoring', 'overheating')。它接着需要实现hook_monitoring($op),并使用actions_do()来触发动作。对于一个简单的具体实现,可参看modules/trigger/trigger.module中的trigger_cron()。
 
3-6.新定义的触发器以一个标签的形式显示在了触发器用户界面
 
    尽管一个模块可以定义多个新钩子,但只有与模块名字匹配的钩子才会在触发器界面创建一个标签。在我们的例子中,监控模块定义了监控钩子。如果它还定义了一个不同的钩子,那么该钩子既不会出现在监控标签下,也不会独自拥有一个标签。然而,对于那些与模块名字不匹配的钩子,仍然可以使用路径http://example.com/?q=admin/build/trigger/hookname来直接访问。
 
老葛的Drupal培训班 Think in Drupal

Drupal版本:

向已有钩子中添加触发器

老葛的Drupal培训班 Think in Drupal

有时候,如果你的代码新增了一个操作的话,那么你可能想要在一个已有的钩子上添加触发器。例如,你可能想向nodeapi钩子添加一个操作。假定你编写了一个模块,用来存档旧节点并将其迁移到数据仓库中。由于这个操作是作用于节点的,所以你可能想在nodeapi钩子下面添加一个archive操作,这样对于内容的所有操作,都会显示在触发器界面的同一个标签下。下面的代码用来添加一个额外的触发器:

 
/**
* Declare a new trigger, to appear in the node tab.
*/
function archiveoffline_hook_info() {
    $info['archiveoffline'] = array(
        'nodeapi' => array(
            'archive' => array(
                'runs when' => t('When the post is about to be archived'),
            ),
        ),
    );
    return $info;
}
    导航到触发器管理页面“管理➤站点构建 ➤触发器”,在触发器列表的最后,我们看到了新增的触发器,如图3-7所示。
3-7.额外的触发器(“当文章即将被存档”)出现在了用户界面
 
    Drupal的菜单系统将使用hook_hook_info()实现中的第一个键,来自动在触发器管理页面创建一个标签。Drupal将使用模块的.info文件中定义的模块名字作为标签的名字(参看图3-7中没有用到的Archive Offline标签)。但是我们的新触发器不需要放在它自己的标签下;通过将我们的操作添加到nodeapi钩子中,我们有意地将新触发器放在了内容标签下。我们可以使用hook_menu_alter()来删除不想要的标签(该钩子的更多详细,可参看第4章)。下面的代码将自动创建的标签,从类型MENU_LOCAL_TASK(Drupal默认将其作为标签显示)改为了类型MENU_CALLBACK,这样Drupal就不再显示它了:
 
/**
* Implementation of hook_menu_alter().
*/
function archiveoffline_menu_alter(&$items) {
    $items['admin/build/trigger/archiveoffline']['type'] = MENU_CALLBACK;
}
 
    为了让archiveoffline_menu_alter()函数起作用,我们需要访问“管理➤站点构建 ➤模块”,这样菜单将被重建。

Drupal版本:

总结

    读完本章后,你应该能够

• 理解如何将动作分配给触发器
• 编写一个简单的动作
• 编写一个高级动作和它的相关配置表单。
• 使用动作管理页面,来创建和重命名高级动作的实例。
• 理解什么是上下文
• 理解动作是如何使用上下文来修改它们的行为的。
• 理解动作的存储、取回、和执行。
• 定义你自己的钩子并将它们显示为触发器。
老葛的drupal培训班 Think in Drupal

Drupal版本: