Avoiding the Template.php of Doom (or, Overriding Theme Functions in Modules)

Drupal's theming system offers developers and designers a flexible way to override default HTML output when specific portions of the page are rendered. Everything from the name of the currently logged in user to the HTML markup of the entire page can be customized by a plugin "theme".

Unfortunately, this system can be its own worst enemy. Themes are very powerful, but in many cases they're the only place where specific output can be changed without hacking core. Because of this, themes on highly customized production sites can easily turn into code-monsters, carrying the weight of making 'Drupal' look like 'My Awesome Site.'

This can make maintenance difficult, and it also makes sharing these tweaks with other Drupal developers tricky. In fact, some downloadable modules also come with instructions on how to modify a theme to 'complete' the module's work. Wouldn't it be great if certain re-usable theme overrides could be packaged up and distributed as part of any Drupal? As it turns out, that is possible. In this article, we'll be exploring two ways to do it: a tweaky, hacky approach for Drupal 5, and a clean and elegant approach that's only possible in Drupal 6.

Under the Hood

Before getting into the details, we'll look at how Drupal allows themes to override HTML rendering. This mechanism will be the key to our sneaky tricks.

Whenever 'themable' HTML is being generated, Drupal modules first assemble the basic data that should pre presented (an array of numbers, a content node...), then call the theme() function. For example:

  
$node = node_load(1); // Load node id 1 from the database
$output = theme('node', $node); // This generates themed HTML
print $output;
  

The first paramater passed into the theme() function is the type of data being themed, while the second parameter is the 'thing' itself. When that function is called, Drupal walks through the following process:

  1. Does the theme handle it?

    The currently installed theme is first in line to render the object to HTML. Drupal checks for a function named theme-name_object-type(), and if it exists, calls it. For example, the Garland theme uses the function garland_breadcrumb() to control how the breadcrumb trail is displayed.
  2. Does the theme engine handle it?

    Next in line is the current 'theme engine.' In most cases, this is Drupal's default PHPTemplate theming engine. Smarty and PHPTal are other possibile engines. As with themes, Drupal checks for a function named theme-engine-name_object-type(), and if it exists, calls it. The PHPTemplate engine uses the function phptemplate_node() to control how nodes are displayed.
  3. Let a module handle it.

    Finally, if no overrides are found, Drupal checks for a function named theme_object-type() and calls it if it exists. These default theme functions are usually provided by modules to offer default HTML output for objects in case no one overrides them.

This approach is very flexible: it gives themes and the underlying theme engines a chance to override the HTML, lets modules provide a 'default' style of output, and it makes the complexities of the overriding process invisible to a developer who just wants to print out a node (or any other themable object) on a page. The only problem is that it doesn't provide a way for another module to jump in between steps 2 and 3, overriding the default HTML.

Drupal 5: Sneaky, Sneaky Hacks

In Drupal 5, there's no officially supported way to overcome this limitation, There is, however, a crafty trick you can use to override theme functions in your modules. Take a look back at step 2 in the explanation of Drupal's overriding process, again. Drupal checks to see whether a function named theme-engine-name_object-type() exists in order to see if a theme engine wants to override the rendering. If that function name exists, Drupal will use it -- even if it's implemented in your module, not the actual theme engine.

What does that mean? If your module implements the function phptemplate_username(), it will be treated as if it's the theme engine in step 2, overriding the default markup provided by Drupal core, without making any changes to the theme itself. Voila!

The downside, of course, is that if the theme engine you're using does provide its own override, no module can play this trick: the function name already exists, and trying to define it again in your module will cause PHP errors. It can still be a useful way to isolate site-specific chunks of theme code in a way that's easy to track, enable or disable, and so on.

Drupal 6: The Land of Milk and Honey

In Drupal 6, things are a bit different. The same basic hierarchy is still in place: first themes, then theme engines, then modules all get opportunities to render an object to HTML. However, Drupal now caches the information about what function should be used in an internal "theme registry." This saves Drupal the work of 'discovering' who's in charge each time the theme() function is called.

In addition to saving time, though, this cached "registry" of theme functions is something that modules can modify using the hook_theme_registry_alter() function. What does that mean? While a module can't insert itself between steps 2 and 3 in the discovery process, it can step in after the discovery process is complete, and replace the default function from step 1 with its own version -- even if it doesn't follow the naming conventions Drupal expects.

Let's take a quick look at how this works, stealing a snippet of code from the WordPress Comments module. It's a module that intercepts Drupal's default rendering of all form elements to tweak the appearance of labels and 'required' flags on certain forms.

  
function wp_comments_theme_registry_alter(&$theme_registry) {
  if (!empty($theme_registry['form_element'])) {
    $theme_registry['form_element']['function'] = 'wp_comments_form_element';
  }
}

function wp_comments_form_element($element, $value) {
  // Here, we provide our customized version of the
  // theme_form_element function from theme.inc...
}
  

The above code is pretty straightforward: in hook_theme_registry_alter(), it first checks to be sure that the form_element theme data is properly defined, then swaps in its own custom function (wp_comments_form_element) in place of the default one (theme_form_element).

The beautiful part of this system is that it continues to work cleanly with custom themes: if a theme overrides the form_element theming code as well, it will still take precedence over wp_comments' version. In addition, there's no chance of colliding function names, as it relies on the theme registry rather than 'magic' function names like phptemplate_form_element().

Wrapup

Drupal's theming system provides powerful tools for building clean HTML markup and layered designs. In Drupal 6, the new theme registry makes the process easier to maintain, and safer to tweak. I'm looking forward to the coming year, as more of Drupal's contributed modules migrate to version 6 and take advantage of its additional capabilities!

Get in touch with us

Tell us about your project or drop us a line. We'd love to hear from you!