A foray into Rules module

Published
2011-05-04 20:39
Written by
Eileen - member of the CiviCRM community and Core Team member - about the Core Team

I recently had a requirement to set up a CiviCRM install so that whenever an event (or specific types) was created an organic group would also be created. When someone registered for that event they would be added to the organic group and if they were registered as a teacher they would become the manager of the organic group.

 

The focus of this blog is on the code implementation of this requirement using rules integration. I wanted to see if it could be done by rules & if that would set a useful precedent. The two aspects of Rules that where the crux of what I was trying to do were not implemented in the CiviCRM Rules module so I will describe them in some detail.

 

The way in which the CiviCRM rules module works is that has functions for the various drupal hooks. When one is called CiviCRM Rules evaluates whether or not CiviCRM Rules integration exists for that hook for the specific entity and option.

 

In our case the focus is on the POST hook. The post hook is called once a CiviCRM entity is created (an entity could be a Contact, an Event, a Group etc). I wanted to expose the POST hook on Event & Participant - but also to do it in a way that I could add other entities easily so I added a drupal admin settings form to provide an interface to allow the site administrator to specify when to call it. At the moment there is no code way of generating a list of entities that call the POST hook but we have talked about introducing that in CiviCRM 4.1. (at the moment available entities are just coded as an array).

 

So, once the civicrm POST hook is called it calls 'RULES' & gives it the 'event' - so when a contact is created RULES is called with 'contact_create'. I felt that with such things as groups and events there was a risk of confusion with events exposed by other drupal modules so for the additional objects I called rules with a prefix ie. 'civicrm_event_create'. Note that I will try to use 'rules event' & civicrm event in this blog to distinguish the two.

 

So, what is involved

1) register the event types. You register the types of events that should be available from your module to rules. So, I registered create, view, delete, edit for each enabled entity. I did this by calling a function in a separate file as I wanted to keep my code as separate as possible (as it may wind up being a separate module).

The rules function that registers the events is called

function civicrm_rules_event_info()

(My additions are in function civicrm_rules_entities_event_info())

 

So, registering my function means that when you go to set up a rule you can choose the rules event you want

 

2) Arguments: Once I have created a rules event I need to be able to do something with it. In this case I wanted to create a node. It turns out you can't create a node using Rules unless you have a User object available to your events. Being able to access alternate objects is called an argument in Rules parlance.

In order to create the argument for the CiviCRM Event I declared this argument when I registered the event.

 

return array(

'cms_user' => array(

'type' => 'user',

'label' => t('User that edited the event'),

'handler' => 'civicrm_rules_events_argument_civicrm_event',

) ,)

 

 

The handler is a function that is called & must return an object of the defined type - in this case 'user' .

This 'argument' - the user object - becomes available for all drupal actions (like creating content - or organic groups in this case) where a user object is required. This might also include adding a role to user or making them inactive.It would be pretty easy to also add a contact object argument here so you could carry out rules actions on that contact like adding them to a group.

 

 

Conditions & Arguments

So, I wanted to be able to say

"WHEN the event that I have just created event is of type x, y,z"

Is of type xyz is a condition. I struggled with this for a bit but basically it's a case of declaring the condition

