UPDATED: Can't Add CSS, JS, RSS Icon Or Set Title Or Messages In Preprocess Page?
on
UPDATED: Can't Add CSS, JS, RSS Icon Or Set Title Or Messages In Preprocess Page?
Often you want to add CSS files, scripts, feed icons or even set Drupal's page title from the theme layer. The most obvious place to call Drupal's functions for these tasks is probably in a page preprocess function. However calling the following Drupal API functions from a theme or module's NAME_preprocess_page() function often doesn't work;
drupal_add_css()drupal_add_js()drupal_add_feed()drupal_add_link()drupal_set_html_head()drupal_set_title()drupal_set_message()(or devel.module'sdpm()).
The reason they often don't work when called from NAME_preprocess_page() (aka HOOK_preprocess_page(), THEMENAME_preprocess_page() and – in the examples in this article – custom_preprocess_page()) is because any custom preprocess function that is defined in the theme layer (or even in modules in most circumstances) gets called after Drupal core's preprocess functions. By the time your custom_preprocess_page() gets called, Drupal core's template_preprocess_page() has already rendered the page.tpl.php variables $styles, $scripts, $head, $head_title, $title and $messages into HTML strings.
(This is not a bug in Drupal, but is due to the nature of overriding which is a fundamental principle on which Drupal's theme layer is designed.)
You can access the rendered HTML string from custom_preprocess_page() with $vars['styles'], $vars['scripts'], $vars['head'] etc., however adding new stylesheets, JS scripts, elements, Drupal messages or setting the title would require either;
- Instead of setting the new item, append it's rendered HTML string representation to the respective
$varsvariable- This often needs quite a bit of amount of difficult-to-maintain code.
- Setting the new item then re-rendering the HTML string contents of the respective
$varsvariable- This requires quite in depth understanding of the get/set function pair and/or research on api.drupal.org, and impacts performance by duplicating processing. It's also beyond the range of programming and PHP skills of many themers.
There are 3 workaround solutions that I know of for this problem.
- These functions can be successfully called from
hook_init()in a custom module. The disadvantages of this solution are that you need to do it in a module (themes can not implementhook_init()), and that there is a performance hit, since the functions execute even on pages that are not requesting a themed HTML page, such as the first request for an imagecache image that hasn't yet been cached, asynchronous requests for JSON, HTML or other ajaxy data, and site services such as calls toxmlrpc.php, RSS feeds and other XML requests.drupal_set_message()will also not work if called fromhook_init()sincehook_init()is not called for pages that are cached for anonymous users, and pages with messages are not cached (IIRC). - These functions can also be called from your theme's
template.phpin the global scope. Doing things in the global scope is a poor practice that should be avoided if possible, so this isn't ideal. However this solution does allow a theme to add these items. It doesn't solve the performance hit problems though; In fact it probably makes them slightly worse. - While the performance hit is not significant in most cases, a more robust solution is sometimes required. This can be fairly readily achieved with
hook_theme_registry_alter(). To do this, add the following code to a custom module (or theme) and rename the function prefixes fromcustom_to the machine name of your module or theme. It should also work with these by putting the code intotemplate.phpthough I haven't actually tested that.
<?php
/**
* Implementation of hook_theme_registry_alter().
*/
function custom_theme_registry_alter(&$theme_registry) {
// Makes sure the early preprocess page function gets called first.
array_unshift($theme_registry['page']['preprocess functions'], 'custom_early_preprocess_page');
// And the late preprocess page function is called last. You probably won't need this.
$theme_registry['page']['preprocess functions'][] = 'custom_late_preprocess_page';
}
/**
* These are slightly different to most hook_preprocess_page() implementations;
* 1. They have a slightly different name to avoid auto-detection by the theme registry.
* 2. They are manually added to the theme registry by custom_theme_registry_alter().
*/
/**
* Implementation of hook_preprocess_THEMEHOOK().
*
* The very FIRST preprocess page function to be called.
* This allows it to add CSS, scripts etc. late in page rendering process, but not too late to miss being added to the page completely.
*/
function custom_early_preprocess_page(&$vars, $hook) {
// These WILL have the desired effect here.
drupal_add_css();
drupal_add_js();
drupal_add_feed();
drupal_html_header();
drupal_set_message();
}
/**
* Implementation of hook_preprocess_THEMEHOOK().
*
* The very LAST preprocess page function to be called.
* If necessary, HTML strings can be appended to rendered HTML strings here. You probably won't need this.
*/
function target_late_preprocess_page(&$vars, $hook) {
// These will NOT have the desired effect here.
drupal_add_css();
drupal_add_js();
drupal_add_feed();
drupal_html_header();
// drupal_set_message() stores the data in the user's SESSION. So the message will not show on THIS page, but it WILL show on the next page the same user requests from Drupal.
drupal_set_message();
}
?>I found this solution does allow a theme to add these items. It doesn't solve the performance hit problems though; In fact it probably makes them slightly worse.
If I'm understanding your post correctly, I think you've overstated point 1: "This often needs quite a bit of amount of difficult-to-maintain code."
In many cases, (JS, CSS, and links, for example), you only need to look at the original code in template_preprocess_page (http://api.drupal.org/api/function/template_preprocess_page/6) and refresh the variable value in the preprocess function using the same code.
For example, for adding a CSS file:
drupal_add_css(path_to_theme . '/file.css', 'theme');
$variables['css'] = drupal_add_css();
$variables['styles'] = drupal_get_css();
I'd hate for themers to get discouraged about needing a module for something easily handled in the theme.
And this isn't to say I didn't like your post! I've never played with the registry_alter hook and definitely appreciate learning more about it with a practical example. Thanks for taking the time to write about it.
Be careful when modifying or adding additional ouput in the theme layer (output that's not style or markup). Care should be taken to cleanse user-submitted data if it has not been already.
or use the following code in your preprocess_page
Another problem with sticking things in the global scope is that they can be executed multiple times per page rendering. Which isn't terrible if you're adding a CSS file, but could be problematic if you're using drupal_set_message().
Thanks a lot, guys!Perfect article!!!