Rules: Creating and Customizing Events

The Rules module is a great module which allows you to very easily create all sorts of conditionally executed actions or workflows. It provides a decent number of predefined events, conditionals and actions to work with. But what if you need to define your own? The reality is, the Rules API is quite easy to leverage as I'll try to demonstrate in this series of three posts.

  • Defining custom events to trap and altering those that do
  • Defining additional conditionals that need to be met

twysocki
#Drupal | Posted

The Rules module is a great module which allows you to very easily create all sorts of conditionally executed actions or workflows. It provides a decent number of predefined events, conditionals and actions to work with. But what if you need to define your own? The reality is, the Rules API is quite easy to leverage as I'll try to demonstrate in this series of three posts.

  • Defining custom events to trap and altering those that do
  • Defining additional conditionals that need to be met
  • Exposing additional actions to perform

Defining custom events to trap

Thanks to the Rules module, the need to define your own event may not come around too often, if at all. Rules already comes packaged with a pretty extensive set of triggerable events to work with. For instance, you already have the ability to create rules that will get triggered when:

  • Viewing, Saving, Updating, Deleting and Publishing a comment
  • Viewing, Saving, Updating and Deleting a node
  • A user is created, logged in, logged out or deleted
  • Saving or Updating terms
  • Viewing a page
  • and many others, including those related to other contributed modules such as the popular Flag module

But even with all the functionality already baked into the Rules module, there may be situations within your own module where you would like to expose an event that users can trigger their own set of rules against. This may be especially helpful when creating a module which you intend to contribute back to the community. To do so is quite simple. The first thing you will need to do is to create a new file within your modules root folder named MODULE_NAME.rules.inc, where MODULE_NAME is the name of your module. Assuming the module we're working with in this example is called 'mymodule', our file would be called 'mymodule.rules.inc'. Once this file is properly named and situated within the root of the module folder, a new custom event can be defined. To do so, implement the hook HOOK_rules_event_info. With this hook, we notify the Rules module of our custom events so that appear in the drop down of the 'Event' drop down on the 'Add new rule' page [admin/rules/trigger/add]. For our example, we can create an event that gets triggered when a user casts a vote of some kind via the VotingAPI module.

/** * Implementation of hook_rules_event_info() * * We will add event handlers to track when a user votes on a piece of content. * This is necessary because votingapi doesn't integrate with rules module out * of the box */ function mymodule_rules_event_info() { $items = array( 'mymodule_votingapi_user' => array( 'label' => t('After a votingapi vote has been cast on a user'), 'module' => 'mymodule', 'arguments' => array( 'voted_user' => array( 'type' => 'user', 'label' => 'Voted user', ), 'vote_tag' => array( 'type' => 'string', 'label' => 'VotingAPI tag', 'handler' => 'mymodule_events_argument_votingapi_tag', ), 'voting_user' => array( 'type' => 'user', 'label' => 'Voting User', 'handler' => 'mymodule_events_argument_voting_user', ), ), ), );

return$items;}

There are a few important parts to the code above.

The names

  • The system name by which we'll reference this event in other areas of the site is set as index value of the $items array, for example: mymodule_votingapi_user (we'll discuss where this will be implemented later).
  • The display name by which this event will appear to the user in the Rules UI is located in the 'label' attribute.

The arguments

The arguments are the parameters which will be made available to the conditions that can be applied to this rule in subsequent steps. In our example above, we highlight three such arguments, 'voted_user', 'vote_tag' and 'voted_user'. You'll notice that the first parameter does not have a handler specified, but the second and third do. The reason is that the Rules module provides some good default handling of certain content types such as nodes, users and comments. In our case, the default handling of user is sufficient for the voted user, but for the voting_user, we'll need to resolve this differently so we will override the handler that Rules would otherwise execute. Note: This will be made more clear in a bit as we demonstrate how this event is triggered. So what about those parameters with the custom handlers? What do the handlers look like? Our second parameter makes a callback to the function mymodule_events_argument_votingapi_tag, which looks something like this:

/** * Argument callback from hook_rules_event_info * * @param object $object *   this will be a comment, node or user object depending on how the event *   was triggered * @param array $vote *   this will be the vote array which triggered the event * @return string *   this will return the tag that of the vote that was submitted */ function mymodule_events_argument_votingapi_tag($object = NULL, $vote = NULL) { return $vote['tag']; }

Simple right? Let's explain. The first parameter $object matches up to the first parameter in our implementation of HOOK_rules_event_info. Since in our example above, we've stated that the first parameter would be a user, the value being passed into this handler will also be a user object. Note: the argument handlers get called in the order they are defined, and populate the associated argument parameter variable. In the example above, the handler for 'voted_user' gets called first, followed by 'vote_tag' followed by 'voting_user'. This means that when the handler for 'vote_tag' is executed, the parameter associated with 'voting_user' (the third handler) hasn't been executed yet and so the value will always be null. Since we can be certain that the third parameter is always null, we've decided to drop it from the function declaration. What about that third parameter--how would we populate the info about the user who did the voting? Let's see:

