hook_theme_registry_alter for advanced template control

hook_theme_registry_alter for advanced template control

Posted 02/17/2008 - 15:00 by Michelle

I'm in the process of porting Advanced Forum to Drupal 6.x and am trying to take advantage of the new theme system. One of the things I wanted to do was get rid of the requirement to copy the forum theme to the site theme directory. My goal was to make it so the whole thing could be run right out of the module directory but allow any template copied to the user's theme to override what's in the module. merlinofchaos helped me quite a bit and told me about hook_theme_registry_alter(), which isn't documented much yet. I decided to write up what I learned today in this article. Keep in mind that this is written by someone who has only really dug into the new theme system in the last few days, but merlinofchaos gave it a look over and he thought it looked ok. If you have comments on the article, I'd like to hear from you, but please no support questions. I barely understand this myself. ;)

---

The theme system in D6 does an excellent job of handling template files which will work well for most people. If you need very specific control over the theme registry, however, there is hook_theme_registry_alter(). This hook lets you get in and modify the registry directly. It has a paramater, $theme_registry, which holds the whole registry. To see what's in it, you can use something like this:

<?php
MODULENAME_theme_registry_alter
($theme_registry) {
 
// Assuming you have the devel module installed
 
dsm($theme_registry);
}
?>

This will give you a nicely formatted listing of the entire array to check out. By modifying this array, you can affect how template files are handled.

Example 1
Let's say you want to provide a node template in your module but you want the user of your module to be able to override that template in their theme. How do you do that? Well, you start out with:

<?php
function MODULENAME_preprocess_node(&$vars) {
  if (
$some_condition_is_met) {
   
$vars['template_files'][] = 'special-template-name';
  }
}
?>

That works great for the special-template-name.tpl in the user's theme directory, but what if they don't want to put it in their theme? How do you get it to default to the one in your module? You might think you can just add in the path to the module before that, but that doesn't work. the template_files variable won't accept paths outside of the theme. So, the trick is to convince the registry that your module is a theme. That's where the hook comes in.

<?php
function MODULENAME_theme_registry_alter($theme_registry) {
 
// Remove the first path under 'node' which is the one for the
  // module that created the template
 
$originalpath = array_shift($theme_registry[$template]['theme paths']);
 
 
// Get the path to your module
 
$modulepath = drupal_get_path('module', 'MODULENAME');
 
 
// Stick the original path and then your module path back on top
 
array_unshift($theme_registry[$template]['theme paths'], $originalpath, $modulepath);
}
?>

Now the theme registry will look first to the user's theme for the template file and then to your module directory.

Example 2
You can use MODULENAME_preprocess_ITEM() to add in variables before they go to the template file. This can be used on your own theme items or existing ones. In the case of existing ones, your variables are merged into the ones made by the original preprocess code. This is a great feature if you just want to add to it. But what if you don't want the other preprocess code to run at all? hook_theme_registry_alter() to the rescue again.

