About overriding themable output

Last modified: August 24, 2009 - 11:33

The following only applies when the default markup needs changes. This section can be skipped if the presentation is handled only through style sheets.

There are three aspects to overriding the themed output. The first is knowing where the source originates, the second is providing the override, and third is understanding its type.

Note that Drupal maintains cached theming data through the theme registry. It must be cleared when setting up overrides.

1. Finding the source:

Finding the source can be difficult to track down due to the hierarchy of theming calls whose source can be spread throughout the whole system.

output overrides
Links to 520KB PDF. Provided for illustration only.

Most of the page elements are typically pulled with theme('page') and placed inside the page.tpl.php template after rendering navigation bits, the bits within the navigation bits, block regions, the blocks within the block regions, etc. Each chunk of themed data is often referred to as a theming "hook".

Note: Theming functions and templates will now be referred to as theming hooks. There are many hooks unrelated to theming throughout the system. Any mention of it here only relates to theming.

Getting to the source of any specific chunk of markup can now be tracked with the devel module. It includes a theming tool to easily visualize the source of any output, its type and tons of other theming data. See the screencast for a demonstration. This is not available for Drupal versions less than 6 due to technical limitations.

2. System of overrides:

The system of overrides has a specific cascading order with few exceptions. Drupal core and modules provide a reasonable default for its markup handled by the theming hook. If it does not suit the requirements of the theme, then an alternate can be provided preventing the defaults from being used. This way, the defaults are left alone and all the theme specific changes are localized to the theme. You should never change code outside your theme to alter default output. (Here are some reasons why you should not "hack core.")

output overrides

If you need more control beyond this convention of overrides, the theme registry can be manipulated for more control.

Note: Although it is still possible, PHPTemplate.engine in Drupal 6 no longer overrides theme functions. In 5, that is what allowed templates to be used for a handful of the theming hooks. It is no longer necessary.

3. Functions vs. templates:

There are two ways of implementing a theming hook, either a function or a template. The type that is used should depend on what you're trying to achieve. Drupal core and modules can use either type to construct the output, and themes can use the same type or change it.

theme flow - function vs template
Links to PDF. Flow map for 5 also available for comparison.

Functions are about five times faster than templates, but they can be difficult for designers who may be only familiar with XHTML. The speed depends on the nature of the hook multiplied by the number of times it is called on a page. For example, using templates for the theming hook "links" could cause a noticeable performance hit for complex and busy sites due to its repeated use.

Note: In versions before 6, core and modules could only work as functions for the theming hooks. Implementing through templates was done by PHPTemplate by overriding the hooks and doing the conversion on the engine layer.

Here are two examples on overriding with the help of devel themer.

Overriding functions:

The theme function theme_menu_local_tasks is a simple function for outputting both primary and secondary tabs. The theming hook in this case is "menu_local_tasks". To override, the "theme" prefix in the function name can be changed to the name of your theme or the theme engine the theme is running under. Using the theme name is recommended due to potential name collisions with sub-themes.

search override

The example shows Garland is using the name of the theme engine for the override. If you were to create a sub-theme based on Garland, then using the name of your theme is mandatory.

Placing the following inside the theme's template.php file will override the default after clearing the theme registry. Change "mytheme" in the function name to the name of your theme.

<?php
function mytheme_menu_local_tasks() {
 
$output = '';

  if (
$primary = menu_primary_local_tasks()) {
   
$output .= "<ol class=\"tabs primary\">\n". $primary ."</ol>\n";
  }
  if (
$secondary = menu_secondary_local_tasks()) {
   
$output .= "<ol class=\"tabs secondary\">\n". $secondary ."</ol>\n";
  }

  return
$output;
}
?>

The only change here is the markup from an unordered list to an ordered list.

A listing of all the theme functions can be found on api.drupal.org.

Overriding templates:

If the default implementation is done as a template then simply copying the source template file into the theme will automatically override it after clearing the theme registry. Here is an example for search-theme-form.tpl.php. Note that the theming hook in this case is "search_theme_form" with the template using hyphens instead of underscores.

search override

That is all you need to do. Open the copied template in your editor to make your alterations. All the core .tpl.php files are documented. It should give a good indication on what can be done with the output.

Note: templates can be placed in any directory within the theme. This allows for better management and less clutter in the base level of the theme directory.

Related pages:

Converting from functions to templates

Converting a theme function into a template takes some initial work but once it is done, it's easier to work with. If you are collaborating with a designer, the conversion will enable them to focus on designing, not coding. There are many templates already available in core and many more will be converted in future versions. Contributed modules that follow best practices should also have templates available. These instructions are provided for the theming hooks not already made available as templates.

Getting Drupal to recognize the theming hook as a template is automated. These are the only requirements to trigger this change:

  • Name of the template must match the theming hook.
  • The underscores in the hook must be changed to hyphens.
  • The template name must have an extension of ".tpl.php". (It can vary based on the theme engine.)

