Basic theme preprocess hooks in Drupal 5

One of the useful aspects of Drupal 6's themeing is theme preprocessing. This occurs in between a module (or Drupal core) calling theme() and rendering the actual template file (or function), and arbitrarily alter the variables passed between the module layer and the theme layer. In this way preprocess hooks act much like Django's context preprocessors, providing ways for modules to "communicate" at the point of themeing.

Drupal 5 doesn't have any preprocess hooks, unfortunately. All it has is the function _phptemplate_variables() in your theme's template.php. That's the sole instance of theme preprocessing in D5, and this bottlenecking means that the function quickly becomes the dumping ground for all of your site's pre-theme logic. Even if you're a module developer, of any level of experience, there's no incentive to factor this code out elsewhere: you just replace it with whatever the module function's called anyway, and your site becomes more brittle because of it.

With that in mind, here's how to free up your D5 theme preprocessing in a reasonably Drupalish manner. We'll use a helper module, and call a single function, safely from within your template.php. That function will replicate the basics of D6's theme preprocessing and let you start writing D5 modules with preprocess logic in them.

helpd6.info

First your helper module needs a .info file. It doesn't need to have very much details in it, but here's a sample file:

name = D6 forward-compatibility functions
description =  D6 functions, like theme preprocessing

helpd6.module

Next, here's the module in its entirety. Once you've saved this and the .info file into a directory called helpd6, in your site's modules directory, you should be able to enable the module in your Drupal administrative pages.

<?php
/**
 * Implementation of basic theme preprocessing
 */
function helpd6_implement_preprocess(&$vars, $hook) {
  // module_invoke_all passes by value
  // So assemble function name and invoke ourselves to pass by reference
  foreach(array('preprocess', "preprocess_$hook") as $stub) {
    foreach(module_implements($stub) as $mod) {
      $fn = $mod . "_$stub";
      $fn($vars, $hook);
    }
  }
}

Note we omit the closing PHP tag. And note also that we put an ampersand by $vars. This means we get the "real" theme variables object to play with, not a copy, so that all our logic can modify that "real" variables object and it's reflected by the time Drupal gets round to themeing.

template.php (excerpt)

Finally, this snippet ties everything together: it calls your helper module from the theme's template.php. Put it at the very end of the _phptemplate_variables() function:

function _phptemplate_variables($hook, &$vars = array()) {
  // ...
 
  // Finally, run preprocess hook functions
  function_exists('helpd6_implement_preprocess') 
    && helpd6_implement_preprocess($vars, $hook);
 
  // Now return variables
  return $vars;
}

You'll see that it checks the function exists first. You could check with module_exists('helpd6') too, but we're aiming for as robust a setup as possible, so best check the function exists rather than the general module.

We also return the variables. In practice, referring to &$vars in the function declaration means they get merged in situ, but in principle the function's definition on api.drupal.org requires the variables array to be returned. So changes inthe PHPTemplate engine might stop the &$vars trick working in the future.

Implementation

You can now implement either hook_preprocess or hook_preprocess_TYPE hooks e.g. hook_preprocess_page. These behave pretty much exactly the same as Drupal 6 preprocess hooks.

As an example, let's say that some nodes on your site have an "author" field. This might be different from Drupal's concept of node author e.g: the node might be created by an admin user, but contain information for a book or other publication; the book itself has an author, who could be anybody in the world, alive or dead. Our site also contains a biography node for each author, so we'd like to have the relevant biography present in the theme layer, to list the biography alongside the book.

Here's a "module"---again, just a single function---that accomplishes just that with the aid of our helpd6 module. It should be self-explanatory, but have a look at the comments for more details.

<?php
/**
 * Implementation of hook_preprocess_node
 */
function authorbiog_preprocess_node(&$vars) {
  // Two possible sources for a biography node:
  // 1. the author is a site user - CCK user ref field
  // 2. otherwise, use a CCK node ref field
  $this_node = $vars['node'];
  $author_uid = $this_node->field_drupal_author[0]['uid'];
  $author_nid = $this_node->field_offsite_author[0]['nid'];
 
  // Load the author's biog into the theme variables
  // (Could use Content Profile to help with this)
  if ($author_uid) {
    $biog = node_load(array('uid' =>$author_uid, 'type' => 'biography'));
  }
  else if ($author_nid) {
    $biog = node_load($author_nid);
  }
 
  // Theme preprocess hooks should ALWAYS make the HTML safe
  // Don't rely on .tpl.php files to filter out possible bad markup
  $biog && ($vars['biog'] = check_markup($biog->body, $biog->format));
 
  // &$vars has been passed by reference, so we don't return anything
  // But you can now access the biog (if it exists) in node.tpl.php as "$biog"
}

You can now access the biography text in the theme layer. Note we don't pass down the whole node: we could, but it's safer to pass down filtered HTML, to avoid any possible security holes. In general, it's the job of theme preprocess hooks to handle HTML security, because you never know where else that HTML is going to end up being rendered.

What's important to remember is that this code, and many other snippets like it, need no longer live in your template.php. It's now in a module. It's something you can turn on and off, just like a module. And it's something your themer need never worry about: that logic is now safely hidden behind the scenes, leaving template.php as a theme helper, what it's meant to be. You don't even need any control logic to determine $hook, because module_implements() and the naming conventions do that for you.

Summary

Drupal 5 themeing will never be as versatile as Drupal 6 themeing. But standard code patterns---especially "Drupalish" ones---can go a long way to abstracting out into modules much of the theme preprocessing that Drupal 5 would otherwise force you to do in your theme layer. The more logic you move out of your theme, the more robust it gets, and the easier it is for a non-developer to maintain.