Drupal 7 Module Development: Drupal’s Theme Layer

0
108
16 min read

 

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.

LEAVE A REPLY

Please enter your comment!
Please enter your name here