Consider the theming function theme_user_signature. The theme hook here is "user_signature". Creating a file named "user-signature.tpl.php" will tell Drupal that the hook is now a template after clearing the registry. Any content in this file will now take the place of the function. The part that takes more work is setting up the variables to be used in this file and that is done through preprocess functions.

A few notes:

  • While it is possible to code directly inside the template, it is not considered good practice. All the complex logic should be separated from the .tpl.php file and placed within preprocess functions. This keeps it clean and easy to manage.
  • There is also the issue of security. The separation can minimize the chance of cross-site scripting attacks by cleaning out potentially malicious user generated content. When handing over template files to your designer, all the output should be clean so they do not have to concern themselves with security issues.
  • If your theme implements both a theme function and a template for a given hook, the theme function will always be used.
  • The auto discovery of theme overrides are done by PHPTemplate engine so make sure your theme has set the engine from the .info file.
  • Compare the theming functions in 5 to the template conversions in 6 for forums. You can use that as an example on converting between the two types.
The theme registry:

Drupal's theme registry maintains cached data on the available theming hooks and how to handle them.

For most theme developers, the registry does not have to be dealt with directly. Just remember to clear it when adding or removing theme functions and templates. Editing existing functions and templates does not require a registry rebuild.

To clear the theme registry, do one of the following things:

  1. On the "Administer > Site configuration > Performance" page, click on the "Clear cached data" button.
  2. With devel block enabled (comes with devel module), click the "Empty cache" link.
  3. The API function drupal_rebuild_theme_registry.

The theme registry is cached data instructing Drupal on the available theming hooks and how to handle it by indicating its type. In previous versions all theming calls were handled on the fly. Since a lot more work is being done under the hood, the cached instructions speeds up the process especially for templates. The theme engine your theme is running under should automatically register all the theming hooks for you.

There are special cases where you may need to work with the registry directly. When your theme requires a new hook to be registered that was not already implemented on the layers below it (core, modules, engine). This includes some forms when they are not explicitly themed by core or modules but instead rely on the default form presentation.

  • More details can be found in the sub-page, The theme registry for special cases.
  • Do not confuse the theme registry with the theme's .info file which is also cached. Points 1 and 2 on clearing the registry will clear both.
  • Your theme must be using phptemplate engine for its templates and functions to be discovered. Other engines should behave the same way. For engineless themes, it must be done manually. See phptemplate_theme to see how it is done.

Suggestion: file re-naming or not?

nwwoman - April 4, 2009 - 14:40

In the text above, the following is written: "If the default implementation is done as a template then simply copying the source template file into the theme will automatically override it after clearing the theme registry."

I'm a noob to Drupal and at the start of learning PHP. I do have xhtml, css experience. Based on my Drupal doc. reading, I suspect it's accurate to say that after copying the source template, per the instructions, that the file name can remain the same. If that's true, then I'd like to suggest that extra detail be added to the text as it's tiny steps such as this that can make executing instructions so much easier for those of us who aren't as experienced as the authors.

Thank you so very much for the lengthy article. It's so helpful to have detailed instructions.

what's the right way to override core theme functions?

jefkin - May 9, 2009 - 08:41

Deep in the dark abyss of core, in the file includes/form.inc is a function that some might think never needs to change: theme_button().

Well If all you ever want to see are text buttons then maybe you'll never need to change this one.

Sure, you say, that's what '#type' => 'image_button' is for right?

Well, I want (client needs) images and text in the buttons.

And we don't want to have to build graphics for each button, particularly since this is a multi lingual drupal.

So, rather than

<input type="submit" name="mybutton" id="nice-button" class="form-button" value="Click me" />
<input type="submit" name="mysubmit" id="nice-submit" class="form-submit" value="Submit me" />

I need to have:

<button type="button" name="mybutton2" id="nicer-button" class="form-button">
  <div><img src="/one-img.png" /> Click me!</div>
</button>
<button type="submit" name="mysubmit2" id="nicer-submit" class="form-submit">
  <div><img src="/some-img.png" /> Submit me!</div>
</button>

My first thought was to use '#type' => 'markup', but these buttons need '#ahah' elements, and '#ahah' isn't allowed with markup. :(

Like I said I don't want to hack core, so no way I want to make a new '#type' for forms, that would make my head spin.

So I want to generate the above two buttons with this:

<?php
$form
['button'] = array ( '#type' =>'button'
                     
,   '#name' =>'mybutton2'
                     
,   '#id'   =>'nicer-button'
                     
,   '#value'=>'<div><img src="/one-img.png" /> ' . t('Click me!') . '</div>'
                     
,   '#ahah' =>array ( ... ahah stuff ... )
                      , );
$form['submit'] = array ( '#type' =>'submit'
                     
,   '#name' =>'mysubmit2'
                     
,   '#id'   =>'nicer-submit'
                     
,   '#value'=>'<div><img src="/some-img.png" /> ' . t('Submit me!') . '</div>'
                     
,   '#ahah' =>array ( ... ahah stuff ... )
                      , );
?>

or better yet:

