Lesson #6 Class Notes wiki -- Theming and the Themer Pack

Events happening in the community are now at Drupal community events on www.drupal.org.
You are viewing a wiki page. You are welcome to join the group and then edit it. Be bold!

The first place to go for anything is the Drupal Handbooks section. Theming is no exception:

The Drupal Handbook: Customization and Theming

Go to the Customization and Theming section of the handbooks for the best basic overview of Drupal theming, and to obtain a great deal of helpful information.

The concept of theming in Drupal very much follows the MVC design pattern. See Wikipedia article on Model-View-Controller pattern. Basically it says that the way stuff looks doesn't need to be connected to what it does. So you can take some functionality that you want to use, and you can make it look the way you want it to look. Drupal does a pretty good job of this in terms of following through in separating the display logic from data processing and business logic, so to speak.

PHPTemplate Theme Engine has been the standard template engine for a while now, created by Adrian, who revolutionized the whole Drupal theming concept. All the great themes are now done with this engine. The Drupal templating system in Drupal core is very flexible, and you could come up with your own templating engine if you wanted to.

PHPTemplate

If you go to http://example.com/themes in your Drupal site, you can see a directory for all the themes that come packaged with Drupal, and there is also a folder called "engines". By default, in the engines directory there is only one subdirectory, phptemplate. There could be others, but in the default Drupal distribution, only phptemplate is standard. If we change directory to engines, we can see all the files comprising the engine, that can explain quite a bit about what is going on. In here is the phptemplate.engine file, which is the workhorse of phptemplate. Take a look at the file in a text editor. It contains a series of functions which you can override. Such as phptemplate_regions(), which determines where you can put blocks.

One of the core elements of phptemplate engine is the _phptemplate_callback function. It listens for theme functions to come through and it figures out whether or not it should route that function call to a file (a .tpl.php file) or whether it should be handled by a function inside a module or inside your template.php file. We'll use that to create function calls and route them to tpl.php files. It can be a lot easier to maintain a series of template files rather than a module full of theme functions. Especially if you are more comfortable from a web design point of view rather than from a programming standpoint.

The file also contains features which can give your theme special configuration options:

/**
* @return
* Array of template features
*/
function phptemplate_features() {
return array(
'toggle_logo',
'toggle_comment_user_picture',
'toggle_favicon',
'toggle_mission',
'toggle_name',
'toggle_node_user_picture',
'toggle_search',
'toggle_slogan'
);
}

The function phptemplate_page basically prepares everything for the individual theme's page.tpl.php file. The variables invoked in the latter file, for example, are also defined in the phptemplat.engine file.

  $variables = array(
'base_path' => base_path(),
'breadcrumb' => theme('breadcrumb', drupal_get_breadcrumb()),
'closure' => theme('closure'),
'content' => $content,
'feed_icons' => drupal_get_feeds(),
'footer_message' => filter_xss_admin(variable_get('site_footer', FALSE)) . "\n" . theme('blocks', 'footer'),
'head' => drupal_get_html_head(),
'head_title' => implode(' | ', $head_title),
'help' => theme('help'),
'language' => $GLOBALS['locale'],
'layout' => $layout,
'logo' => theme_get_setting('logo'),
'messages' => theme('status_messages'),
'mission' => isset($mission) ? $mission : '',
'primary_links' => menu_primary_links(),
'search_box' => (theme_get_setting('toggle_search') ? drupal_get_form('search_theme_form') : ''),
'secondary_links' => menu_secondary_links(),
'sidebar_left' => $sidebar_left,
'sidebar_right' => $sidebar_right,
'site_name' => (theme_get_setting('toggle_name') ? variable_get('site_name', 'Drupal') : ''),
'site_slogan' => (theme_get_setting('toggle_slogan') ? variable_get('site_slogan', '') : ''),
'css' => drupal_add_css(),
'styles' => drupal_get_css(),
'scripts' => drupal_get_js(),
'tabs' => theme('menu_local_tasks'),
'title' => drupal_get_title()
);

There are similar functions for node, for comment, for block, and for boxes.

Question: What is a box?

Answer: The following is the phptemplate engine 151 bytes long box.tpl.php file in its entirety:

<div class="box">

<?php if ($title): ?>
<h2><?php print $title ?></h2>
<?php endif; ?>

<div class="content"><?php print $content ?></div>
</div>

Overriding phptemplate.engine functions

One of the things you can do if you have a complex theme with a difficult set of requirements you can actually create your own custom _page() function. Now to do so, the aim is to keep the phptemplate.engine file in clean condition, so as to follow the MVC design pattern of separating the data of the content from its display. The problem arises when you are not receiving the data that you need in your display, so the business logic must be modified.

