Drupal 7 Module Development: Drupal's Theme Layer

Exclusive offer: get 50% off this eBook here
Drupal 7 Module Development

Drupal 7 Module Development — Save 50%

Create your own Drupal 7 modules from scratch

$26.99    $13.50
by John Wilkins | December 2010 | Drupal Open Source

The most obvious part of Drupal's theming system is the Appearance page, which lists all of the themes installed on your website. When you choose a theme from the Appearance admin page, you are applying a specifc graphic design to your website's data and functionality. However, the applied theme is in reality only a small part of the entire theming layer.

Since we're building a web application, everything outputted by your functionality will need to be marked up with HTML. Drupal calls the process of wrapping your data in HTML and CSS as theming.

In this article, by John Wilkins, author of Drupal 7 Module Development,we will discuss about the architecture of the system, theme functions, templates, render elements, and the theme registry.

 

Drupal 7 Module Development

Drupal 7 Module Development

Create your own Drupal 7 modules from scratch

  • Specifically written for Drupal 7 development
  • Write your own Drupal modules, themes, and libraries
  • Discover the powerful new tools introduced in Drupal 7
  • Learn the programming secrets of six experienced Drupal developers
  • Get practical with this book's project-based format
        Read more about this book      

(For more resources on this subject, see here.)

Business logic versus presentation logic

So what would be the best way to get our data and functionality marked up? Do we simply wrap each piece of data in HTML and return the whole as a giant string? Like the following example:

return '<div class="wrapper">' . $data . '</div>';

Fortunately, we don't. Like all other well-designed applications, Drupal separates its business logic from its presentation logic. Traditionally, the primary motivations for this separation of concerns are as follows:

  1. To make the code easier to maintain.
  2. To make it possible to easily swap out one layer's implementation without having to re-write the other layers.

As we shall see, Drupal takes the "swap-ability" aspect to the extreme.

As we mentioned in the introduction of this article, the default theme selected on the Appearance page is the most obvious part of the theme layer. Also, you might think that the theme is responsible for applying the HTML and CSS for the website. However, there are thousands of contributed modules on drupal.org. Should the theme be responsible for marking up all of those modules' data? Obviously not.

Since a module is most intimately familiar with its own data and functionality, it's the module's responsibility to provide the default theme implementation. As long as the module uses the theme system properly, a theme will be able to override any HTML and CSS by hot-swapping its own implementation for the module's implementation.

After the data has been retrieved and manipulated in the heart of your module (the business logic), it will need to provide the default theme implementation. Sometimes a particular theme will need to override your implementation in order for it to achieve a specifc design goal; if the theme provides its own implementation, Drupal will use the theme implementation instead of the module's default implementation.

$variables = array('items' => $list, 'type' => 'ol');
$content = theme('item_list', $variables);

By calling the theme() function, we are delegating the responsibility of determining and using the proper theme implementation. We're saying:

"Hey, theme()! I want to markup my data as an item_list. Can you do that for me? I don't need to know the details. kthxbye."

Our module just needs to decide which theme hook it wants to use to markup its data. Should the data be displayed in an unordered list, a table, or a wordle?

Hook crazy?

In addition to API hooks, Drupal also has theme hooks. A theme hook is simply the name of a particular way to markup some data. For example, passing data to the item_list theme hook will result in different markup then passing data to the links theme hook. However, while normally every module's hook function will be called when Drupal invokes an API hook, only one theme hook implementation will be invoked when Drupal invokes a theme hook.

There are actually two different ways you can make an implementation (which we will discuss later), but for now we'll only talk about the simplest method for module developers—theme functions. When you call theme(), it will look for a default theme function named theme_HOOKNAME and for an optional theme override function called THEMENAME_HOOKNAME. If you dig into Drupal's internals, you'll fnd a theme_item_list() inside includes.inc or theme.inc. This is Drupal's default theme implementation for an item_list. If our active theme was Bartik, and if Bartik implemented a theme override called bartik_item_list(), then theme() would use the Bartik theme's implementation instead of the default one.

The preceding fgure shows one piece of data as it passes through a module and a theme. However, in order for you to understand the full power of Drupal's theme layer, you also need to understand how the entire page is built.

