Writing an action for Drupal 6

Note: this applies to the development version of Drupal 6 as of June 12, 2007 with the actions patch applied.

Actions are atomic functions that can be assigned to run at different times during runtime. For example, you could have an action that emails a user and have that run when a node is inserted. Or when a comment is deleted. Or when a user logs in.

When writing an action, you first need to decide whether or not your action will need to know anything beyond the context in which it runs. For example, an action that prefixes the title of a node with "New! " does not need any other information; it just needs the node. Let's actually create that action a moment.

First, we need to describe the action to Drupal. We do that with hook_action_info.

<?php
function modulename_action_info {
  return array(
   
'modulename_prepend_new_action' => array(
      
'type' => 'node',
      
'description' => t('Prepend "New! " to the title of a node'),
      
'configurable' => FALSE,
      
'hooks' => array(
        
'nodeapi' => array('presave', 'insert', 'update', 'view'),
        
'comment' => array('insert'),
       )
    )
  );
}/
?>

The array returned is keyed by function name. So later on we'll create a function called modulename_prepend_new_action(), probably not the best name but let's roll with it.

The 'type' key declares what object type this action will work on. It will work on nodes, so it's a 'node' type of action, or a node action.

The 'description' will appear in the UI when the administrator is assigning actions to hooks on the "Assign actions" screen.

The 'configurable' key declares whether or not the action needs additional information to run. This action doesn't; it simply modifies the node it's been given. More about configurable actions in a minute.

The 'hooks' key determines where this action may be assigned. For example, this action running during the 'register' op of the user hook would make no sense. But running in the 'presave' op of the nodeapi hook, where it can modify the node object just before it is saved, makes perfect sense.

Now for the action itself. It's dead simple.

<?php
function modulename_prepend_new_action(&$node, $context) {
 
$node->title = t('New!') . ' ' . $node->title;
}
?>

What's in $context? Well, that's where Drupal puts any information that it thinks the action might want to use. Suppose we wanted this action to run when a comment is added. That's great. But internally, the comment is the object being worked with during the comment hook. So the actions module looks at the comment, finds the nid, loads the node, and passes that to the action (because it's a node action it expects a node). But since Drupal already has a copy of the comment object on hand, it puts it into the $context so that if the action wants to, it can look at it and make decisions based on that. So that's what $context is.

Now for the second kind of an action, a configurable action. That's like a stored procedure. For this we'll look at a comment action called Unpublish comment containing keyword(s). Obviously the action needs to know which keywords to compare with the comment body so it can figure out whether to unpublish the action or not. So it is a configurable action. Note that we don't use configurable actions directly; we make instances of them. We could make multiple action instances with descriptions like this:

Unpublish comment containing 'butter'
Unpublish comment containing 'blubber'
Unpublish comment containing 'flubber' or 'moose'

We would create these "action instances" on the "Manage actions" screen that the actions module provides. Here's the action_info() hook:

<?php
function comment_action_info() {
  return array(
   
'comment_unpublish_by_keyword_action' => array(
     
'description' => t('Unpublish comment containing keyword(s)'),
     
'type' => 'comment',
     
'configurable' => TRUE,
     
'hooks' => array(
       
'comment' => array('insert', 'update'),
      )
    )
  );
}
?>

Now we need to provide a way for the user to input which keywords to look for. So we create a comment_unpublish_by_keyword_action_form() function. Note we just append _form to the action function name. Here's the function, simplified:

<?php
function comment_unpublish_by_keyword_action_form($context) {
 
$form['keywords'] = array(
   
'#title' => t('Keywords'),
   
'#type' => 'textfield',
   
'#description' => t('The comment will be unpublished if it contains any of the keywords above. Use a comma-separated list of keywords. Example: funny, bungee jumping, "Company, Inc.".'),
   
'#default_value' => isset($context['keywords']) ? implode(',', $context['keywords']) : '',
  );
  return
$form;
}
?>

We could make a validation function (comment_unpublish_by_keyword_action_form_validate()) if we wanted to, but let's skip that and go right to the submission function. The taxonomy module already has a great implementation of turning comma-delimited keywords into an array, so we'll just use that. Code reuse!

<?php
function comment_unpublish_by_keyword_action_submit($form, $form_state) {
  return array(
'keywords' => taxonomy_explode_tags($form_state['values']['keywords']));
}
?>

Note that we return a keyed array of parameters needed by the action. These parameters will be available inside $context. So the action will need to look for $context['keywords'] and that's exactly what it does:

<?php
function comment_unpublish_by_keyword_action($comment, $context) {
  foreach (
$context['keywords'] as $keyword) {
    if (
strstr($comment->comment, $keyword)) {
     
db_query('UPDATE {comments} SET status = '. COMMENT_NOT_PUBLISHED .' WHERE cid = %d', $comment->cid);
     
watchdog('action', 'Unpublished comment %subject.', array('%subject' => $comment->subject));
      break;
    }
  }
}
?>

There you have it. Try writing an action of your own!

Comments

Can you explain a little more what does the submission function do?(comment_unpublish_by_keyword_action_submit)

comment_unpublish_by_keyword_action_submit() is run when the configuration form for the action has been filled out and submitted. Its job is to return a keyed array of values that will then be available to the action when it runs.

Hey John,

Great book(s). They've really helped me a lot.

Is it possible to use triggers/actions to modify/override any module's function? Example: I'd like to log to the db whenever tellafriend sends e-mail to each 'friend' using the function 'tellafriend_page_submit'. I need to record some custom data that's separate from the watchdog table to use later.

Thanks.