function civicrm_rules_entities_condition_info() {

return array(

'civicrm_rules_condition_participant_role' => array(

'label' => t('Participant Role'),

'arguments' => civicrm_rules_entities_conditions( t( 'Participant Role' ),'civicrm_participant' ),

'module' => 'CiviCRM participant',

),

'civicrm_rules_condition_event_type' => array(

'label' => t('Event Type'),

 'arguments' =>  civicrm_rules_entities_conditions(  t( 'Created Event' ),'civicrm_event' ),

'module' => 'CiviCRM event',

),

);

 

In the case of 'event_type' civicrm_rules_entities_conditions simply returns an array ('civicrm_event' => t('Created Event'))  which seems to be crucial to getting the event entity id passed through to the condition function.

 

For the civicrm_rules_condition_event_type I wanted to allow the admin to use checkboxes to choose the type - I took a while to figure out how to do that but it turned out I just needed a function with _form on the end:

/**

* Condition: Check for content types - Configuration form

*/

function civicrm_rules_condition_event_type_form($settings, &$form) {

$form['settings']['civicrm_event_type'] = array(

'#type' => 'checkboxes',

'#title' => t('CiviCRM Event Types'),

'#options' => civicrm_rules_get_options( 'event_type'),

'#multiple' => TRUE,

'#default_value' => isset($settings['type'] ? $settings['type'] : array(),

'#required' => TRUE,

);

}

 

If the administrator specifies this condition as part of their rule it calls the function which is the array key from the 'info' function.

 

/**
 * Condition civicrm_event type
 */
function civicrm_rules_condition_event_type(&$entityobj,&$eventTypes) {
  return empty($eventTypes[$entityobj->event_type_id])  ? false : true;
}

 

So, conditions + arguments

It took me a while to figure out conditions & arguments . Here is the next part of the workflow broken up in Rules-speak

- When someone registers for an event (rules event : civicrm_participant_create)

- If their Role is '1' (rules condition : civicrm_rules_condition_participant_role)

- Subscribe 'the user who signed up' (argument) to 'the node related to the event' (argument)

Loading up the 'node related to the event' seemed to me to require a cck field against the event & turned out to be an odessey of it's own. In the end I have just implemented code that will load up a node based on the value in a cck field called 'civicrm_event' matching the event ID.

CODE

https://svn.fuzion.co.nz/repos/fuzion-code/trunk/drupal/modules/custom/civicrm_rules

In order to deploy the rules I packaged them as a feature. The feature is not good to go out of the box for someone else as it also includes views, and migrate tasks in order to create organic groups from existing events

https://svn.fuzion.co.nz/repos/egw/trunk/sites/all/modules/egwc_custom_modules/ogevents

The rule itself is defined in

https://svn.fuzion.co.nz/repos/egw/trunk/sites/all/modules/egwc_custom_modules/ogevents/ogevents.features.inc

 

 

Where to go:

I think it's fairly easy to extend the POST hook side of rules with more arguments & conditions & ideally offer civicrm entities as arguments.

 

I think deciding whether to present civicrm_entities with the 'civicrm_' prefix is an important question. CiviCRM Event & Phone events attract the attention of the event & phone modules - hence I called them civicrm_event rather than event

 

I kept my code mostly in a separate file & referred to it as entities but I suspect all the generic 'stuff' should be in one file & there should be another file for entity specific arguments / conditions (per entity) although sometimes a file for conditions & one for arguments appeal. Whatever way .. it needs structure.

 

Doing this integration highlighted to me the usefulness of some things we have talked about implementing :

 

1) Having the POST hooks defined in Code. Lobo, Xavier & I have talked about moving the POST hooks to the DAO and having a function somewhere that defines which DAO actually call it - since we probably don't need to call it for all DAO

 

2) A major reason I didn't go with cck field was that I couldn't make it do autocomplete on event using the existing API - the API should accept '%blah%' arguments on all fields - another one for the long to-do list.

Filed under

Comments

Hi,

 

I think the cost of firing a post on an entity that "doesn't need it" is null, but that if history repeat itself, a hook you don't implement because nobody needs is going to be needed at one point because whatever we never forsaw.

 

1) Either we put the pre and post in the DAO, and take them out from the BAO or wherever they are spread, or we create new hooks (dlobo convinced me that option isn't good).

 

We put that into CRM_DAO, so if there is a really good reason not to fire the hook for an entity, it can be overrided.

 

For the rule integration, not sure I followed everything, and it seems a bit has been eaten by  the input filter.

 

X+

 

I think Lobo said tht we can't put the PRE in the DAO because processing is done in the BAO after they are called. The one hook where lack of context seems to be caused a bit of pain for me is the 'token' hook. It seems the token code calculates what tokens are in a bit of text but doesn't pass this information to the token hook modules.

 

I think the conversation about only calling DAO for some entities was about there being 135 tables & not wanting to call the hook processing for all.

 

Re rules integration - not sure I followed it all either :-)