Theming Modules in Drupal 6

Our Target Module: What We Want

Before we begin developing a module, here's a brief overview of what we want to accomplish.

The module we will write in this article is the Philosophy Quotes module (philquotes will be our machine-readable name). The goal of this module will be to create a block that displays pithy philosophical quotes.

We will implement the following features:

  • Quotes should be stored along with other basic content, making it possible to add, modify, and delete this content in exactly the same way that we create other articles.
  • Since our existing themes aren't aware of this quotes module, it must provide some default styling.

We will progress through the creation of this module by first generating a new "quote" content type, and then building a theme-aware module.

Creating a Custom Content Type

As Drupal evolved, it incorporated an increasingly sophisticated method for defining content. Central to this system is the idea of the content type. A content type is a definition, stored in Drupal's database, of how a particular class of content should be displayed and what functionality it ought to support.

Out of the box, Drupal has two defined content types: Page and Story. Pages are intended to contain content that is static, like an "About Us" or "Contact Us" page. Stories, on the other hand, are intended to contain more transient content—news items, blog postings, and so on.

Creating new pages or stories is as simple as clicking on the Create Content link in the default menu.

Theming Modules in Drupal 6

Obviously, not all content will be classified as either a page or a story, and many sites will need specialized content types to adequately represent a specific class of content. Descriptions of events, products, component descriptions, and so on might all be better accomplished with specialized content types.

Our module is going to display brief quotes. These quotes shouldn't be treated like either articles or pages. For example, we wouldn't want a new quote to be displayed along with site news in the center column of our front page.

Thus, our quotes module needs a custom content type. This content type will be very simple. It will have two parts: the text of the quote and the origin of the quote.

For example, here's a famous quote:

The life of man [is] solitary, poor, nasty, brutish, and short.—Thomas Hobbes.

The text of this quote is "The life of man [is] solitary, poor, nasty, brutish, and short", and the origin in this example is Thomas Hobbes. We could have been more specific and included the title of the work (Leviathan) or even the exact page reference, edition, and so on. But all this information, in our simple example, would be treated as the quote's origin.

Given the simplicity of our content type, we can simply use the built-in Drupal content type tool to create the new type.

To generate even more sophisticated content types, we could install the CCK (Content Creation Kit) module, and perhaps some of the CCK extension modules. CCK provides a robust set of tools for defining custom fields, data types, and features.

But here our needs are simple, so we won't need any additional modules or even any custom code to create this new content type.

Using the Administration Interface to Create a Content Type

The process of creating our custom content type is as simple as logging into Drupal and filling out a form.

The content type tool is in Administer | Content management | Content types. There are a couple of tabs at the top of the page:

Theming Modules in Drupal 6

Clicking the Add content type tab will load the form used to create our new content type.

Theming Modules in Drupal 6

On this form, we need to complete the Name and Type fields—the first with a human-friendly name, and the second with a computer-readable name. Description is often helpful.

In addition to these fields, there are a few other form fields under the Submission form settings and Workflow settings that we need to change.

Theming Modules in Drupal 6

In the Submission form settings section, we will change the labels to match the terminology we have been using. Instead of Title and Body, our sections will be Origin and Text.

Changing labels is a superficial change. While it changes the text that is displayed to users, the underlying data model will still refer to these fields as title and body. We will see this later in the article.

In the Workflow settings section, we need to make sure that only Published is checked. By default, Promoted to front page is selected. That should be disabled unless you want new quotes to show up as content in the main section of the front page.

Once the form is complete, pressing the Save content type button will create the new content type.

That's all there is to it. The Create content menu should now have the option of creating a new quote:

Theming Modules in Drupal 6

As we continue, we will create a module that displays content of type quote in a block.

Before moving on, we want a few pieces of content. Otherwise, our module would have no data to display.

Here's the list of quotes (as displayed on Administer | Content management | Content) that will constitute our pool of quotations for our module.

Theming Modules in Drupal 6


Content and Nodes

How does Drupal treat content for this custom content type and where is the content that we create stored?

Drupal treats such content—even for custom content types—as nodes. A node is a generic data type that represents a piece of content. Drupal assigns each node a Node ID (NID), which is unique within the Drupal installation. Along with the basic information that the node comprises (like title and body), Drupal also tracks information on the status of the node, modifications of the node, related comments, and so on.

