Writing Extensible Drupal Modules, part 1

First in a 3 part series

In the past few months, I've spent a bit of time writing Drupal modules that are designed to have other modules interact with them. In some instances to add content, change configuration, modify display, or participate in module processes. Writing modules that do this turns out to be frighteningly easy. With Drupal, even more so.

The idea behind extensible modules is that you never know what a module will be needed for down the road. It's much easer to write a new module that plugs into the old one, rather than having to go through the process of re-writes, code upgrades, and other headaches that revisiting projects that you put down months ago entail.

I'll give three examples of extensibility that I've implemented that I think are useful for module developers. The first is the SignIt module, a module that is designed to implement petition like functionality. When I first started writing this module it was a fork of the Petition module, but with additional functionality. It was deeply tied into CiviCRM as well. As time progressed, features were tacked on, CiviCRM functionality was never quite finished, and suddenly, other developers were asking me for new features. The key functionality that was always up for discussion was what user data was being collected and where should it be stored. Different use cases required divergent needs. Plus, the number of if (module_exists('civicrm')) { statements was growing, and I realized the situation was getting out of hand. The solution was to try to make SignIt more of a framework which other modules could plugin into predefined functionality.

So the first thing that I wanted to do was to identify what kinds of interactions that I wanted to allow. The problem that I was contenting with has several facets. When we talk about what user data is being collected, we also have to talk about presentation. SignIt has an additional aspect that since it is not a content type (it attaches to a content type), a user uses the SignIt form to create their petition like functionality. It maybe the case that there are choices that administrators can make that users can't- eg, the administrator can set default values for SignIt options which a normal user does not have license to.

Already, we've identified 3 cases: the creation (where a user creates the SignIt), administration (where an admin defines defaults), and display (where we show users data that has been collected). Now display is really two things in the SignIt case- on the one hand it is the form that is shown to the end user where data is collected, and the other is what data is shown to the end user who is a general user of the site. Now we're up to 4 functions and we haven't even figured out where to save the data yet. So since each module that interacts with the SignIt module may want to save specific kinds of data (eg, CiviCRM), we have to make sure they can. Finally, since SignIt is a module that can send emails (or other yet to be defined things), there is functionality for sending. There are at least 6 functions that we're going to want to be able to handle, and there should probably be room for more.

In the case I want to show here, I wanted to integrate some aspects of the Profile module for gathering user data. Since users on the site already had profile data, it seemed much easier to use the Profile fields for data collection in this particular instance. While I wouldn't be using user profiles to store data, it did provide the continuity with the rest of the site. Additionally, I knew that CiviCRM would follow the same setup, it was time to create the SignIt hook system.


/**
* run the signit hook, returns specified things
* @param $hook_name is the name of the hook to call, eg "signit_user"
* @param $op is the action that a module should run
* @param $data is signit data
* @param $config is configuration data
* @return an array of data
*
*/
function signit_extend($hook_name, $op = null, $data = null, $config = null) {
$items = array();
foreach (module_implements($hook_name) as $module) {
if ($new = module_invoke($module, $hook_name, $op, $data, $config)) {
$items = array_merge($items, $new);
}
}
// make sure we return an array
if (! is_array($items)) { return array(); }
return $items;
}

This is the core of the SignIt hooks and it makes it extremely easy to pull in functionality using this kind of model. This signit_extend() function is paired with the actual call to the hook in places where the extension is needed. signit_extend() is called with a hook name which is checked for in all of Drupal's active models. Drupal will return the names of all the modules that implement the hook, so with module_invoke() it's easy to then run the hook in each of the modules. In this case, all of SignIt's hooks are returning arrays of data. Your case maybe different which may force you to write a specific extension for each of your tasks if they require different kinds of data return.

The first example in the SignIt module is in on SignIt creation form. SignIt adds a form on node/X/edit if SignIts are enabled for that content types. Via Drupal's hook_formalter(), SignIt's signit_create_form() is called which adds additional form elements to the standard edit form. Inside of signit_create_form() is where the extension happens.

One of the nice things about this kind of hook system is the elegance of its implementation. Inside of signit_create_form(), to pull in all the modules that use the signit_user hook that we defined earlier, there is a single line of code:


// get all modules which implement hook_signit_user
$form['signit']['user'] = signit_extend('signit_user', 'create', $signit);

This function call calls all modules that implement the hook_signit_user, setting the $op to create and appends the results to the existing $form. At this point, it's just a matter of creating your own module with a hook_signit_user hook.

The signit_profile module is a good example of how this works (also now included in the base install of the SignIt module). If you take a look at the SignIt Profile module:

/**
* implementation of hook_signit_user
* @param $op is the kind of operation
* @param $data is data being passed in
*/
function signit_profile_signit_user($op, $data, $config = null) {
switch ($op) {
case 'create':
return signit_profile_user_create_form($data);
break;

Here you can see the hook_signit_user getting implemented, with the options for what to do when different $op values are passed in. In our case, we're looking at the create option. Additionally, $data is being passed in that can pre-populate the form (particularly in an edit vs. create instance).

The signit_profile_user_create_form($signit) returns a Drupal form array, which is merged with any other forms being returned via the signit_extend() function.

There are a number of different possibilities for $op in the hook_signit_user hook, but all of them work essentially the same way- they return an array of values that get re-incorporated into the main module.

The advantages to this technique are many- for one, it makes it much easier to add in new, compartmentalized functionality without threatening the existing code base. Secondly, it makes it much easier for other people to participate in module development- you can make your own module that plugs into someone else's without needing to submit patches, or gain CVS access to their project.

In this example, I've avoided a few topics- validation and data collision. When you start having multiple modules all jumping into the fray, it gets more important to start thinking about this. Drupal's hook_nodeapi is a really good example of how to approach these kind of issues. That being said, you can easily implement your own hooks using the system above if you are thoughtful of how you want your modules to be extensible.

In the next installment, I'll talk about Media Mover's hook system and how it uses a suite of modules to do sophisticated handling of media.

Comments

Add a new comment