The correct way to do this is to copy this whole function and make your own override in your own theme's template.php file and no longer in the engine area, adding additional variables, for example in the variables array.

You can also do the same thing with the phptemplate_node function. Let's suppose we wish to override the taxonomy handling. The default behavior of phptemplate is just to send through all the $taxonomy links along with the node. So to make a custom version, you would copy the entire phptemplate_node() function and you would go into your theme directory and include the function in your template.php file.

template.php is in a way a general file within your theme directory which sets up your overrides, defines custom variables, does your regions, etc., and is where your custom theme functions go. So we simply add it to the file, simply changing the function name (which is already declared of course in phptemplate.engine) from phptemplate_node() to garland_node() for example (according to the name of your theme).

How PHPTemplate works in a nutshell

Someone somewhere calls a theme function, such as theme_page(). That goes through to phptemplate.engine, which sets up the variables, and those variables are then passed to page.tpl.php (which will do something like "print $header).

There is no diagram apparently showing this [tbd].

Making it do something

We go into PHPTemplate Theme Snippets in the Handbook. And choose the section Customising the user profile layout.

The first step is to create a tpl.php file for the user profile. The following code is added to phptemplate.php:

function phptemplate_user_profile($account, $fields = array()) {
// Pass to phptemplate, including translating the parameters to an associative array. The element names are the names that the variables
// will be assigned within your template.
/* potential need for other code to extract field info */
return _phptemplate_callback('user_profile', array('account' => $account, 'fields' => $fields));
}

What this does is set things up so we can have our own user_profile.tpl.php file by picking up the theme_user_profile() function which is built into the user module. See http://api.drupal.org/api/5/function/theme_user_profile.

So in the above code, the _phptemplate_callback function's first parameter gives rise, when it goes through phptemplate.engine, to user_profile.tpl.php being found in the current theme directory, and the function _user_profile within the file is passed the second parameter.

This can be done with any theme_ function you wish to override.

So to continue, we could, for example, essentially copy the contents of theme_user_profile() into the new user_profile.tpl.php file and add a bit of custom stuff:

<h1>hola</h1> 
<?php
$output = '<div class="profile">';
$output .= '<h1>Hello dojo!</h1>';
$output .= theme('user_picture', $account);
foreach ($fields as $category => $items) {
if (strlen($category) > 0) {
$output .= '<h2 class="title">'. $category .'</h2>';
}
$output .= '<dl>';
foreach ($items as $item) {
if (isset($item['title'])) {
$output .= '<dt class="'. $item['class'] .'">'. $item['title'] .'</dt>';
}
$output .= '<dd class="'. $item['class'] .'">'. $item['value'] .'</dd>';
}
$output .= '</dl>';
}
$output .= '</div>';

print $output;
?>

Being careful to change the return in the last line to print. It's kind of a silly example, but the whole point is to get out of php-land and get into template land. So we try to separate the php stuff from the html stuff to make it easier to read, so the first part looks like this:

  <div class="profile">
<h1>hola, chicos!</h1>
<p>This is normal html.</p>

<?php print theme('user_picture', $account); ?>

<?php foreach ($fields as $category => $items) {
...

Now in the above loop, it is very hard to separate the logic (the controller's model and view generation) from the html (the view).

In any case, this is an example of how a theme function can be overriden, and now we are editing an html-like theme file with interspersed but as cleanly separated as possible php, which is much clearer for designers. The basic idea in a team situation would be for designers to work on the tpl.php file, while programmers worked on the template.php file, for example.

Of course, MVC is a worthy objective, but sometimes we need to get things done, so we can do things like the following (copied from working code):

  <div class="profile">
<h1>hola, chicos!</h1>
<p>This is normal html.</p>

<?php
$result = db_query('SELECT title FROM {node} WHERE uid = %d', $account->uid);
while ($node = db_fetch_object($result)) {
print '<h3>' . $node->title . '</h3>';
}
?>
...

(Josh's working code from lesson can be found in section 1 of the attached pastebin.txt, see below.)

Even if it does break MVC, which is a great model, it is something to strive for, but you shouldn't let it stand in the way of getting something done that's useful. However, the db query could be moved to the template.php file and the result could be assigned to an array, and then in user_profile.tpl.php, this could simply be presented via a loop (or implode statement, but foreach is less complicated for a designer to read clearly) which would insert the desired markup. This would comply with separation of logic and presentation. It is a trade off, sometimes.

The code reflecting this discussion (and also removing the History info out of the $fields rendering) can be found in section 2 of the attached pastebin.txt

(merlinofchaos' version which shows adherance to MVC via the "Themer Pack way", can be found (with small typos corrected and working) in section 3 of the attached pastebin.txt).

Overheard on IRC

(16:22:54) merlinofchaos: Warning: Box is an holdover from older Drupal, so it's kind of badly used =)

...
(16:28:00) sepeck: also note that it's best to have your custom themes in sites/all/themes. I would suggest starting with something simplier then garland (like blue marine) Copy and rename the theme you want to exploit

Question: Is there a naming convention to distinguish between logic and presentation template files?

Answer: No. But template.php is the place for logic.

Question: Is there an easy way to see what fields are available in the $field parameter in user_profile.tpl.php?

Answer: print_r($fields)!

Question: I wish to override a theming function. I copy it into my theme's template.php, and call it MYTHEME.function-name; it is invoked, but what turns off the code I am overriding?

Answer: The logic is in function theme() itself. It 'looks' for the function in order of precedence: THEMENAME_*, ENGINENAME_* and finally theme_*. See http://api.drupal.org/api/5/function/theme_get_function.

It was pointed out that $my_nodes could have been rendered as an unordered list using:

<?php print theme('item_list', $my_nodes); ?>

The Themer Pack project

merlinofchaos' Themer Pack will be great in the context of the discussion of separating logic and markup and the overriding of theme elements, since it will break out all the themeable functions and variables from the modules and core, breaking down a complex maze into obvious, workable chunks.

The Themer Pack project is basically an attempt to go through all the theme functions in Drupal and create tpl.php files for them. In the context of what we have been doing in this lesson, it will break down a complex scenario into workable chunks.

One might think that a regular expression based script could do this, but there are situations such as the html generating loop in the theme_user_profile function, which are much trickier.

So there is a whole group working on this, the Themer Pack working group: http://groups.drupal.org/themer-pack-working-group.

"angrydonuts.com is merlinofchaos' blog. You should read it
because he often puts stuff in there that will save your life."

josh_k

There are some tricky questions, for example theme_table() is great for developers who want to output their data in tabular fashion, but... this is your theme function for a designer... What? This is evil. It is completely incomprehensible for a designer. So this has to be split up into a series of tpl.php files, so you have a tpl.php file for the header, the footer, a tpl.php file for each individual row. That is the type of breakout that will allow a designer to come along and say "My table uses groups of three rows, so I am going to theme three rows at a time and put in my attributes and make it work exactly the way I want it to work."

There are a number of other places where you have these types of problems. For example theme_item_list().

The tpl.php files will not be bunched together in the theme directory, but rather distributed in subdirectories (as specified in the template.php file by the first parameter of the _phptemplate_callback() function). as can be seen in the Work in Progress thread at the working group ( http://groups.drupal.org/node/2526 ):

aggregator/
block/
book/
color/
comment/
drupal/
filter/
forum/
locale/
menu/
node/
poll/
profile/
search/
system/
taxonomy/
upload/
user/
watchdog/

So the idea is that you will be able to drop in the tpl.php files you want to use.

The harder question is, how do we actually make it better for people?

There is some overhead involved with keeping track of all the files involved, and Drupal will be loading up a bunch of theme functions that it is not going to use. Once the core and the modules were refactored, it will provide a significant boost, and will lighten the load a great deal. For example, in the user module, the function theme_user_profile is being loaded even though it is not going to be used. The improvement will be that the funcions now will only be loaded as need on the fly.

Along these lines someone [please specify] did a script to strip functions from modules and place them into .inc files, so they would only be loaded as needed. Performance improved since the memory usage dropped markedly, although apache had to load a series of files conditionally.

Question: How do you know which functions are overriden?

Answer: You can just look in the template.php file, which is where all the overriden function are, with the exception of files like node.tpl.php. And we have already seen in earlier Drupal Dojo lessons that node-dojo.tpl.php and node-battle.tpl.php also work, thanks to the intelligence of phptemplate. So anything that isn't in the template.php file or is not being overriden by the presence of a file in the theme directory can be taken from theme functions and overriden.

merlinofchaos' refactoring of the lesson's code

(checked, working version available in section 3 of the pastebin.txt file)

Clean separation of logic and markup, thoroughly readable for non-programmer designers!

The first part, pertaining to template.php, cleanly separates out all the logic and database access. A set of variables is now generated as well as the callbacks registering the tpl.php files.

Four basic variables are created inside the array $vars (account, fields, picture, categories), some processing is done on them, and the model and view is generated and sent to the tpl.php files via separate _phpcallback_callback calls for user_profile_item, user_profile_category and user_profile, which in each case send the $vars array as the second parameter.

We also make use of a tpls (templates) subdirectory.

Going through the template.php section in more detail:

  • We are picking up the separate user account, fields, picture and categories elements. and placing them into the $vars array.
  • This $vars array will then be passed as a single model variable (second parameter of the _phptemplate_callback() function).
  • The processing creates the model, for example, $category is placed into $vars['category'].
  • The foo.tpl.php files (views) will be specified in the first parameter of the _phptemplate_callback() calls.
  • The foo.tpl.php files then invoke the variables passed to them in a clean, highly designer intuitive manner. Compare to the theme_items_list() function, for example.

So it would be a great contribution to apply the same method to various the directories of funtions the Themer Pack working group has specified and help out there. Once that work is done, theming will be much easier because some of the heavy lifting will already have been accomplished.

Question: What are the "categories" here in this code?

Answer: User account categories (history, blog...). If I enable blog module, it will show up too. They originate in hook_user (see http://api.drupal.org/api/5/function/hook_user ), a core Drupal hook for user accounts. Enabling the profile will allow for additional categories to be created, which can include a series of administer defined fields (Administer > User Management > Profiles). If we add a new textfield and specify category "Personal" and Title "Nickname" and Form (field) name "profile_nick", specifying a visibility of "Public field, content shown on profile page and on member list pages.", with a weight of -5 to place it at the top of the profile section; and then go to My Account, we will see when we edit that we have a Personal tab, and fields to enter the nickname. Which then appears in the My Account view page as... a category!

So you may either use History, which comes from user.module, or else one of its elements, and then use the profile module to create additional categories and fields, and theme them accordingly.

What we have essentially done here (in section 3 of pastebin.txt) is to replicate the existing user profile code in the Themer Pack way.

Customizing

One could also unset the $fields variable sent by phptemplate.engine and create a customized model of data to be then passed to the tpl.php files to be themed.

We create the user_profile_custom.tpl.php file, which simply starts out by printing the $fields info:

<?php
print_r($fields);

And invoke it from a callback from template.php, passing also the $fields variable:

function phptemplate_user_profile($account, $fields = array()) {
return _phptemplate_callback('tpls/user_profile_custom', array('fields' => $fields));
}

giving:

Array
(
[Personal] => Array
(
[profile_nick] => Array
(
[title] => Nickname
[value] => inglés
[class] => profile-profile_nick
)

)

[History] => Array
(
[history] => Array
(
[title] => Member for
[value] => 8 weeks 1 day
[class] => user-member
)

)
)

A very creative way to see what you have to play with, both on the model (data) and view (tpl invocation via _callback_phptemplate) generation side (template.php) and on the view side itself (foo.tpl.php).

Question: Why not include the trailing ?> at the end of the files we are dealing with that contain php?

Answer: a) it's not necessary (php assumes the close at the end of a file); and b) if you have whitespace below that symbol it can interfere with the output of the headers and put a site offline, since a whitespace line has been output before the system is ready to start processing.

(See this issue , for example).

Now, we can do this to user_profile_custom.tpl.php:

<code> <?php
print_r($fields);
?> </code><hr />
<div style="border: 1px solid blue; padding: 5px; float: right;">
<h2>Nickname</h2>
<h4><?php echo $fields['Personal']['profile_nick']['value']; ?></h4>
</div>

yielding:

Nickname

inglés

Now, when you have a lot of these fields to theme the echo $fields statements and their attributes can get pretty tedious. So one possibility is to do some preprocessing of the model (data) in template.php (tested code):

function phptemplate_user_profile($account, $fields = array()) {
$vars = array(
'account' => $account,
'fields' => $fields,
'picture' => theme_user_picture($account), // WRONG!
'picture' => theme('user_picture', $account), // RIGHT!
'categories' => '',
);
foreach ($fields as $category => $items) {
foreach ($items as $key => $values) {
$vars[$key] = $values['value'];
}
}
// And put it all in the final wrapper.
return _phptemplate_callback('tpls/user_profile_custom', $vars);
}

with user_profile_custom.tpl.php now reading (tested code):

<?php
print_r($fields);
?>
<hr />

<div style="float: right; margn: 10px; padding: 5px; border: 1px solid blue;">
<h2>Nickname</h2>
<h4><?php echo $profile_nick ?></h4>
</div>

<div style="float: left; margn: 10px; padding: 5px; border: 1px solid blue;">
<h2>Nickname</h2>
<h4><?php echo $history ?></h4>
</div>

And that works!

Question: Is there a list of overrideable themeable functions?

Answer: Yes, see http://api.drupal.org/api/5/group/themeable,  "Functions that display HTML, and which can be customized by themes." Anything that begins with theme_ is overrideable, providing that the form theme('function name', arguments) is used, as above. Additional functions can be found by grep'ing inside contrib modules for theme_foo() functions.

So:

theme_user_picture($account); //WRONG! will interfere with overriding process and will break your site!
theme('user_picture', $account); //RIGHT! you are calling the core theme function with params.

 

AttachmentSize
pastebin.txt9.02 KB