Exploring Drupal V6, Part 3: Building a Drupal module

You've learned the basics of Drupal V6 and even added modules to a Drupal site. In this final installment in the "Managing Drupal V6" series, learn how to write and deploy a custom module to create a novel content type.

Martin Streicher, Editor in chief, Linux Magazine

Martin StreicherMartin Streicher is the editor in chief of Linux Magazine. Martin earned a Master of Science in Computer Science from Purdue University and has been programming UNIX-like systems since 1986 in the Pascal, C, Perl, Java, and (most recently) Ruby programming languages.


developerWorks Contributing author
        level

15 September 2009

In this day and age, every endeavor requires a Web site. A Web site is simultaneously a business card, a brochure, a portfolio, a catalog, an invitation, an annual report, an advertisement, and an outpost. Indeed, without a URL and adequate placement in a search engine, a business can go unnoticed.

Frequently used acronyms

  • API: Application programming interface
  • HTML: Hypertext Markup Language
  • HTTP: Hypertext Transfer Protocol
  • SQL: Structured Query Language
  • URL: Uniform resource locator

However, not all Web sites require the same features, and the technical expertise of operators varies, as well. Hence, some content-management system (CMS) software is intentionally modest and simple to use, while others are expansive and commensurately intricate. Budgets vary, too. One Web site may have the resources to purchase an expensive commercial application, while another may have more modest means and expectations.

And although there is no single "right solution" — only the solution that's right for any circumstance — many choose the same software: Drupal. Drupal is open source, free to use (in all senses of the word), and easy to launch, but wildly extensible, making it a worthy option for small, large, emerging, and aspirational sites alike. Hundreds, if not thousands, of Drupal modules, or extensions, are available to customize a Drupal site, and if no suitable module exists, writing a new one is a reasonable undertaking.

Part 1 of this series demonstrates how to install and launch Drupal V6, the most recent revision of the popular package, on a UNIX®-like system. Part 2 shows how to add modules to a Drupal V6 site to incorporate new features. This final installment shows how to write and deploy a custom module to create a novel content type.

Like Drupal itself, a custom module is written in PHP, so familiarity with the programming language and its tools and libraries is beneficial. Typically, a module also accesses an underlying database. Some experience with a relational database and SQL is helpful, as well.

Introducing Drupal module development

A Drupal content module typically extends the standard Drupal node with supplemental fields stored in a separate table. For example, an attachment module might record the name, location, and size of a file in its own table, then associate that information with a story node. A content module also leverages the Drupal user, role, and permission subsystem to realize and enforce privileges and persist preferences. Figure 1 captures the logical structure.

Figure 1. Drupal content module
Drupal content module

Developing a Drupal content module requires several steps:

  1. Name the module. Whether you plan to distribute your module or not, its name must be unique to prevent clashes with other modules. Choose a name that's descriptive and short.
  2. Create a separate directory for the module's code and associated resources. A module has metadata, an installer, code, and templates. All these resources are collected in one independent directory. Being amassed in one place also simplifies distribution and installation.
  3. Write an .info file to identify the module and its purpose. This metadata defines prerequisites, the proper name of the module, and more.
  4. Write the code for the module, including a module installer, a data-entry form, and a display template.
  5. Install, enable, and configure the module. You install your own module using the same technique shown in Part 2.

Drupal provides a well-defined interface to extend its core features. To create a module, you must implement that interface — or what Drupal calls hooks. For example: One hook provides help, another set of hooks augments the Drupal database with the tables required for your module, and yet another set persists your module's special data to the database. Unlike previous releases of Drupal, which required developers to write SQL and HTML, Drupal V6 provides rich APIs to define your custom schema and describe your custom form. The APIs use PHP and simple data structures. However, hooks that access the database directly are still authored in SQL.

Additionally, each Drupal module follows a strict set of coding conventions to ensure uniformity and facilitate code sharing and community maintenance. Those conventions are intentionally omitted here for brevity, with one exception. Do not terminate your module's code with a closing ?>. If your Drupal log indicates that text has been sent before the standard HTTP headers, remove any leading white space and the trailing ?> from your files. See Resources for a complete list of coding standards.


Building Flitter — A Twitter-like content type

For this demonstration, let's create a module to record very short messages, such as those exchanged on Twitter. Each message should have a title (just like every Drupal node) and a terse statement of 140 characters or less. Let's call the module Flitter. To realize Flitter, you must extend the Drupal database to include a new Flitter table; permissions to access Flitter data, or flits; a form to edit a flit; and some code to manipulate the database.