However, since all of the active theme's modifcations occur after any module modifcations, from a module developer's perspective, all of this theme inheritance is transparent. Since modules don't need to know anything about the structure o the theme and its ancestry, we'll simply talk about "the theme" in this book. Just be aware that the actual theme may be more complex.

Base themes and sub-themes
If you've previously read anything about Drupal theming, you've probably heard about base themes and sub-themes. Any theme can declare a parent theme in its .info file using the base theme key and it will inherit all the hook implementations from its parent theme (and its parent's parent theme, and so on).

Data granularity

One of the things that makes Drupal theming so powerful is its granularity. Each piece of content is handled separately as it's passed through the theming system. Each bit of data is themed individually, then combined into ever-larger chunks. At each step in the aggregation process, it's themed again. The following illustration will make this clearer:

As you can see in the preceding illustration, for a typical blog post, each comment is pulled from the database and sent through the theme system to get HTML markup added to it. Then all the comments are aggregated together into a "comment wrapper" where additional markup and, usually, a "new comment" form is added. Then the single group of comments is passed to the node theming where it is combined with other pieces of the blog post's content. This process of theming bits of content, aggregation, and theming again is repeated until we've built the entire HTML page ready to be sent to a web browser.

There are two advantages to this granular system. First, since each module is responsible for theming its own data, it can either create a very specialized theme hook for its data or it can re-use an existing theme hook. Re-using a theme hook ensures a consistent set of markup for similar data structures while still allowing customized CSS classes (Most theme hooks allow custom classes to be passed as parameters.) For example, the list of links after a node (read more, add new comment, and so on) re-uses the links theme hook, and the links after each comment use the same links theme hook.

The second advantage is for the theme developer. Having a fine-grained theming system means that a theme, if it chooses to, can literally rewrite all of the markup for its own design purposes. As module developers we need to be keenly aware of the themer's desire to have granular theming overrides.

 

Theme engines

Some themes require alternate theme engines. Theme engines can provide alternate template syntax, naming standards, and helper functions. Several theme engines are available for download at http://drupal.org/project/theme+engines. However, we won't be discussing any theme engines except for Drupal's default theme engine, PHPTemplate. The PHPTemplate theme engine has been the default theme since Drupal 4.7, has been continuously improved with each version, and has proven its worth again and again. Over 99% of themes available for download on drupal.org use the default PHPTemplate theme engine.

Two ways to theme

So now that we have a good understanding of higher level concepts, let's get down to the nitty-gritty of theme implementations. There are actually two different ways to implement a theme hook:

  1. Theme functions: pass data to a PHP function to wrap it in markup
  2. Templates: pass data to a template which is a PHP file mixed with markup and PHP print statements

Let's look at each of these in turn.

Theme functions

For a module developer, the easiest type of implementation to understand is a theme function. Theme functions just need to follow a few simple rules in order for them to work properly.

First, the name of the theme function follows the pattern:

theme_[theme hook name]

Since the theme hook name is used directly in the theme function's name, theme hook names have the same constraints on naming as regular PHP function names; the only valid characters in theme hook names are alphanumeric characters and underscores. So if a module has created an example_format theme hook, it would implement it with theme function named theme_example_format().

Second, the theme function will only have a single parameter, as follows:

function theme_THEME_HOOK_NAME($variables) {…}

The theme function variables are an associative array containing the pieces of data we wish to markup and any options we want to pass to the function. It may seem extremely odd not to use multiple parameters and PHP's ability to specify default values for each parameter. In fact, previous versions of Drupal did use multiple parameters. We'll see why Drupal now only uses one parameter in just a moment when we talk about preprocess functions.

For an example of a $variables array, let's look at how the DocBlock of the theme_item_list() function defnes it:

  • Items: An array of items to be displayed in the list. If an item is a string, then it is used as is. If an item is an array, then the data element of the array is used as the contents of the list item. If an item is an array with a "children" element, those children are displayed in a nested list. All other elements are treated as attributes of the list item element.
  • Title: The title of the list.
  • Type: The type of list to return (e.g. ul,ol).
  • Attributes: The attributes applied to the list element.

The items and title keys hold the actual data, and the type and attributes keys are options that specify how to build the item list.

Third, the theme function should return a string that contains the rendered representation of the data. This is usually a string of HTML, but some theme hooks return other types of themed markup. For example, theme_syslog_format returns a simple string with pipe-separated data values for use in a *NIX syslog error log.

That's it! As you can see, theme functions have very simple requirements and in every other way are standard PHP functions.

The major difference between most functions and theme functions is that you should never call theme functions directly. It may be tempting to take your data and call theme_item_list($vars) directly, but you should instead call theme("item_list", $vars). This method of calling theme functions indirectly ensures that themes are able to override any module's default theme function (or template). It also allows the theme() function to work additional magic, including allowing other modules to alter the theme function's variables before they are used.

Preprocess functions

Now we're starting to see the real flexibility of the theme system. Preprocess functions allow one module to alter the variables used by another module when it calls a theme hook. So if some code passes data to theme() for a particular theme hook, preprocess functions will be called to alter the data before the actual theme hook implementation is called. The following steps are carried out:

  1. Code calls theme('hook_name', $variables).
  2. theme() calls preprocess functions for hook_name.
  3. Preprocess functions modify variables.
  4. theme() calls actual implementation for hook_name with modifed variables.

All preprocess functions take the form of:

[module]_preprocess_[theme hook name](&$variables)

So if the foo module wants to alter the variables for the item_list theme hook, it could define the function as follows:

function foo_preprocess_item_list(&$variables) {
// Add a class to the list wrapper.
$variables['attributes']['class'][] = 'foo-list';
}

Notice that the $variables parameter is defned with an ampersand in front of it. That's PHP notation to pass the parameter by reference. Instead of getting a copy of the variables, the foo_preprocess_item_list() function will get access to the actual $variables which is later passed to the theme function implementation. So any modifications that the preprocess function makes to the $variables parameter will be preserved when those variables are passed to the theme function. That's the reason our example foo_preprocess_item_list() function doesn't return anything; its work is done directly on the original $variables.

This is extremely handy for module developers as it allows all sorts of integration with other modules. Since the variables parameter is a mix of data and options, modules can alter both the raw data and change the way data will be rendered. This can be as simple as one module needing a special class for use in its JavaScript code and adding that class to another module's themed content by appending to the $variables['attributes']['class'] array, or can be more complex interactions like the i18n module translating the language used in blocks.

Imagine we've built a retro module that integrates GeoCities and we want to replace all links to a user's profle page with a link to the user's GeoCities homepage. We can do that relatively easily with a preprocess function.

First let's look at the following theme_username function's documentation:

/**
* Format a username.
*
* @param $variables
* An associative array containing:
* - account: The user object to format.
* - name: The user's name, sanitized.
* - extra: Additional text to append to the user's name, sanitized.
* - link_path: The path or URL of the user's profile page, home
* page, or other desired page to link to for more information
* about the user.
* - link_options: An array of options to pass to the l() function's
* $options parameter if linking the user's name to the user's
* page.
* - attributes_array: An array of attributes to pass to the
* drupal_attributes() function if not linking to the user's page.
*/

Quite conveniently, theme_username() has a handy $link_path variable that we want to alter to achieve our old-school giggles. Assuming that we've used some other business logic with the user module's hooks to load our GeoCities URL into the user's account (the "hard" part), replacing the link to the user's profle page can be accomplished with the following simple preprocess function:

/**
* Implements awesomeness with hook_preprocess_username().
*/
function retro_preprocess_username(&$variables) {
$variables['link_path'] = $variables['account']->geocities_url;
}

That's it! We don't have to override the user module's theme implementation; we just modify its parameters.

Theme overrides

While module developers usually don't have to worry about whether a theme overrides a particular theme function or not, it's still important to understand how this mechanism works.

Drupal theme is normally composed of CSS, images, JavaScripts, template files (discussed shortly), a .info file, and a template.php file. The template.php file is analogous to a module's .module file. It contains all of the PHP functions for the theme and is automatically loaded when the theme is initialized.

If a theme wants to override a particular theme function, it needs to copy the theme function from its original location and paste it into its template.php file. Then it needs to change the function's prefix from theme to its own name and finally, it needs to start making the desired changes to the function.

For example, if the Bartik theme wants to override the theme_menu_local_tasks() function in order to add some markup around the page's tabs, it would copy the entire function from includes/menu.inc, paste it into Bartik's template.php, and rename it to bartik_menu_local_tasks().

Fortunately, when a theme overrides a default theme function, a module's preprocess functions continue to work as normal.

Themes also have the ability to create preprocess functions. If the Bartik theme decides to format a user's name in "last name, frst name" format, it can implement a bartik_preprocess_username() function. Fortunately, a theme's preprocess functions do not override a module's preprocess functions. All preprocess functions are run; frst any module's preprocess functions and then the theme's preprocess function.

Template files

While theme functions might be the easiest for module developers to understand, template files are the easiest for themes to grasp. When a theme hook is implemented with template files, they are used instead of theme functions. However, from a module developer's standpoint, there is actually a remarkable amount of similarity between template files and theme functions. First, let's take a closer look at template files.

Templates are files primarily containing HTML but with some PHP statements mixed in using the template's variables. Instead of declaring a theme_hook_name() function, a module would instead create a hook-name.tpl.php file. The following are the contents of a typical template file, typical-hook.tpl.php:

<div class="<?php print $classes; ?>"<?php print $attributes; ?>>

<?php if ($title): ?>
<h2<?php print $title_attributes; ?>>
<?php print $title; ?>
</h2>
<?php endif;?>
<div class="submitted">
<?php print t('By !author @time ago', array(
'@time' => $time,
'!author' => $author,
)); ?>
</div>

<div class="content"<?php print $content_attributes; ?>>
<?php
// We hide the links now so that we can render them later.
hide($content['links']);
print render($content);
?>
</div>

<?php print render($content['links']); ?>
</div>

The preceding example shows the full gamut of the things that you are likely see in a template file. They are as follows:

  1. Printing a variable containing a string?
  2. Printing a translatable string using t()
  3. Conditional if/else/endif statement
  4. Delaying rendering on part of a render element with hide()
  5. Printing a render element

All of the PHP in a template should be limited to printing out variables. This limited amount of PHP makes it much easier for non-programmers to learn how to use template fles compared to theme functions. However, for module developers, the template implementation is still very similar to the theme function implementation; the handful of differences are relatively minor.

As with theme function implementations, our module would still need to invoke the theme hook using theme().

$variables = array('typical' => $typical_object);
$output = theme('typical_hook', $variables);

The theme() function would discover that the typical_hook theme hook was implemented as a template and render the corresponding typical-hook.tpl.php file.

The only valid characters in theme hook names are alphanumeric characters and underscores. This is true of all theme hooks, regardless of whether they are implemented as a theme function or as a template file. However, when theme() looks for template implementations, it will automatically convert any underscores in the theme hook name into hyphens while searching for the template file. For example, calling theme('user_picture', $variables) will result in the template file named user-picture.tpl.php being rendered.

Also, just like theme functions, other modules can modify the variables using preprocess functions.

In template fles the focus is on printing out variables in various places in the markup. So for template fles, the preprocess function takes on a more important role. The only difference between a theme function's preprocess functions and a template file's are the number and type of preprocess functions.

Drupal 7 Module Development Create your own Drupal 7 modules from scratch
Published: December 2010
eBook Price: $26.99
Book Price: $44.99
See more
Select your format and quantity:
        Read more about this book      

(For more resources on this subject, see here.)

The preprocess zoo

When you write a theme function, its natural to pass the raw data in as parameters and generate any display-related meta-data inside the function. With a template file, that's not really possible without putting complex PHP inside the template. However, as was stated earlier, all of the PHP in a template file should be limited to just the bare minimum required to print out a PHP variable. Any processing that we need to do on the raw data parameters to ease it into print-ready variables should be done in preprocess functions.

"template_" preprocess functions

When a module defnes a theme hook by creating a template file, that module should also create a corresponding preprocess function to set up and process any variables that are needed by the template file, but are not passed as parameters to heme(). By convention, that preprocess function should be of the following form:

template_preprocess_[theme hook name](&$variables)

The template_prefix tells Drupal's theme system that this preprocess function is the primary preprocessor for the theme hook's variables and should be run before any other module's preprocess function.

Here's an example that should make this concept a bit clearer. This is an actual code snippet from Drupal's block preprocess function. In each page region, all of the blocks in the region get a variable whose value alternates between "odd" and "even". These values can be used to create zebra-striped styling, that is, alternate styling on every other block in a region.

function template_preprocess_block(&$variables) {
// We store all block counters using drupal_static().
$block_counter = &drupal_static(__FUNCTION__, array());

// All blocks get an independent counter for each region.
if (!isset($block_counter[$variables['block']->region])) {
$block_counter[$variables['block']->region] = 1;
}

// Generate the zebra striping variable.
$variables['block_zebra'] = ($block_counter[$variables['block']-
>region] % 2) ? 'odd' : 'even';

// Increment the region's block count.
$block_counter[$variables['block']->region]++;
}

The PHP logic in this function is directly related to the display of the block and not to the general business logic of this data. So, it doesn't make sense that the block module would calculate that meta data before calling theme(); the meta data clearly belongs to the display logic, which is why it's placed in the block module's preprocess function.

Multi-hook preprocess functions

In some rare circumstances, you may need to alter or provide some variables for all theme hooks. In fact, Drupal's theme system does provide some variables to all templates; the preprocess function that provides these variables is both a "template_" preprocess function and a multi-hook preprocess function. Multi-hook preprocess functions are simply functions that don't have a _HOOK suffx added to their name and are run for every single template file. Their name is of the following form:

[module]_preprocess(&$variables, $hook)

Obviously, there can be a big performance hit if a module needlessly implements a multi-hook preprocess function. If you're contemplating writing one, if at all possible, consider writing several preprocess functions that target the specifc hooks you need instead, rather then hit all hooks.

Now, if you were paying close attention to the form of the name, you'll also notice that these functions actually receive two parameters, namely, the $variables array and a $hook parameter. $hook, as the name suggests, contains the name of the actual theme hook currently being run. So, while a foo_preprocess(&$variables, $hook) function is run for every template file, it will still be able to tell which template is currently being requested. In fact, $hook is the second parameter for all preprocess functions, but $hook is only useful for multi-hook preprocess functions.

For a good example of a multi-hook preprocess function, let's look at the function that the theme system uses to set up several variables common to all template files—the template_preprocess() function, which is as follows:

function template_preprocess(&$variables, $hook) {
// Tell all templates where they are located.
$variables['directory'] = path_to_theme();

// Initialize html class attribute for the current hook.
$variables['classes_array'] = array(drupal_html_class($hook));
}

As you can see, this preprocess function creates a $directory variable which can be used to tell where the template file is located on the web server. In the $classes_array variable, it also starts to set up the CSS classes used in the outer-most wrapping div of the template.

Process functions

Obviously, inside our template file, when we print out our dynamically created list of classes, we'll need the variable to be a string. <?php print $classes_array; ?> will, most unhelpfully print out "array". In earlier versions of Drupal, classes were dynamically created but were immediately concatenated into strings. So themes would see one long string with multiple classes in it, menu-block-wrapper menu-block-1 menu-name-management, for example. This made removing or altering classes difficult as themers had to master PHP's string-manipulation functions or even (gasp!) regular expressions.

In Drupal 7, this problem for themers has been solved using the new process functions. Process functions are an additional phase of variable processing functions that run after the initial preprocess functions. In all respects, process functions are exactly like preprocess functions; there are template_ prefixed process functions, multi-hook process functions, module-provided process functions, and theme-provided process functions. The only difference is that process functions are run after all preprocess functions have been run.

Process functions are extremely useful when you have meta data that is likely to be manipulated by other modules or themes and you wish to delay the rendering of the meta data until just before the template file itself is rendered.

In the preceding code example, the template_preprocess() function creates a $classes_array variable that holds an array of classes to be used on the wrapping div in the template file. Modules and themes can easily add classes by simply adding an additional array element from inside their preprocess function, as follows:

$variables['classes_array'][] = 'extra-savoir-faire';

Themes can use much simpler array manipulation functions in order to remove or alter classes.

// Search for the bogus class and return its array key
// location. If not found, array_search returns FALSE.
// Remember that 0 is a valid key.
$key = array_search('bogus', $variables['classes_array']);
if ($key !== FALSE) {
// Alter the class.
$variables['classes_array'][$key] .= '-dude';
}
// Or remove the no-soup class.
$variables['classes_array'] = array_diff($variables['classes_array'],
array('no-soup'));

In addition to the $classes_array variable, the template_preprocess() function also creates $attributes_array, $title_attributes_array, and $content_attributes_array variables which are used for HTML attributes on the outermost wrapping div, the title's heading tag, and the content's wrapping div, respectively. You'll see each of these variables used in the typical-hook.tpl.php example.

After modules and themes are given an opportunity to alter these variables, the theme system uses the template_process() function to render those arrays into a simple string, as follows:

function template_process(&$variables, $hook) {
// Flatten out classes.
$variables['classes'] = implode(' ', $variables['classes_array']);

// Flatten out attributes, title_attributes, and content_attributes.
$variables['attributes'] = drupal_attributes(
$variables['attributes_array']);
$variables['title_attributes'] = drupal_attributes(
$variables['title_attributes_array']);
$variables['content_attributes'] = drupal_attributes(
$variables['content_attributes_array']);
}

A similar problem troubled module developers in Drupal 6. It was impossible to call drupal_add_css() or drupal_add_js() in a MODULE_preprocess_page() function because the lists of CSS files and JavaScript files were already generated before any of the preprocess functions were run. Again, process functions come to the rescue. Drupal 7 delays the generation of these lists until the template_process_html() function is run.

Drupal 7 Module Development Create your own Drupal 7 modules from scratch
Published: December 2010
eBook Price: $26.99
Book Price: $44.99
See more
Select your format and quantity:
        Read more about this book      

(For more resources on this subject, see here.)

Order of preprocess execution

Now with all these different favors of processing functions, it can get a bit confusing as to which function runs in what order. Fortunately, there are just three simple rules that are used to determine the order of processing. They are as follows:

  • All preprocess functions run before all process functions
  • template_ prefixed functions run frst. [module]_ prefixed functions run next. [theme]_ prefixed functions run last
  • Multi-hook functions run before hook-specifc functions

This results in the following order of execution for a particular theme hook:

 

  1. template_preprocess()
  2. template_preprocesss_HOOK()
  3. MODULE_preprocess()
  4. THEME_preprocess()
  5. THEME_preprocess_HOOK()
  6. template_process()
  7. template_processs_HOOK()
  8. MODULE_process()
  9. MODULE_process_HOOK()
  10. THEME_process()
  11. THEME_process_HOOK()

Whew

If the THEME is actually a list of inherited base and sub-themes, each THEME_-prefixed item above could be a list of each base theme's and sub-theme's functions, which would make the list even longer. See the "Base themes and sub-themes" tip near the beginning of this chapter if you haven't read it already.

Summary

In this article we have covered:

  • Business logic versus presentation logic
  • Data granularity
  • Theme engines
  • Two ways to theme

Further resources on this subject:


About the Author :


John Wilkins

John Wilkins has been a web developer for a long time. In April 1993, he was one of the lucky few to use the very first graphical web browser, Mosaic 1.0, and he’s been doing web development professionally since 1994. In 2005, John finally learned how idiotic it is to build your own web application framework and discovered the power of Drupal; he never looked back.

In the Drupal community, he is best known as JohnAlbin, one of the top 20 contributors to Drupal 7 and the maintainer of the Zen theme, which is a highly-documented, feature-rich “starter” theme with a powerfully flexible CSS framework. He has also written several front-end-oriented utility modules, such as the Menu Block module. John currently works with a bunch of really cool Drupal developers, designers, and themers at Palantir.net.

Books From Packt


Drupal 7
Drupal 7

Drupal 6 Attachment Views
Drupal 6 Attachment Views

Drupal 6 Search Engine Optimization
Drupal 6 Search Engine Optimization

Drupal 6 Performance Tips
Drupal 6 Performance Tips

Drupal 6 Panels Cookbook
Drupal 6 Panels Cookbook

Drupal E-commerce with Ubercart 2.x
Drupal E-commerce with Ubercart 2.x

Flash with Drupal
Flash with Drupal

Learning Drupal 6 Module Development
Learning Drupal 6 Module Development


Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software