/** * Argument callback from hook_rules_event_info * * @param object $object *   this will be a comment, node or user object depending on how the event *   was triggered * @param array $vote *   this will be the vote array which triggered the event * @return string *   this will return the user object that submitted the vote */ function mymodule_events_argument_voting_user($object = NULL, $vote = NULL) { global $user; return $user; }

In this example callback, we actually aren't using either of the values being passed in, but we leave them there as placeholders. We could pull the UID field from the $vote array and execute a user_load on the value, but in this situation that would result in the same user as just pulling the currently authenticated user via the global $user variable. Note: Another reason why these two callbacks maintain the two parameters, and are generalized, is that if we were to add a second event to our HOOK_rules_event_info above, say for instance on comment triggered voting api, the data being passed to our $object variable would actually be a comment object rather then a user object.

Triggering our event

So we have a new event which we can create rules against on the Add new Rules page of the Rules admin. But how does this rule get triggered? Up to this point, this rule would never get triggered unless we invoked it via the function rules_invoke_event. For example, if we wanted to trigger this event each time a new vote was inserted into the votingapi table, then we would go into our mymodule.module and implement the hook HOOK_votingapi_insert.

/** * Implementation of hook_votingapi_insert * * We want to trigger our custom action when a vote via voting api is cast. * Currently written to handle comments, nodes and users * * @param array $votes *   this is an array of votes that have been cast */ function mymodule_votingapi_insert($votes) { //loop over the votes that have been cast foreach ($votes as $vote) { //switch on the type of vote submitted switch ($vote['content_type']) { case 'comment' : //we could invoke a similar event here if we wanted break; case 'node' : //we could invoke a similar event here if we wanted break; case 'user' : $vote_user = user_load($vote['content_id']); rules_invoke_event('mymodule_votingapi_user', $vote_user, $vote); break; }//end - switch }//end - loop }

The important line here is where we make the call to rules_invoke_event. This function works much the same as the core module_invoke or theme function does. Here the parameter list matches up to the parameter list we defined earlier. Where we defined the first parameter as 'voted_user' we are passing in the result of user_load of the content_id attribute. This is because we know that the vote was placed on a user object, so therefore the content_id here will be the user's UID.

Altering an existing event handler

As we mentioned earlier, the arguments that you expose in an event are important because these are the arguments that will be made available to the conditions that you want to add to the Rule you are creating. So the question may be, what if there is an existing event which doesn't expose all the parameters we want to have exposed--can we add it? The answer is YES! This is accomplished via the hook HOOK_rules_event_info_alter. One module that works particularly well with Rules is the Flag module. There are numerous events and conditionals that are exposed via the Flag module which make it a great combination. But sometimes there are special business cases which the developers of Rules or Flag didn't foresee, and you may need to do a work around. The great thing is that the HOOK_rules_event_info_alter makes this easy. In our example below, we'll look to add an additional argument to one of the events that the Flag module exposes. This additional argument will come in useful for a particular Rule that we want to create.

/** * Implementation of hook_rules_event_info_alter * * We want to alter the flag module's event handler for when nodes are flagged * to expose the user associated with the node which was flagged. we need this * in order to validate if the user who was flagged as met all conditions. */ function mymodule_rules_event_info_alter(&$events) { //if the 'flagged_user' hasn't been set. we check for this in case the module //is updated in the future and starts to supply it if (!isset($events['flag_flagged_mymodule_like_it']['arguments']['flagged_user'])) { $events['flag_flagged_mymodule_like_it']['arguments']['flagged_user'] = array( 'type' => 'user', 'label' => 'flagged user', 'handler' => 'mymodule_rules_event_get_flagged_user', ); } }

In the code sample above, we look for the the event being exposed by the flag module for a particular flag called 'like it'. The Flag module exposes this event in the form of flag_flagged_[MODULE_NAME]_[FLAG_NAME]. The first thing we do is to check if this event already has the argument named 'flagged_user' present. If it doesn't, we add it just as we would if we had declared this event in the first place (as we had done above). In this case, since it's doubtful that the module already knows or is passing the user by default (since we then wouldn't have to be doing this), we're going to assign it the handler function mymodule_rules_event_get_flagged_user. This function has access to all the parameters that have been defined up to this point by this specific event. In this event, the fourth argument ends up being a node object which can be used to determine the user who is the author. The code for this is below:

/** * Event handler callback which is added in hook_rules_event_info_alter of this * module. This will look at the node object, retrieve the user uid and return * the user object associated with it. * * @param object $flag *   this is the flag object which is passed in via the event callback * @param integer *   this is the node nid of the content which was flagged * @param object $account *   this is the user object of the user doing the flagging * @param object $node *   this is the node object which was flagged * @return object *   this will be the user object of the user who owns/created the node which *   was flagged. */ function mymodule_rules_event_get_flagged_user($flag, $content_id, $account, $node = NULL) { $response = FALSE; if (isset($node->uid) && is_numeric($node->uid)) { $response = user_load($node->uid); } return $response; }

That's all there is to it! At this point, you should hopefully be able to trigger events when a vote is cast via the votingapi module, or any other case you might come up with.

Check back soon for two more posts where we'll show how to create custom actions and custom conditionals. In the meantime, check out the sessions the Treehouse team has proposed for the upcoming Chicago DrupalCon!

 

twysocki