<?php
$form
['button'] = array ( '#type' =>'button'
                     
,   '#name' =>'mybutton2'
                     
,   '#id'   =>'nicer-button'
                     
,   '#value'=>theme('my_button_icon', 'one-img', t('Click me!'))
                      ,  
'#ahah' =>array ( ... ahah stuff ... )
                      , );
$form['submit'] = array ( '#type' =>'submit'
                     
,   '#name' =>'mysubmit2'
                     
,   '#id'   =>'nicer-submit'
                     
,   '#value'=>theme('my_button_icon', 'some-img', t('Submit me!'))
                      ,  
'#ahah' =>array ( ... ahah stuff ... )
                      , );
?>

Where I can handle the theming and finding the images. (already done)

The problem arises that theme_button() writes an <input> and I need a <button></button>;

I tried this:

<?php
/*
** new theming extension function to make buttons work correctly.
**
** mostly copied from the theme_button in core: includes/form.inc
** modified to find the 2 div's we use for css formating and use
** <button>... stuff ... </button> format  instead of
** <input ... stuff ... />
*/
function my_themer_button($element)
{
 
// Make sure not to overwrite classes.
 
if (isset($element['#attributes']['class']))
  {
   
$element['#attributes']['class'] =
     
'form-'. $element['#button_type'] . ' ' .
     
$element['#attributes']['class'];
  }
  else
  {
   
$element['#attributes']['class'] = 'form-'. $element['#button_type'];
  }
  if (
2 <= preg_match_all('/div/', $element['#value']))
  {
    return 
'<button type="submit" '
         
. (empty($element['#name'])
                ?
'' : 'name="' . $element['#name'] . '" ')

          .
'id="'         . $element['#id']
          .
'" '           . drupal_attributes($element['#attributes'])
          .
'>'            . check_plain($element['#value'])
          .
"</button>\n";
  }
  return 
'<input type="submit" '
       
. (empty($element['#name'])
              ?
'' : 'name="' . $element['#name'] . '" ')

        .
'id="'      . $element['#id']
        .
'" value="' . check_plain($element['#value'])
        .
'" '        . drupal_attributes($element['#attributes'])
        .
" />\n";
}
?>

Which seems ok, code-wise, provided that the function is called in place of theme_button(). But this function is not called.

Is the *ONLY* way to override the core theme function to create a templated theme? I hope not, because I don't know how to attach the '#ahah' form ingredients of these buttons, since '#ahah' isn't allowed with '#type' => 'markup'

If I have to use the template, how can I get "at" the magic '#ahah' stuff? Which, I think lives in the $element['#attributes'];

If I did use a templated theme with a preprocess function, the preprocess function gets the $variables or $vars argument, does $vars contain an 'element' value?

Update: a Solution of sorts

jefkin - May 9, 2009 - 18:38

There may be a more elegant solution out there, but I did find a way to override the theme_button core function:

Instead of function my_themer_button($element) I used function phptemplate_button($element).

This solves the problem for the specific case of my site, but this begs the question.

    What happens if another module wants to override the same core function, besides the obvious conflict if they also use phptemplate_button function name.

I suppose I'd have to make a theme to handle it.

Well in my case I'll stick with phptemplate_button, but I think there has to be a better way?

Can anyone clue me in?

That's my question too.

kratib - November 14, 2009 - 23:22

Templates in sub-directories do not work for me.

yakker - September 14, 2009 - 22:09

Note: templates can be placed in any directory within the theme. This allows for better management and less clutter in the base level of the theme directory.

I've tried this many times, sometimes keeping "base" templates in the base directory (node.tpl.php), sometimes moving all, sometimes one. Without fail: any template that I put in a subfolder of my base theme's directory (no sub themes going on here) is no longer recognized. Is there a step you have to do to alert Drupal to your theme's extra tpl files? Cache refresh isn't the issue (in fact, you have to do that to expose the issue).

Sorry... I originally thought that if I had node.tpl.php in my theme directory, any node-based tpl files in subdirectories would work. They do not - node.tpl.php must be in the subdirectories (too). Which makes some sense now that I understand it.

Templates in subfolders

scott.whittaker - November 10, 2009 - 04:01

I can get node-based templates to work in subfolders but nothing else seems to work.

For example I have a subfolder called node and all the node-based templates go in there: node-whatever.tpl.php and that works just fine, and as far as I can tell this also seems to work for views and blocks.

But it doesn't work for page-based templates - admittedly most of which come from additional template suggestions created in template.php.

Is there a way to get Drupal 6 to look for custom templates in a subfolder (or better, in subfolders)?

My page-whatevers are working...

yakker - November 10, 2009 - 20:08

But I've had to put them all in the same subfolder along with page.tpl.php. I.e. I have a subfolder called tpl_page/ and inside it are page.tpl.php and all the page-whatever.tpl.php's. It's working for blocks, views, node etc. as well... Not sure if that helps at all (my pages are also based on custom suggestions).

 
 

Drupal is a registered trademark of Dries Buytaert.