<?php
function MODULENAME_theme_registry_alter($theme_registry) {
  foreach (
$theme_registry['TEMPLATE_TO_OVERRIDE']['preprocess functions'] as $key => $value) {
    if (
$value == 'PREPROCESS_FUNCTION_TO_OVERRIDE') {
      unset(
$theme_registry['TEMPLATE_TO_OVERRIDE']['preprocess functions'][$key]);
    }
  }
?>

By doing a foreach, you wipe out only the preprocess function you don't want to run and leave any others in the chain, such as your own.

Hey Michelle, Thanks for

Hey Michelle,

Thanks for bringing this up. I do have a question, sorry if this is common knowledge by now. For Drupal 6, can modules now have theme tpl files inside the module directory? From example one, the hook_theme_registry_alter() function seems to indicate that is the case.

If not, I would like to know how the hook_theme_registry_alter() function (in example 1) function knows the "module" function it is templating - if the theme template.php file is being called before the .module (and it's functions) file? I hope that makes sense...

Posted by Elvis McNeely (not verified) on Mon, 02/18/2008 - 03:49
by-reference parameters

I would think that altering the $theme_registry variable requires it to be passed by reference... so, in order to improve your demonstration of this extremely nice trick, you should maybe adapt the function signatures accordingly.

Thanks for the tip, I always wondered why I can't do any themeing from inside a module - now I can!

Posted by Jakob Petsovits (not verified) on Mon, 02/18/2008 - 06:27
Eh?

"I hope that makes sense"

Not to me, sorry. No clue what you're asking.

Michelle

Posted by Michelle on Mon, 02/18/2008 - 10:08
Reference

"I would think that altering the $theme_registry variable requires it to be passed by reference"

Evidentially it doesn't. I have no idea why, though. You'd think it would. I just copied from the devel themer module.

Michelle

Posted by Michelle on Mon, 02/18/2008 - 10:09
Passing by reference

Hi, Michelle, how are you?

function MODULENAME_theme_registry_alter($theme_registry) {

To modify the $theme_registry variable you must pass it by reference, like so:

function MODULENAME_theme_registry_alter(&$theme_registry) {

I will report back to tell you if this works. Looks like it should :-)

(Not that I will ask for support on this....)

ps: I love that text editor you are using, I am using the same on my site... I thought I was the only one to use this module... it's great, ain't it? You can also add your own buttons to it. Like what you did with the strike.

Posted by Caroline Schnapp (not verified) on Wed, 08/20/2008 - 12:41
api.drupal.org

Checkout the documentation [1], that says:
http://api.drupal.org/api/function/hook_theme_registry_alter/6

[1] http://api.drupal.org/api/function/hook_theme_registry_alter/6

Posted by Visitor (not verified) on Wed, 08/20/2008 - 13:00
So there are 2 problems here

$theme_registry is not passed by reference, so any changes made to that variable will be lost.

$template has not been set, it needed to be set to 'page' in my case.

And when I correct these, it works!

Here is the corrected code (replace capitals with what is relevant in your situation):

<?php
function MODULE_NAME_theme_registry_alter(&$theme_registry) {
 
$template = 'THE_THEME_HOOK_HERE'; // example: page
  // dsm ($theme_registry);
 
$originalpath = array_shift($theme_registry[$template]['theme paths']);
 
// Get the path to this module
 
$modulepath = drupal_get_path('module', 'MODULE_NAME');
 
// Stick the original path with the module path back on top
 
array_unshift($theme_registry[$template]['theme paths'], $originalpath, $modulepath);
}
?>

Caroline

Posted by Caroline Schnapp (not verified) on Wed, 08/20/2008 - 13:08
I want to clarify something...

The $template here is really the theme hook, and not the actual name of the template you want to use.

For example, the template file name I am using in my module is page-webform.tpl.php, to theme the page hook (in theme parlance) used to display a webform on its dedicated page....

The other function I am using to tell the theme system to use page-webform.tpl.php is this one:

<?php
function MODULE_NAME_preprocess_page(&$variables) {
 
// If this is a node page (not a list page) and
  // the node is shown in 'view' mode rather than 'edit'.
 
if (isset($variables['node']) && (arg(2) === NULL)) {
   
// If the content type of that one node is 'webform'.
   
if ($variables['node']->type == 'webform') {
     
// drupal_set_message('Hello, we are showing a webform on its dedicated page');
     
$variables['template_file'] = 'page-webform';
    }
  }
}
?>

Posted by Caroline Schnapp (not verified) on Wed, 08/20/2008 - 13:16
Reference

It doesn't need to be passed by reference. See http://drupal.org/node/269578#comment-879871 and the comments that follow it.

It's been a long time since I wrote this and even my own code gets fuzzy to me after a while... The $template part may be a goof from genericizing this out of advforum. Here's the code it came from:

<?php
   
// Convince the registry that advforum in the module directory is a theme
    // so our templates are found.
   
$templates = array('node', 'comment', 'forums', 'forum_list', 'forum_topic_list', 'forum_icon', 'forum_submitted');
    foreach (
$templates as $template) {
     
$originalpath = array_shift($theme_registry[$template]['theme paths']);
     
$modulepath = advanced_forum_path_to_style();
     
array_unshift($theme_registry[$template]['theme paths'], $originalpath, $modulepath);
    }
?>

Hope that helps,

Michelle

Posted by Michelle on Wed, 08/20/2008 - 14:35
I see.

Maybe it would be a good idea to pass it by reference in your module as well as the Devel module, anyway. Why so? To follow the API signature. If you follow the link given above by... Visitor.

hook_theme_registry_alter(&$theme_registry) <--- Drupal API

It makes the code more consistent with the API documentation. No one will ever ask the question again, like did Morbus Iff.

In Drupal 7, this ugly 'hack' in drupal_alter() will be removed anyway: http://api.drupal.org/api/function/drupal_alter/6

Read this (oh my god my head is spinning...):

// PHP's func_get_args() always returns copies of params, not references, so
// drupal_alter() can only manipulate data that comes in via the required first
// param. For the edge case functions that must pass in an arbitrary number of
// alterable parameters (hook_form_alter() being the best example), an array of
// those params can be placed in the __drupal_alter_by_ref key of the $data
// array. This is somewhat ugly, but is an unavoidable consequence of a flexible
// drupal_alter() function, and the limitations of func_get_args().
// @todo: Remove this in Drupal 7.

Posted by Caroline Schnapp (not verified) on Wed, 08/20/2008 - 15:49
Did already...

Maybe it would be a good idea to pass it by reference in your module as well as the Devel module, anyway

I have no control over Devel and it's been changed in advforum for a long time.

Michelle

Posted by Michelle on Wed, 08/20/2008 - 18:17
good trick

good trick

Posted by Visitor (not verified) on Fri, 10/17/2008 - 12:26
Having trouble applying this

Having trouble applying this hook to a Views view:

function MODULE_NAME_theme_registry_alter(&$theme_registry) {
  $template = 'THE_THEME_HOOK_HERE'; // example: page
  $originalpath = array_shift($theme_registry[$template]['theme paths']);
  $modulepath = drupal_get_path('module', 'MODULE_NAME');
  array_unshift($theme_registry[$template]['theme paths'], $originalpath, $modulepath);
}

Tried $template='views' and $template='views-view' but they're not being picked up from my MODULE_NAME (as it were) directory. Is it different for views? Is this the wrong 'template' variable to set?

Posted by eon (not verified) on Thu, 03/26/2009 - 15:30
Sorry, I haven't the

Sorry, I haven't the foggiest.

Michelle

Posted by Michelle on Thu, 03/26/2009 - 19:22
>> $originalpath =

>> $originalpath = array_shift($theme_registry[$template]['theme paths']);

$template is unset - as michelle said its a legacy of the code she based it on (the foreach statement)

you probably want...

$originalpath = array_shift($theme_registry['view']['theme paths']);

inspect the $theme_registry variable for clues.

Posted by Simon Bettison (not verified) on Fri, 05/01/2009 - 09:09