hook_theme_registry_alter for advanced template control
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.
Comments
15 comments postedHey 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...
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!
"I hope that makes sense"
Not to me, sorry. No clue what you're asking.
Michelle
"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
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.
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
$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):
<?phpfunction 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
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:
<?phpfunction 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';
}
}
}
?>
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
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.
I have no control over Devel and it's been changed in advforum for a long time.
Michelle
good trick
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?
Sorry, I haven't the foggiest.
Michelle
>> $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.