Writing actions (Drupal 6.x)
Drupal actions are best understood using an appropriate definition.
For Drupal site administrators: actions are individual "things that Drupal can do." Some examples of actions are sending an email, publishing a node, banning a user or promoting a node to the front page of a website. Typically you will encounter actions in Drupal's configuration screens.
For developers: an action is a function that operates like a stored procedure. The function's parameters, if any, are stored in the database and the function is executed by retrieving these stored parameters and calling the function.
Can you give me an example of where an action might be useful?
Actions are usually used to configure Drupal's response to an event. For example, suppose a Drupal site administrator wants to be notified by email whenever a new user signs up. She would configure a "Send email" action and use the trigger module to assign that action to execute when a new user joins the site. (Technically, that would be when the user hook's 'insert' op runs.)
Where does the actions code actually live?
The actions engine, which executes actions, lives in includes/actions.inc. The dispatcher for actions is in modules/trigger.module.
The configuration screens for adding, removing, and configuring individual actions are part of system.module.
The interface for assigning actions to events (that is, hooks) is provided by modules/trigger.module.
The hook that describes actions (hook_actions_info()) and the actions themselves live in individual modules. Actions that affect nodes, like the "Publish node" action, live in node.module.
How do I create a new action?
There are two steps. First, we must describe the action to Drupal using hook_action_info(). Then, we must actually write the code that will be executed.
Describing an action with hook_action_info()
Let's take a look at the user module's implementation of hook_action_info():
<?php
/**
* 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(
'nodeapi' => array('presave', 'delete', 'insert', 'update', 'view'),
'comment' => array('view', 'insert', 'update', 'delete'),
'user' => array('logout'),
),
),
'user_block_ip_action' => array(
'description' => t('Ban IP address of current user'),
'type' => 'user',
'configurable' => FALSE,
'hooks' => array(
'nodeapi' => array('presave', 'delete', 'insert', 'update', 'view'),
'comment' => array('view', 'insert', 'update', 'delete'),
'user' => array('logout'),
)
),
);
}
?>hook_action_info() must return an array, keyed by the function names of the actions being described. In user.module's user_action_info() we are describing two actions. We'll focus on the first one. The array key of the first action being described is 'user_block_user_action'. That's the name of the function that will actually be executed when this action runs. The name of the function is constructed by using the following convention:
modulename + description of what the function does + '_action'
In this case it is
user + block user + action
which gives us 'user_block_user_action'.
Next, we need to provide some information in the array using the following keys:
description: an easy-to-understand description of what the action does
type: the type is determined by what object the action acts on. Possible choices are node, user, comment, and system. Or your own custom type.
configurable: TRUE or FALSE. This determines the interface that Drupal will use when configuring actions. When set to FALSE we have the simplest case, where there is no interface to configure the action. In our example, the "Block current user" action does not need any additional information since the current user can be easily determined by Drupal at runtime. A more complicated action, such as a "Send email" action, would need to know things like who to send the email to, what to put in the subject line and the body of the email, etc.
hooks: this is an array of all of the operations this action is appropriate for, keyed by hook name. The actions module uses this to filter out inappropriate actions when presenting the interface for assigning actions to events. For example, the "Block current user" action defines the 'logout' operation of the 'user' hook, but not the 'login' operation. That's because it would be kind of silly to block a user as soon as the user logged in. It should be noted that this is an interface issue only; Drupal does not enforce these restrictions on execution of actions. Note: if you are writing actions in your own modules and you simply want to declare support for all possible hooks, you can set 'hooks' => array('any' => TRUE).
Writing an action
Now that we've described the action to Drupal, we can write the actual code that runs when the action is executed. Let's look at the code for the "Block current user" action:
<?php
/**
* Implementation of a Drupal action.
* Blocks the current user.
*/
function user_block_user_action(&$object, $context = array()) {
// get the uid from the object
if (isset($object->uid)) {
$uid = $object->uid;
}
elseif (isset($context['uid'])) {
$uid = $context['uid'];
}
else {
global $user;
$uid = $user->uid;
}
// make sure we have a user record
if ($uid) {
$user = user_load($uid);
// block the user
db_query("UPDATE {users} SET status = 0 WHERE uid = %d", $uid);
// log out the user
sess_destroy_uid($uid);
// record a message noting the action taken
watchdog('action', 'Blocked user %name / uid=%uid.', array('%name' => check_plain($user->name), '%uid' => $uid));
}
}
?>Note that this code is now out of sync with the code in Drupal core (http://api.drupal.org/api/function/user_block_user_action) from where it was originally taken; there is an open issue (#497358: user_block_user_action() may log incomplete watchdog message) to fix it in core.
First, let's look at the function signature for the action. Two parameters are passed, an object and an array.
$object: this is the object on which the action expects to act. It corresponds with the 'type' that was declared for this action in hook_action_info(). For example, if the type 'user' was declared, the action will be passed a user object.
$context: this is an array that contains additional information that may be helpful for the action to look at to determine the context under which the action is currently running. For example, the actions module sets the 'hook' and 'op' keys of the context array (e.g., 'hook' may be 'nodeapi' and 'op' may be 'insert') so that the action can examine them and make various decisions if necessary.
Next, let's look at the action itself. It really has two parts. First, it determines the user to block by first looking at the user object it has been passed; failing that, it looks in the context array for the uid; failing that, it uses the global $user to determine the uid to block itself. Some of you may be curious about this. Why have these fallback positions? Why not require the passage of the "correct" first parameter? The answer is twofold. First, with actions we want a function signature that is universal so the underlying actions engine has to do less work, so we can have better performance. Second, suppose you want to block 50 users. Doing a full user_load() on each one just to get an object you can pass to an action is not performant when you can hand over the uid in the context array instead.
The second part of the action is where the user is actually blocked and a watchdog entry recorded.
Now you know how to make a nonconfigurable action. A configurable action must also provide form handlers in order for the administrator to set the action's stored parameters. Look at the "Unpublish comment containing keyword(s)" action in comment.module for an example, specifically at the functions comment_unpublish_by_keyword_action_form(), comment_unpublish_by_keyword_action_submit(), and comment_unpublish_by_keyword_action().
I've written a module that provides hooks. How can I assign actions to run when those hooks run?
Use the instructions provided in the writing triggers page of the handbook. Additional examples are also available in the node_hook_info() or comment_hook_info() or user_hook_info() functions.
I want to execute an action but I don't want to use actions.module.
OK. You can run any action directly from code by using the actions_do() function, which lives in includes/actions.inc.
I want to execute an action but only if certain conditions are met
Have a look at the Rules module and its documentation which is specifically designed to allow you to define conditionally executed actions, such as "send an email when a new node is posted, but only if the content type is 'node'."
I've updated my action declaration in hook_action_info() but the action has not been updated. Help!
Getting Drupal to refresh the info is slightly cumbersome, until you work out how. When you visit the actions settings page at admin/settings/actions Drupal will detect (via actions_synchronize() that the action has been updated but won't automatically start using the new action information.
Go to the "Recent log entries" report (admin -> reports -> recent log entries) and you should see a note about orphaned action(s). Click on the message to view the full details and then click on the link "Remove orphaned actions". Note that this will also remove any trigger assignments for this action.
Your latest and greatest action declaration will now be used. Don't forget to revisit the triggers page to assign your action(s) again.
Note that any changes you make to the code of your custom action implementation take effect immediately - it is only the information declaration in hook_action_info() that is hard to erase!

Configurable actions
Configurable actions require a form. A little spelunking revealed that what's needed for a configurable action action_function is to define functions action_function_form($context) and action_function_submit($form, $form_state) (and probably action_function_validate too).
For example, the action comment_unpublish_by_keyword_action is configurable and defines functions comment_unpublish_by_keyword_action_form and comment_unpublish_by_keyword_action_submit.
More on configurable actions in John VanDyk Blog
This blog has an example of a configurable action:
http://www.sysarchitects.com/node/47
changing action names
Before you can go to "Recent log entries" and see the orphaned message, you should disable the module associated with the action. Re enable it after you clear out the orphaned action.
$node object is not passed to customized action function
I want to have an action for my customized node, which basically set the moderate flag of the node. My implementation of hook_action_info() is:
<?phpfunction mynode_action_info() {
return array(
'mynode_moderate_action' => array(
'type' => 'mynode',
'description' => t('Moderate post'),
'configurable' => FALSE,
'behavior' => array('changes_node_property'),
'hooks' => array(
'nodeapi' => array('presave'),
'comment' => array('insert', 'update'),
),
),
'mynode_unmoderate_action' => array(
'type' => 'mynode',
'description' => t('Unmoderate post'),
'configurable' => FALSE,
'behavior' => array('changes_node_property'),
'hooks' => array(
'nodeapi' => array('presave'),
'comment' => array('delete', 'insert', 'update'),
),
),
);
}
?>
These are my implementation of the action:
<?phpfunction mynode_moderate_action(&$node, $context = array()) {
$node = $context['node'];
$node->moderate = 0;
watchdog('action', 'Set @type %title to moderated.', array('@type' => node_get_types('name', $node), '%title' => $node->title));
}
function mynode_unmoderate_action(&$node, $context = array()) {
$node = $context['node'];
$node->moderate = 1;
watchdog('action', 'Set @type %title to unmoderated.', array('@type' => node_get_types('name', $node), '%title' => $node->title));
}
?>
To my surpise, $node object was empty, and I have to refer to $context['node']. I thought the $node object should contain the node reference, if "behaviour" is set to "changes_node_property"? This code works only if I declare it in node.module (but I don't like the idea of patching node.module):
<?phpfunction node_moderate_action(&$node, $context = array()) {
$node->moderate = 0;
watchdog('action', 'Set @type %title to moderated.', array('@type' => node_get_types('name', $node), '%title' => $node->title));
}
function node_unmoderate_action(&$node, $context = array()) {
$node->moderate = 1;
watchdog('action', 'Set @type %title to unmoderated.', array('@type' => node_get_types('name', $node), '%title' => $node->title));
}
?>
'type' => 'mynode' should be
'type' => 'mynode'should be'type' => 'node'.'type' => 'mynode' should be
I tried that, but it does not help at all. This is an old post of mine about Action for Drupal 5.x with Drupal 6.x style:
http://drupal.org/node/368708
Btw, I just upgrade my site to Drupal 6.x :)