To begin, create a directory for the Flitter module. By convention, the module should reside in $DRUPAL_ROOT/sites/all/modules/flitter, where $DRUPAL_ROOT is the root directory of your Drupal site, such as /var/www/drupal. (The directory $DRUPAL_ROOT/modules is reserved for Drupal's core modules.)

$ export DRUPAL_ROOT=/var/www/drupal
$ mkdir -p $DRUPAL_ROOT/sites/all/modules/flitter

The first command sets a helpful variable. mkdir -p creates the Flitter directory and all intermediate directories that are missing along the absolute path.

Next, create the module's metadata file, flitter.info, in $DRUPAL_ROOT/sites/all/modules/flitter/flitter.info. The contents of this file identify the module in the module administration page. The file is a standard PHP .ini file and must contain entries for name, description, core, and php.

name          = "Flitter" 
description   = "A custom content type to display very brief messages"
core          = 6.x 
php           = 5.1

The name and description are self-evident; be sure to enclose both values in quotation marks. The core specifies which Drupal version is compatible. And php details which revision of PHP is required. The latter two values preclude mismatches between the needs of the module and the capabilities of the system.

To continue, create a file for the bulk of the module's code in $DRUPAL_ROOT/sites/all/modules/flitter/flitter.module. The first hook to implement is module_help(), where module is the name of your module. In this case, the hook is called flitter_help(). This hook provides additional information about the Flitter module and its intended usage.

<?php
function flitter_help($path, $arg) { 
  if ($path == 'admin/help#flitter') { 
    $txt = 'A flitter is a very short message. '
      . 'The title should clearly convey the topic, '
      . 'and the message should convey an opinion, status, '
      . 'event, or reference in 140 characters or less. ' ;
    $replace = array(); 
    return '<p>' . t($txt, $replace) . '</p>';
  }
}

Figure 2 shows how this hook might appear once the module is installed. flitter_help() renders the information for the module's help page.

Figure 2. module_help() renders information for module's help page
module_help() hook

The function t() used in the last line of code is provided by Drupal. Specifically, t()— short for translate— attempts to convert the English text, $txt, into the user's preferred language. Here, t() would attempt to translate the help text from English into the user's native tongue. The translation is conducted via a dictionary you define.

The second argument to t()$replace— is used to swap placeholders in the text with actual values. In this example, no placeholders are employed. Yet, for example, $txt could include a placeholder for, say, the address of a URL abbreviation service.

$txt = 'A flitter is a very short message. The title should clearly '
  . 'convey the topic, and the message should convey an opinion, status, '
  . 'event, or reference in 140 characters or less. Use !url to shorten ' 
  . 'URLs.';

In the code above, !url is a named placeholder. The ! form of placeholder substitutes one string directly with another. (Other forms of placeholders are available, too.) This call performs the actual replacement, substituting !url with http://www.bit.ly: t($txt, array('!url' => 'http://www.bit.ly/'));.

Let's continue building the module. Because Flitter creates a new content type, the next step must define a new database table to persist the custom Flitter data and correlate its fields to a Drupal node. Let's call a Flitter datum a flit.

To abstract the specifics of the underlying database and to simplify module development, Drupal V6 introduces a Schema API (see Resources) to define tables using plain PHP associative arrays. By convention, the creation and deletion, which occurs when you uninstall the module, of the new Flitter table is defined in a special file, flitter.install, using a set of specifically named hooks. Listing 1 shows the entirety of flitter.install.

Listing 1. flitter.install
<?php
function flitter_install() {
  drupal_install_schema('flitter');
}

function flitter_uninstall() {
  drupal_uninstall_schema('flitter'); 
}

function flitter_schema() {
  $schema['flitter'] = array(
    'fields'      => array(
      'vid'       => array(
        'type'    => 'int',
        'unsigned'=> TRUE,
        'not null'=> TRUE,
        'default' => 0),
      'nid'       => array(
        'type'    => 'int',
        'unsigned'=> TRUE,
        'not null'=> TRUE,
        'default' => 0),
      'message'   => array(
        'type'    => 'varchar',
        'length'  => 140,
        'not null'=> TRUE,
        'default' => '')),
    'indexes'     => array(
      'nid'       => array('nid')),
    'primary key' => array('vid'));
  
  return $schema;
}

The code of flitter_schema() should look vaguely familiar: It resembles what you might otherwise write in SQL to define fields, albeit expressed as parameters, so it's readily translated to the specifics of whatever database engine powers your Drupal site. The associative array, $schema['flitter'], must define values for fields, indexes, and primary_key, which are the table's columns, the table's indices to speed lookups, and the table's primary key, respectively.

Recall that a core Drupal node records essential information, such as the title of a piece of content, the date the content was created, and the disposition or state of the content (published, draft, etc.), so those fields need not be repeated in a flit. The fields required for a flit are its version, vid; the node ID associated with the flit, nid; and the flit message itself, which is defined as a 140-character text field.

The value defined for indexes in $schema['flitter'] is also notable: The index refers to the tuple (version ID, node ID), which hastens the lookup for the most recent version of a flit.

The previous hook defines the new data structure, but does not describe how it and a node relate. To correlate the new flit table to a node, you must implement another hook: flitter_node_info(). This hook returns an array to describe the Flitter content type (or in other cases, all the content types the module implements). flitter_node_info() is shown in Listing 2. It should appear as another function in the flitter.module file.

Listing 2. flitter_node_info() describes content types defined by Flitter
function flitter_node_info() {
  return array(
    'flitter' => array(
      'name'        => t('Flitter'),
      'module'      => 'flitter',
      'description' => t('A very brief message to acquaintances.'),
      'has_title'   => TRUE,
      'title_label' => t('What are you doing?'),
      'has_body'    => FALSE));
}

The information conveyed by flitter_node_info() appears in multiple places. The name and description fields appear in the Create Content page, as shown in Figure 3.

Figure 3. module_node_info() describes purpose of content type
The module_node_info() hook

The has_title and has_body control whether the title and body fields provided by node, respectively, appear in the form to create and edit a flit. Figure 4 shows the resulting form. Because has_title is true, the field appears with the label defined by title_label. The body field is hidden because has_body is false.

Figure 4. Form to create a new flit
Form to create a new flit

But flitter_node_info() only describes how the standard node fields appear in the form. To add the field for the flit custom message, you must implement another hook: flitter_form(). Because data entry is a seminal function in any CMS, Drupal offers a Form API (see Resources) to describe form fields. Listing 3 shows the flitter_form() hook.

Listing 3. flitter_form() describes form required by Flitter
function flitter_form(&$node) {
  $type = node_get_types('type', $node);
  if ($type->has_title) {
    $form['title'] = array(
      '#type'         => 'textfield',
      '#title'        => check_plain($type->title_label),
      '#required'     => TRUE,
      '#default_value'=> $node->title);
  }
  
  $form['message'] = array(
    '#type'           => 'textfield',
    '#size'           => 70,
    '#maxlength'      => 140,
    '#title'          => t('Tell us more'),
    '#description'    => t('A short description or elaboration'),
    '#required'       => TRUE,
    '#default_value'  => isset($node->message) ? $node->message : '');
    
  return($form);
}

The if statement in flitter_form may puzzle you. Isn't the title a required field for a flit? As defined by the hook, yes. However, the site administrator can disable the title field via the administrative tools. Hence, the form for a flit must check whether the title field is enabled and otherwise omit it. There is no such test for the message field because it's the core of a flit.


Permissions and the database

At this point, all the data-collection hooks are finished, but there are three sets of module hooks left to create: permissions, database access, and display. These sets of hooks are shown in order in Listing 4, Listing 5, and Listings 6.

Listing 4 defines flitter_perm(), which enumerates the permissions honored by the module. However, without its companion, flitter_access(), the permissions are abstract and arbitrary names. flitter_access() maps each named permission to one of create, update, or delete.

Listing 4. flitter_perm() defines permissions available in module
function flitter_perm() {
  return array(
    'create flit',
    'edit flits',
    'delete flits' );
}

function flitter_access($op, $node, $account) {
  switch ($op) {
    case 'create':
      return user_access('create flit', $account);
    case 'update':
      return user_access('edit flits', $account);
    case 'delete':
      return user_access('delete flits', $account);
  }
}

Figure 5 shows the permissions for Flitter on the permission administration page. Recall that you created the groups Management and Staff during the installation and configuration in Part 1.

Figure 5. Flitter module defines its own permissions
The Flitter module defines its own permissions

Listing 5 includes all the hooks required to interact with the Drupal database. These hooks—flitter_insert(), flitter_update(), flitter_delete(), and flitter_nodeap1()— use SQL to affect the database, and the syntax of the SQL is specific to the database engine you are using. In this case, the syntax is for MySQL. The syntax for other relational databases is similar.

Listing 5. Hooks add, modify, and remove flits
function flitter_insert($node) {
  db_query(
    'INSERT INTO {flitter} (vid, nid, message) '
      ."VALUES ('%d', '%d', '%s')",
      $node->vid, $node->nid, $node->message);
}

function flitter_update($node) {
  if ($node->revision) {
    flitter_insert($node);
  } else {
    db_query(
      "UPDATE {flitter} SET message = '%s' WHERE vid=%d", 
        $node->message,
        $node->vid);
  }
}

function flitter_delete($node) {
  db_query("DELETE FROM {flitter} WHERE nid=%d", $node->nid);
}

function flitter_nodeap1(&$node, $op, $note, $page) {
  if ($op == 'delete revision') {
    db_query("DELETE FROM {flitter} WHERE vid=%d", $node->vid);
  }
}

The former three hooks require little explanation, with one exception: The if statement in flitter_update() determines whether versioning is enabled. If so, rather than update an existing record, which overwrites the information, the method creates a new record to capture the revision. The oddly named flitter_nodeap1() is a special hook to delete a specific revision. The hook flitter_delete() deletes every version of a specific node.

Listing 6 defines the three hooks required to retrieve, prepare, and display a flit. flitter_load() retrieves a specific revision from the database. flitter_view() is largely boilerplate and transforms the node into viewable HTML. flitter_theme() names a template to render the flit and specifies that the message field be sent to be rendered. Again, the title and other fields found in the flit's associated node are sent automatically.

Listing 6. Hooks fetch, prep, and display a flit
function flitter_load($node) {
  $r = db_query("SELECT message FROM {flitter} WHERE vid=%d", $node->vid);
  return db_fetch_object($r);
}

function flitter_view($node, $note = FALSE, $page = FALSE) {
  $node = node_prepare($node, $note);
  $message = check_markup($node->message);
  $node->content['flitter_info'] = array(
    '#value' => theme('flitter_info', $message));
  
  return($node);
}

function flitter_theme() {
  return array( 
    'flitter_info' => array( 
      'template' => 'flitter_info', 
      'arguments' => array( 
        'message' => NULL)));
}

Listing 7 is the last piece of the puzzle. It is a listing of flitter_info.tpl.php, the template used to render each flit. Like the other files shown here, the template is also stored in the module directory.

Listing 7. flitter_info.tpl.php
<div class="biography_info"> 
	<h2>
	<?php print t('Message'); ?>:</h2> 
	<?php print $message; ?>
</div>

Figure 6 shows a flit rendered on the home page. Immediately below the flit is a traditional Drupal story, which demonstrates how heterogeneous content can be rendered on the same page.

Figure 6. Flit rendered on home page alongside a story
Flit rendered on the home page alongside a story

At completion, your module directory should now have (at least) four files, like so:

$ ls flitter
flitter.info		flitter.module
flitter.install		flitter_info.tpl.php

You can now enable the module, as shown in Figure 7. When you enable the module, its installer adds the Flitter table, makes the content type available to qualified users, and renders flits to specification. If you choose to disable and uninstall the module, all the flits and associated nodes are purged.

Figure 7. To run Flitter, turn it on like any other module
Run Flitter

Build what you want

Hopefully, you now know enough about Drupal to evaluate it for your own use. If you have a list of requirements and some aren't directly met by the Drupal core, browse the long list of available modules to see whether the feature you desire has already been implemented. Chances are, someone has already built a solution. If no suitable module is available, you can create a new feature in little time using the Drupal module APIs. You can even consider using Drupal as a base for expansive new development.

Resources

Learn

  • Visit Drupal.org to find documentation, add-on modules, and other resources, as well as connect with others in the Drupal community.
  • Check out the showcase Theme Garden of available Drupal themes. Similarly, Drupal Modules is a searchable database of available Drupal modules.
  • See the complete list of Drupal coding standards.
  • To listen to interesting interviews and discussions for software developers, check out developerWorks podcasts.
  • Stay current with developerWorks' Technical events and webcasts.
  • Follow developerWorks on Twitter.
  • Check out upcoming conferences, trade shows, webcasts, and other Events around the world that are of interest to IBM open source developers.
  • Visit the developerWorks Open source zone for extensive how-to information, tools, and project updates to help you develop with open source technologies and use them with IBM's products.
  • Watch and learn about IBM and open source technologies and product functions with the no-cost developerWorks On demand demos.

Get products and technologies

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Open source on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Open source
ArticleID=427370
ArticleTitle=Exploring Drupal V6, Part 3: Building a Drupal module
publish-date=09152009