Oh The Huge Manatee

A blog about technology, open source, and the web... from someone who works with all three.

Adding Rules to Your Contrib Module (So You Can Reject Half the Tickets in Your Queue)

Drupal’s Rules module lets site builders define complex and custom behaviors without having to code a custom module. That puts a lot of power in the hands of the site builder, and it makes developers (rightly) nervous. But there’s another way to think about it.

What’s fantastic about Rules is that it lets everyone who wants your module to behave slightly differently to change the behavior for themselves. In fact, if you build the logic of your module into Rules you get an excuse to reject half the tickets in your contrib module queue on the grounds of “works as intended,” aka “do it yourself.”  To me, this is the quiet brilliance of Commerce module’s dependence on Rules. Their code is almost 100% functional elements, with as little behavioral logic as possible. All of the logic is written in Rules. Of course they still distribute Commerce with a hefty set of default rules, so it works with a standard implementation out of the box. But at the same time, it is open to completely custom workflows without the developers ever having to get involved.

Today we’re going to cover adding Rules to existing code. This is an easy way to write solid and useful patches for other contrib modules out there, patches that get used. For your own projects you will probably want to implement this code in your own custom module, just hooking into core or contrib behaviors. Either way the logic is the same.

At it’s core, the Rules API lets you expose functions you’ve already written to the GUI. 90% of the work is just declaring the functions. There are three elements that Rules cares about:

  • Events: Potential triggers that a user can use to take action with Rules.
  • Actions: Functions which take a set of parameters and do something. 
  • Conditions: Test functions, which return TRUE or FALSE.

You declare these elements to Rules with hooks, usually assembled into a separate modulename.rules.inc file. First, let’s define an event with hook_rules_event_info().

<?php
/**
* Implements hook_rules_event_info().
* Fire a Rules event when a node is validated.
*/
function mymodule_rules_event_info() {
  return array(
    'mymodule_node_is_being_validated' => array(
      'group' => t('My module'),
      'label' => t('A node is being validated!'),
      'variables' => array(
        'node' => array(
          'type' => 'node',
          'label' => t('unsaved node being validated'),
        ),
        'form_id' => array(
          'type' => 'text',
          'label' => t('Form ID'),
        ),
      ),
    ),
  );
}
?>

This implementation tells rules that you are defining an Event with the machine name mymodule_node_is_being_validated. In the Rules dropdown select box that lets you choose the triggering action for a new rule, your Event is called “A node is being validated!” and is sorted into the group “My Module”.  The variables array sets the variables that are offered with this particular event. Each variable in the array must have a  machine name (the key), a human-readable label and a data type. Now, anywhere that you want to fire this rules event, you call it with rules_invoke_event(‘mymodule_node_is_being_validated’, $node) to fire the Event. In this case, you could hook into node validation and add it easily enough:

<?php
/**
* Implements hook_node_validate().
* Fires our Rules event on node validation.
*/
function mymodule_node_validate($node, $form, &$form_state) {
  rules_invoke_event('mymodule_node_is_being_validated', $node, $form['#form_id']);
}
?>

This example brings up one of the big limitations of Rules: you can’t fail validation. We can set an error message, respond with all the power of Rules, but no matter what you do you cannot make this node form fail validation. The good people at rules_forms module are trying to address this, but it’s a much thornier problem than it looks.

Next, let’s define a Rules action with hook_rules_action_info(). This time we’ll use a real world use case from a recent client: our Rules Action will allow you to subscribe a user to a node with the subscriptions module. Again, we’ll add this info implementation to our mymodule.rules.inc file for clarity.

 

<?php
/**
* Implements hook_rules_action_info()
* Add a Rules Action for subscribing a user to a node.
*/
function mymodule_rules_action_info() {
  return array(
    'mymodule_subscribe_user_to_a_node' => array(
      'label' => t('Subscribe a user to a node'),
      'group' => t('Subscriptions'),
      'parameter' => array(
        'user' => array(
          'type' => 'user',
          'label' => 'Subscribing user',
        ),
        'node' => array(
          'type' => 'node',
          'label' => 'Target node',
        ),
      ),
    ),
  );
}
?>

 