Nodes are stored inside the Drupal database. In fact, there are several database tables devoted to maintaining nodes. Later on, we will interact with the database to retrieve our quotes. However, the module we create in this article will only make scant direct use of the database layer.

Now that we have a custom content type and a few new content items, we are ready to proceed to module development.

The Foundations of the Module

We will begin this module by creating .info and .module files.

Our module will be named philquotes, and (as would be expected) will be located in drupal/sites/all/modules/philquotes.

Inside that directory, we will first create a standard module .info file:

; $Id$
name = "Philosophy Quotes"
description = "Dynamic display of philosophy quotes."
core = 6.x
php = 5.1

Next, we will start our new philquotes.module file:

// $Id$
* @file
* Module for dynamic display of pithy philosophy quotes.
* Implementation of hook_help()
function philquotes_help($path, $arg) {
if ($path == 'admin/help#philquotes') {
$txt = 'This module displays philosophical quotes in blocks. '.
'It assumes the existence of a content type named "quote".';
return '<p>'. t($txt) .'</p>';

The above code implements hook_help() to provide some information about the module.

Our philquotes module is mainly intended to provide block content.

Next, we will implement hook_block()—the hook that controls what content is displayed in a block.

Here's our block hook:

* Implementation of hook_block().
function philquotes_block($op = 'list', $delta = 0, $edit = array()) {
switch ($op) {
case 'list':
$blocks[0]['info'] = t('Philosophical Quotes');
return $blocks;
case 'view':
$item = _philquotes_get_quote();
if(!empty($item)) {
$content = theme('philquotes_quote',
$blocks['subject'] = t('Pithy Quote');
$blocks['content'] = $content;
return $blocks;

For the most part, this code should also look familiar. In the case where the operation ($op) passed into the hook is list, this should simply return information about the blocks this hook makes available. In our case, there is just one block.

The more important case for us, though, is the highlighted section above. In the case where view is passed in as the operation, the module should be returning some content destined for display to the user. This content is wrapped in the $blocks variable.

As far as this hook_block() implementation is concerned, we will focus on the highlighted portion above.

When a block is generated for viewing, every block item contains two pieces: a subject and some content. The subject of our block will always be Pithy Quote, but the content is generated by a two-step process:

  • Getting a quote from the database
  • Adding any necessary theming information to that quote

In the above code, this is done by the following two statements:

$item = _philquotes_get_quote();
if(!empty($item) {
$content = theme('philquotes_quote',
// ...

We use !empty($item) to make sure that the returned item is not NULL, which would indicate that there were no quotes available. When $item is NULL, we simply avoid sending a return value, and the block does not show up at all.

The first statement performs the database lookup, and the second handles theming. Let's take a detailed look at each.

A Simple Database Lookup

The philquotes_block() hook calls the _philquotes_get_quote()function to get content for display. The _philquotes_get_quote() function is considered a private (module only) function, since it begins with the underscore character.

The task of this private function will be to return a single quote content item from the database. Quotes are stored as nodes. To add some spice to our module, we'll get a random quote node rather than progressing sequentially through the quotes.

The process of retrieving our content item can be broken down into two steps:

  1. We need to get the node ID of the random quote we are going to display.
  2. We need to retrieve the node's data, specifically the title (which contains the quote's origin) and the body (which holds the text of the quote).

To accomplish these tasks, we will use the Drupal API on two levels. To get a random node ID, we will have to write some simple (but low-level) SQL. Once we have that, though, we can use a higher-level function to get the node's content.

Getting the Node ID

The first step is to get the Node ID for a quote. To get this, we will interact with the database using Drupal's database API.

The database API provides low-level access to the database using SQL statements. Drupal will handle the details of connection management. It also provides some level of type checking, string escaping, and other protective features.

Drupal developers have provided a useful overview of the database API. It is available as part of the standard API docs:

The Drupal database has six tables devoted to node maintenance. Right now, we only need to use one directly: the node table. As the generic name implies, this is a high-level table that contains basic information about each node in Drupal.

The node ID is the only field we need returned from the node table. But we don't want just any node from the table to be returned. What constraints must be placed on the returned node?

  • We want the node to be published. By default, our quotes are published on creation, but it is possible that one was unpublished, and we don't want to display such a node. The status field in the node table indicates whether a node is published (1) or unpublished (0).
  • We want the node to have the content type quote. The type field in the node table contains the name of the node's content type.

Those are the only constraints we need to place. Of course, constraints can be a lot more complicated—we could limit it to only quotes newer than last week, or quotes that are owned by a particular user, and so on. Such restraints would be implemented in SQL.

Now we have all the information we need to construct our SQL. If we were to run the query from a MySQL monitor, it would look as follows:

SELECT nid FROM node
WHERE status=1 AND type='quote'

This will return one random node ID for a node that is published (status=1) and is of the right content type (type='quote').

To execute a similar query using Drupal's database API, we need to make a minor change.

Wrapping the SQL in PHP code, we create a function that looks as follows:

function _philquotes_get_quote() {
$sql = "SELECT nid FROM {node} ".
"WHERE status=1 AND type='quote' ORDER BY RAND() LIMIT 1";
$res = db_query($sql);
$item = db_fetch_object($res);
// Do something with the $item.

The $sql variable contains our quote, but in a slightly altered form. Instead of referring directly to the table name, we have substituted a table placeholder denoted by curly braces. Before executing the query, Drupal will substitute the correct name of the table where the {node} placeholder appears.

Table name placeholders provide administrators the ability to change table names to better fit existing conventions. For example, some ISPs require that all tables have certain pre-determined prefixes. By using table name placeholders, we can avoid the need to make changes to code when table names are changed in the database.

The Drupal db_query() function handles the execution of the query. It returns a resource handle ($res) that can be used to manipulate the results of the query.

The db_fetch_object() function provides access to the rows returned from the database.

Our query was limited to only one result. So rather than looping through the result set, we can simply fetch the first returned item: $item = db_fetch_object($res);.

Now we have an object, $item, that contains the $nid attribute. We can use this attribute to access the contents of the node.

Getting the Node's Content

Armed with the Node ID, we can retrieve a specific piece of content from the database.

To do this, we could write another (more complex) SQL statement to get the node contents. Since node content is spread across multiple tables, we would have to make use of a couple of joins to get the desired content. Fortunately, there is a simpler way.

The Drupal node API provides a convenient function that does the heavy lifting for us: node_load(). If we pass this function the NID, it will return an object that contains the node's contents.

Nodes are implemented using a Drupal module. The main node API (found in drupal/modules/node/node.module) provides dozens of methods for working with nodes.

Using node_load(), we can finish off our function as follows:

function _philquotes_get_quote() {
$sql = "SELECT nid FROM {node} ".
"WHERE status=1 AND type='quote' ORDER BY RAND() LIMIT 1";
$res = db_query($sql);
$item = db_fetch_object($res);
$quote = node_load($item->nid);
return $quote;

Only the highlighted lines are new. All we are doing here is fetching the complete node object with node_load(), and then returning this object. (If node_load() is given a NULL value, it simply returns FALSE. We are relying on the philquotes_block() function to deal with empty results.)

What does this do in the context of our module? Let's look back at the lines of philquotes_block() that we worked on earlier:

$item = _philquotes_get_quote();
if(!empty($item)) {
$content = theme('philquotes_quote',

The $item variable in philquotes_block() now contains the node object we just retrieved with _philquotes_get_quote() (the value of $quote). We have the data we need. Now we need to format it for display.

Next, we will look at the theme() function (and some related code), and how it handles turning our object into a themed string.

Theming Inside a Module

Once the philquotes_block() function has obtained the content of a quote, it must add some formatting and styling to the data. This is handled with functions that make use of the theme system. And the first stage is handled with the theme() function.

In the module, we want to provide some default theming, but in a way that makes use of the theme system. This provides more flexibility: theme developers can change the layout of our module without having to change any of our code.

Default Themes
Often, a module adds content that existing themes do not already provide layout information for. In such cases, the module developer should provide a default theme. A default theme should provide layout information for the new content that the module makes available.

The theme() function is called in philquotes_block() with three parameters:

$content = theme('philquotes_quote',

The first, philquotes_quote, tells the theme() function which theme hook should be executed. The theme() function will query the theme system to find an implementation of a function called theme_philquotes_quote() or a template called philquotes_quote.

Neither a matching function nor a matching theme exists in the default installation of Drupal. But in a moment, we will solve this problem by creating a default theme function that will be part of our module.

The next two parameters are the body and title of the quote we want to display. (Recall that $item is the object that contains the content of our random quote node.)

The theme() function itself does not do anything special with parameters after the first. Instead, they are passed on to the special theme hook (in this case, theme_philquotes_quote()).

Where will the theme() function, which now must find theme_philquotes_quote(), look for themes? One place it will look is inside the currently enabled theme. But there is no philquotes_quote function or template in that theme.

Drupal's theme system will also look among other registered themes. Modules can register their own theme functions, making these themes available to the theme system. If no theming is provided by the default theme, the module's theme will be used. Since our module is providing new content that needs some theming, we will need to register a theming function to provide default theming.


Registering a Theme

The philquotes module needs to register a theme function that takes a quote content item and formats it for display as an HTML block.

Essentially, when a theme is registered, it declares new theme hooks, which other parts of the theme system can use or override.

To register the module's default theme, we need to implement the theme registration hook (hook_theme()), following the naming convention _theme(), where is the name of the module:

* Implementation of hook_theme().
function philquotes_theme() {
return array(
'philquotes_quote' => array(
'arguments' => array('text' => NULL, 'origin' => NULL),

In a nutshell, this function provides the theme system with a comprehensive list of what theme functions (hooks) are provided by this module, and how each function should be called.

There are a lot of different configurations that we might want for a theme hook. Unfortunately, the result of this is that the data structure that hook_theme() must return is a daunting series of nested associative arrays.

In the above example, the arrays are nested three-deep. Let's start with the outermost array.

The outermost array contains elements of the form 'theme_function_name' => configuration_array. In the above example, it looks as follows:

return array(
'philquotes_quote' => array(
// Contents of the array...

The theme_function_name string should be the name of a theme hook that this module implements or provides. In our theme, for example, we will provide a theme for philquotes_quote.

The actual name of the theme hook function will be theme_philquotes_quote(), but the theme_ portion is omitted when we register the handler.

The value of this array element, configuration_array, is another associative array that provides Drupal with information about how this module handles the theme. We will look at this array in a moment.

The outer array may register more than one theme function. For example, we might register three different theme functions for a module as follows:

return array(
'mytheme_a' => array( /* settings */ );
'mytheme_b' => array( /* settings */ );
'mytheme_c' => array( /* settings */ );

This code would register three theme hooks, each with an array of configuration options.

We are registering only one theme hook, though, and we can now take a closer look at the second level of associative arrays.

This array will contain configuration information about how this module implements the theme hook. Let's look at the configuration options that our philquotes_quote theme hook will have:

'philquotes_quote' => array(
'arguments' => array( /* parameter info */ ),

The value for the philquotes_quote key in the outer array is itself an associative array. The keys that this array holds are well defined, and all the eight keys are explained in the hook_theme() API documentation (

For example, the template key can be used to point to a template file that should be used instead of a theme function. Had we chosen to use a template file called philquotes_quote.tpl.php, we could have called the above as follows:

'philquotes_quote' => array(
'template' => 'philquotes_quote',
'arguments' => array( /* parameter info */ ),

When theme_philquotes_quote() is called with these parameters, it would look for a file called philquotes_quote.tpl.php inside the philquotes module directory. (The template file extension is appended automatically.)

In this example, the items in the arguments array would be passed in as variables to the template.

Other similar directives exist for adding preprocessing functions, including other PHP files, and so on.

For our module, however, we will implement the hook as a function. The only directive we want in this array is arguments.

In the context of a hook function (as opposed to a template), this array entry is used to indicate what parameters will be passed to the function.

For our module hook, we only need two: text and origin.

* Implementation of hook_theme()
function philquotes_theme() {
return array(
'philquotes_quote' => array(
'arguments' => array('text' => NULL, 'origin' => NULL),

Items in the arguments array are of the form 'argument_name' => default_value.

Initially, we set both values to NULL; however, if we want to provide more robust default values, we could do so here.

Based on this array, we can now construct the method signature of our theme hook. It will be:

function theme_philquotes_quote($text, $origin)

The name of the function is the hook name with theme_ prepended, and the two parameters here should correspond to the elements in the arguments array.

Now we are ready to create this function.

Creating a Theme Hook Function

We have just created a theme registration function, overriding the hook_theme() function for our module. In it, we have declared that this module implements a hook called theme_philquotes_quote() that takes two arguments, $text and $origin. Now we will create that function.

The goal of this function is to take the same content (a single quote) and configure it for display. Here is our first version of this function, which will provide a basic display of the quote:

* Theme function for theming quotes.
* @param $text
* The quote content as a string.
* @param $origin
* The original source of the quote, as a string.
* @return
* An HTML themed string.
function theme_philquotes_quote($text, $origin) {
$output = '<div id="philquotes-text">'. t($text)
.'</div><div id="philquotes-origin">'. t($origin) .'</div>';
return $output;

All we have done in this function is wrap the content of the $text and $origin variables in <div/> tags, each of which has an id attribute.

Looking back at our philquotes_block() function, we can see what happens from here: The string that this function returns will be rendered as a block item. When it is rendered, it should look something like this:

The header, Pithy Quote, comes from philquote_block(), while the text and origin of the quote are rendered into two un-styled <div/> elements. We have taken our node content and formatted it as HTML.

The block might not show up for two reasons. First, if there are no published quotes, nothing will be displayed here. Second, the block cache may need to be cleared. The cache-clearing tool included with the Devel module can do this for you.

But this formatting of our quote is not particularly attractive. We can improve it by adding a CSS stylesheet to our module.l

Adding a Stylesheet

Earlier, the theme_philquotes_quote() function wrapped our quote's text and origin information inside of two <div/> tags. Each tag has a unique id attribute:

  • philquotes-text
  • philquotes-origin

Using those IDs, we can create a stylesheet that styles those two elements.

By convention, a module's main stylesheet should be named .css (where is the name of the module). As with all other module files, this file belongs inside the module directory (drupal/sites/all/modules/philquotes, for this example). Our philquotes.css file looks as follows:

#philquotes-text:first-letter {
font-size: 18pt;
font-weight: bold;
#philquotes-origin {
font-style: oblique;
text-align: right;
margin-right: 5px;

Here we have a simple stylesheet. When used, it will add some additional styling to our bland HTML.

However, simply having the stylesheet (and naming it correctly) is not enough to add this style to the default theme. Drupal does not automatically include a module's stylesheet when the page is rendered. We have to tell the theme system to include it.

Adding a stylesheet is done with a built-in function: drupal_add_css(). Using this function in our theme_philquotes_quote() hook, we can instruct the theme system to include the module's CSS file along with the other stylesheets Drupal will list in the HTML it sends to the client.

function theme_philquotes_quote($text, $origin) {
$module_path = drupal_get_path('module', 'philquotes');
$full_path = $module_path .'/philquotes.css';
$output = '<div id="philquotes-text">'. t($text)
.'</div><div id="philquotes-origin">' . t($origin) . '</div>';
return $output;

The above highlighted lines show the necessary modifications. Before Drupal can load the stylesheet, it must have the full path to the location of that stylesheet. The CSS file is located in our module, and we can construct the full path to this module using the drupal_get_path() function. Then we can append /philquotes.css to the string returned by drupal_get_path().

drupal_get_path() is another useful function that is often needed for module development. It takes two arguments.


The first is the item type, which (for module developers) is usually either theme or module, depending on whether the path is part of a theme or a module. The second parameter is the name of the theme or module that this should get the path for. To get the path of the Descartes theme we created in the last chapter, then, we could use drupal_get_path('theme', 'descartes').

For the details of drupal_get_path(), see

We can now pass $full_path into the drupal_add_css(), which will include a link to our CSS file in the header of the HTML output when this module is used.

The output from our module should now look something as follows:

With the addition of our stylesheet, the output of our philquotes module is now styled.

What we have done so far is created a default theme for our module. But why go through all of this trouble when we could have just hard-coded the HTML into the module?

Here is one very good reason.

By creating a default theme, we have ensured that our module can be displayed regardless of whether a theme developer created templates or theme functions specifically for our module.

Yet by using the theme system, we have also made it possible for a theme designer to override (or modify) our default theme. Thus, a theme developer can change the layout and styling for our module's content without having to edit a line of the module's code.

In the next section, we will see how this is done.

Overriding the Default Theme from a Theme

One of the chief advantages of using the theme system in a module is that it affords the theme developer the ability to use a module, but theme the module's contents as desired. Here we will take a look at theming module contents from a theme.

A Quick Clarification

We are now treading on the verges of terminological overload. The word 'theme' runs the risk of becoming ambiguous. So let's pause for just a moment and get clear on what we are about to do.

Thus far, we have been working on a module. In this module, we have created a default theme. This default theme has provided layout for this module's content. The default theme is used when the site's theme (be it Descartes, Bluemarine, Garland, or another theme) does not provide facilities for handling this module's content.

Now we are going to take a lateral step and work on a theme. We are switching directories from drupal/sites/all/modules to drupal/sites/all/themes.

We will modify the current theme, creating an alternative presentation of the quotes that are retrieved from our philquotes module. In other words, we are overriding the module's default theme.

We will look at two ways of doing this. One method will override the CSS styling only, and the other will make use of PHPTemplates to override the theme hook.

Overriding the Default Theme's CSS

If we look at in the <head/> section of a page that uses the philquotes block, we should see a link to the module's stylesheet:

<link type="text/css" rel="stylesheet" media="all"
href="/drupal/sites/all/modules/devel/devel.css" />
<link type="text/css" rel="stylesheet" media="all"
href="/drupal/sites/all/modules/philquotes/philquotes.css" />
<link type="text/css" rel="stylesheet" media="all"
href="/drupal/themes/bluemarine/style.css" />
<link type="text/css" rel="stylesheet" media="all"
href="/drupal/sites/all/themes/descartes/new.css" />

The highlighted link was added by the module's call to drupal_add_css().

What if we want to override the styles from that file?

The placement of the CSS files is significant. Module CSS files are always loaded before theme CSS files. That way, theme files can use the CSS cascade rules to override or augment the styles specified in a module's CSS. (See the CSS2 specification for more information on the cascade:

Because of the order, the theme developer can override the module's default CSS by adding CSS statements to the theme's stylesheets.

So, for example, we could change the style of the <div id="philquotes-text"/> element simply by adding a few lines to the end of the new.css file we created for the Descartes theme:

#philquotes-text {
color: pink;

This change doesn't directly override any of the styling added by the philquotes.css. Instead, it augments existing style.

Now, in addition to the styles already added by philquotes.css, the text of the quote will be rendered in a lovely shade of pale pink:

Theming Modules in Drupal 6

In this way, theme developers can override and extend the default CSS defined in the module.

Overriding Layout with Templates

In addition to being able to override CSS directives, a theme developer can also override the module's default theme hook. For example, we could create a simple template file in the Descartes theme to override the layout provided by the philquotes module's theme_philquotes_quote() function.

For the theme engine to recognize that this template is overriding the theme_philquotes_quote() hook implementation, it must be named philquotes_quote.tpl.php.

As an example, we can create a simple template in drupal/sites/all/themes/descartes/philqyotes_quote.tpl.php that looks as follows:

// $Id$
<div id="philquotes-text">
<?php print $text; ?>
<div id="philquotes-origin" style="background-color: #efefef">
<?php print $origin; ?>

This preserves the same basic structure as the previous module, but hard-codes in a background color. When rendered, the output would look something like this:

Theming Modules in Drupal 6

Note the light gray background in the origin section, as added in the preceding template.

But what happened to the other styles? Why isn't the origin in italics or the first letter of the quote's text in large caps?

The answer is found in the fact that the template overrode the theme_philquotes_quote() function. That function was responsible for the initial addition of the stylesheet with drupal_add_css().

However, with the addition of the template, that function is no longer called and the stylesheet is no longer included. To include it, we would have to add the appropriate stylesheets[all][] directive to the theme's .info file.

stylesheets[all][] = style.css
stylesheets[all][] = new.css
stylesheet[all][] = philquotes.css

It is good for a module to provide a default theme, and when this is done correctly, it maximizes the effectiveness of Drupal's module and theme systems. A module will never be without a theme, but the theme developer will also be able to keep a module's look and feel consistent with the rest of the site.


In addition to creating a new module that provides a default theme, we worked with a simple custom content type and even made our first foray into the Drupal database API. At this point, we have a solid foundation for future module development.


If you have read this article you may be interested to view :

You've been reading an excerpt of:

Learning Drupal 6 Module Development

Explore Title