You can see that we use the same label, group, and module declarations here. Actions take parameters as well, which are structured very similarly to the variables in events. You don’t have to do anything more to make an action: this is it. When Rules fires the action it will look for a function called mymodule_subscribe_user_to_a_node, and fire it with the parameters provided in the GUI. In this case we wrapped our own function around subscribe module’s functionality, because of the way subscribe’s own subscription function works.

 

<?php
/**
* Creates a subscription for the user
*
* @param $user
*  The user to subscribe to the node.
* @param $node
*  The node to which the user will subscribe.
*/
function mymodule_subscribe_user_to_a_node($user, $node) {
  module_load_include('inc', 'subscriptions', 'subscriptions.admin');
  if (!is_object($node) || !isset($node->nid)) {
    watchdog('rules_subscriptions', 'Error: object passed is not node. Data: !node', array('!node' => print_r($node, true)), WATCHDOG_ERROR);
    return;
  }
  $form_state = array(
    'values' => array(
      'stype' => 'node',
      'sid' => $node->nid,
      'uid' => $user->uid,
      'author_uid' => NULL,
      'send_interval' => _subscriptions_get_setting('send_interval', $user),
      'updates' => _subscriptions_get_setting('send_updates', $user),
      'comments' => _subscriptions_get_setting('send_comments', $user),
    ),
  );
  drupal_form_submit('subscriptions_add_form', $form_state, 'node', $node->nid);
}
?>

 

So much for Rules actions - they’re even easier than events!  There is one big defining difference for Actions though: they can provide new variables back to Rules. This is as easy as a provides array in hook_rules_action_info(), and returning an array of values from the actual function called. 

Now let’s look at a Rules condition. No surprise by now, we declare the existence of our Rules Condition through hook_rules_condition_info() in mymodule.rules.inc :

 

<?php
/**
* Implements hook_rules_condition_info().
* Checks if the given user is admin.
*/
function mymodule_rules_condition_info() {
  return array(
    'mymodule_user_is_admin' => array(
      'group' => t('My Module'),
      'label' => t('User is Admin'),
      'parameter' => array(
        'user' => array(
          'type' => 'user',
          'label' => t('User to test'),
        )
      ),
    ),
  );
}
?>

 

Once again we see the familiar structure to declare what is about to happen. And just like in an Action, Rules is going to fire a function named after the array key, mymodule_user_is_admin, with the parameters provided. Here we’ll have a very simple function to check this, and it has to return TRUE or FALSE

 

<?php
function mymodule_user_is_admin($user) {
  if ($user->uid == '1') {
    return TRUE;
  }
  return FALSE;
}
?>

Great, so now your module is fully integrated with Rules, right? Well, you have to actually include the Rules logic with the module. Rules provides hook_rules_default_configuration for you to do this, but writing rules by hand is a pain. The shortcut solution is to build your Rules in the Rules GUI and export them, and paste the export into your code. entity_import() is a handy function from the EntityAPI module you can use to translate the export format into the format Rules needs. Here’s a sample (hook_rules_default_configuration must be implemented in mymodule.rules_defaults.inc):

 

<?php
/**
* Implements hook_default_rules_configuration().
*/
function mymodule_default_rules_configuration() {
  $rules = array();
  $rules['rules_mymodule_behavior'] = entity_import(
    'rules_config', 
    '............');
  return $rules;
}

Note that the new rule name has to be prefixed with rules_ .  That’s all you need to do.

What’s wonderful about these functions is how simple they are. Depending on how your module is written, adding Rules support could be as simple as adding calls to hook_rules_event_info(), hook_rules_action_info(), and hook_rules_condition_info() that connect to existing functions already in your module. In 10 minutes or less, you can open your module up to much more flexibility than was ever possible before. And if you dare to prune the functionality out of your existing code, you can build it in the Rules GUI, export it, and have all your logic exposed and totally customizable in a further 15 minutes. That’s less than half an hour, and you can start training your users to deal with their own problems. :)

Questions? Problems? Leave us a message in the comments…

Comments