Skip to main content

Using open source software to design, develop, and deploy a collaborative Web site, Part 6: Building a custom module in Drupal

Alister Lewis-Bowen (alister.lewisbowen@gmail.com), Senior Software Engineer, IBM, Software Group
Alister's photo
Alister Lewis-Bowen is a senior software engineer in IBM's Internet Technology Group. He has worked on Internet and Web technologies as an IBM UK employee since 1993. Alister was brought to the U.S. to work on the Web sites for the IBM-sponsored sports events, then as senior Webmaster for ibm.com. He is currently helping create semantic Web prototypes.
Stephen Evanchik (evanchsa@gmail.com), Software Engineer, IBM, Software Group
Stephen's photo
Stephen Evanchik is a software engineer in IBM's Internet Technology Group. He has been a contributor to many open source software projects, the most notable being his IBM TrackPoint driver in the Linux kernel. Stephen is currently working with emerging semantic Web technologies.
Louis Weitzman (louis.weitzman@gmail.com ), Senior Software Engineer, IBM, Software Group
Louie's photo
Louis Weitzman is a senior software engineer in IBM's Internet Technology Group. For 30 years he has worked at the intersection of design and computation. He helped develop an XML, fragment-based content management system in use by ibm.com, and currently is involved with bringing the design process to emerging projects.

Summary:  In this series, the IBM® Internet Technology Group designs, develops, and deploys a closed community Web site using a suite of software that is freely available. Part 5 introduces Drupal concepts and several programming standards used to develop a Web site. This article continues exploring our process and describes the creation of a custom Drupal module for announcements. The authors discuss implementation and include code samples that show how to create your own custom module. Use this article as a starting point to build your own custom modules.

View more content in this series

Date:  12 Sep 2006
Level:  Intermediate
Activity:  5806 views

Introduction

In this article you learn how to create a very simple, custom module to provide announcements on a Web site. The information should not be interpreted as a rigid set of development guidelines, but rather, as a place to start when building your own custom modules. In many cases we provide a quick introduction to topics and refer to the Drupal documentation to provide in-depth details.

Our fictitious company, International Business Council (IBC), needs to display relevant announcements that are automatically published and then removed from the Web site after specified times. The announcements require a few new data fields:

  • An abstract that is different from the default teaser and is controlled by the author of the announcement (and lets us illustrate several Drupal features)
  • A publication date, when the announcement will first appear on the Web site
  • An expiration date, when the announcement will be removed from the Web site
  • The ability for the administration staff to see the announcements even if the announcement is not within the publication and expiration dates

The home page displays the current announcements in the main content area, as shown in Figure 1. A sidebar is used on every page to include the most recent announcements. Announcements that the current user created are highlighted with a colored background.


Figure 1. IBC home page
IBC home page

Getting started

Before starting, we create the file that will contain all of our code that defines the new module. Then, we will modify the database to support the information relevant to our new module.

File creation

To get started, we create the modules/announcement directory under the ibc_site. We create an announcement.module file in that directory, as shown in our Eclipse environment in Figure 2. Typically, the name of a module is the same as the new node type it creates. This file will contain all of the specific announcement code we describe in this article.

Note: Current best practices for Drupal are to place all non-core modules into the sites directory for the associated domain. So in our example, it would be better to place the announcement directory into the sites/drupal.development directory created in the earlier articles. Keeping contributed modules out of the core distribution simplifies the updates to the core Drupal code.

Also in Drupal 4.7, formal .install files can be used to prepare any database tables and data required for your modules. Instead of using the announcement.sql file, you can use php to perform the SQL operations within an announcement.install file within your announcement module directory. You can read more about this at http://drupal.org/node/51220.

Thank you, Boris, for pointing this out. For more details on this method, see Boris' blog.


Figure 2. Navigator view in Eclipse
Navigator view in Eclipse

Database modifications

All data represented by a node is stored in the node table and, in version 4.7, the node_revisions table. As a node is modified, revisions are stored in the node_revisions table, including the title, teaser, and body. Because the existing Drupal tables did not support the requirements of our new module, we created an announcement table to store the announcement-specific information. To have more control over the teaser, we included our own abstract in this new table. We first create the table and will later write the functions to access that table on specific events.

Listing 1 shows the command that creates an announcement table that is linked to the node table through the node ID, nid. The new columns in the table are the abstract, the publish_date, and the expiration_date. We create the new table in the database at the SQL command line or through the MySQL query browser. We saved this database command in the announcement.sql file in the announcement directory (Figure 2). This step documents the database tables for this module and also allows for easy updating of the database if needed.


Listing 1. Command to create new announcement table
CREATE TABLE announcement (
  nid             int(10) unsigned NOT NULL default '0',
  abstract		  varchar(255) default '',
  publish_date    integer NOT NULL default '0',
  expiration_date integer NOT NULL default '0',
  PRIMARY KEY (nid)
);
	


Developing the module

Drupal's interface for modules is through a set of functions called hooks. In this section we'll show the development of several hooks to support our custom module. They represent a starting set of functions to get our module up and running; we implemented only some of the Drupal hooks.

hook_settings
Creates attributes for this module that the administrator can modify.
hook_help
Provides documentation that appears in various places in the interface.
hook_perm
Defines the permission categories for information access.
hook_access
Defines the access permissions for different operations and users.
hook_menu
Sets up the URL paths and functions to call when handling access.
hook_link
Defines the links that can be added to presentations throughout the site.
hook_block
Defines a block of information from this module to be displayed.
hook_form
Defines the interface widgets to use when adding and editing this node.
hook_validate
Validates the input from a user before storing to the database.
hook_submit
Modifies the node, after validation, but before updating the database.
hook_load
Loads additional node information from the database.
hook_insert
Saves additional node information the first time.
hook_update
Saves additional node information if the node already exists.
hook_delete
Deletes additional node information when the node is deleted.
hook_cron
Performs scheduled actions as defined by the administrator.
hook_search
Defines custom search on the information of this node.
hook_nodeapi
Acts on nodes defined by other modules.
hook_node_info
Determines the name and attributes of the module's node types.

hook_settings

The settings hook provides a way to add attributes to the module that can be controlled by the administrator and can be used when displaying this module. For our announcement module we want to limit the number of announcements that are presented in the sidebar. The block hook will generate the presented announcement elements. However, we can give the administrator the ability to set the number of announcements that are shown in the sidebar through the administrator's user interface. These module-specific attributes are accessible in the interface by the following relative URL, as shown in below:

admin/settings/<module_name>

Listing 2 shows the announcement_settings hook implementation.


Listing 2. Announcement_settings hook implementation
function announcement_settings() {
  $form = array();
  $form['announcement_block_max_list_count'] = array(
     '#type'		=> 'textfield',
     '#title'		=> t('Maximum number of block announcements'),
     '#default_value'	=> variable_get('announcement_block_max_list_count', 3),
     '#description'	=> t('The maximum number of items listed in the announcement block'),
     '#required'	=> FALSE, 
     '#weight'		=> 0
  );
  return $form; 
}

The announcement_settings hook, as shown in Listing 2, defines an interface element that is used by the administrator when specifying the value for this attribute. The index to the form array is the variable name; for example announcement_block_max_list_count. The components of the array stored at this index define how the administrator's user interface will be constructed.

This value can then be accessed when building the sidebar of announcements to display. Using the code fragment in Listing 3, we retrieve the persistent variable defined in the settings hook when building the sidebar.


Listing 3. Retrieving the variable set in the settings hook
$items = variable_get('announcement_block_max_list_count', 3);
    

hook_help

The help hook provides a place to put documentation that will be displayed when an administrator or user interacts with the system. Two places this occurs is when the administrator can enable or disable a module in the admin/modules page. Figure 3 shows the line in the administrators screen to enable this module, with the announcement module highlighted.


Figure 3. Help documentation on administrator's page
Help documentation on administrator's page

Similarly, when a user adds a new announcement using the create content link, they will see a description of the node they are adding, as shown in Figure 4.


Figure 4. Help description of the node type that can be added
Help description of the node type that can be added

Listing 4 shows the implementation of the help hook for the announcement module that generates these two descriptions. Other help descriptions can also be produced.


Listing 4. Announcement_help hook implementation
function announcement_help($section) {
  switch ($section) {
    case 'admin/modules#description':
      return t('Enables the creation of announcement pages ' .
               'that are presented on the home page.');
    case 'node/add#announcement':
      return t('An Announcement. Use this page to add an announcement page.');
  }
}

hook_perm

The perm hook defines the access permissions that each role can be assigned. Arbitrary strings can be used to describe the actions relevant to your application. Here, we differentiate between creating an announcement and editing (or deleting) an announcement, as seen in Listing 5. The hook_access function can then use these permissions to control access to content.


Listing 5. Announcement_perm hook implementation
function announcement_perm() {
     return array('create announcement', 'edit announcement');
}

The default Drupal roles are anonymous user and authenticated user. The administrator can create additional roles from within the interface from the path admin/access/control. These roles, combined with the permissions defined above, form the basis of the permissions console as shown in Figure 5. This matrix of check boxes allows the administrator to enable permissions to each of the distinct roles within the system. The announcement module has been highlighted here to show the two strings defined in the hook_perm in Listing 5. Only a user with the roles of administrator or operations can create and edit announcements.


Figure 5. Interface allowing administrator to assign permissions
User interface to assign permissions to modules for roles.

hook_access

Each module can restrict access to the data that it represents. The access hook allows the module author to control that access. In practice, call the user_access function to check if the current user has the given access permissions. In Listing 6, in the user_access function we refer to the permissions defined in the announcement_perm function.

The access hook function is supplied the operation (for example, create, view, update or delete) and the node that is the focus of the operation. The function returns a Boolean value indicating whether or not the current user should have access to the operation on the specified node.

In this implementation, only users that have been assigned the "create announcement" permission can create an announcement. Users that have the "access content" permission (any authenticated user) can view an announcement. The update and delete operations can be executed if the user initiating the request is the owner of the content or if they have been explicitly granted permission to edit an announcement.

The first registered user created as part of the Drupal installation, the user with ID = 1, has root privileges and can edit and modify any piece of data in the system.


Listing 6. Announcement_access hook implementation
function announcement_access($op, $node) {
  global $user;
	
  if ($op == 'create') {
	  return user_access('create announcement');
  }
    else if ($op == 'view') {
 	 return user_access('access content');
  }
  else if ($op == 'update' ||  $op == 'delete') {
	if($user->uid == $node->uid || user_access('edit announcement')) {
	   		return true;
	}
	else {
		return false;
	}
  }
  else {
  	return false;
  }
}

hook_menu

How Drupal responds to URLs is defined in the menu hook function. This function defines callbacks for specific URLs and menu items. The standard way Drupal constructs URLs when referring to a specific node is to put the node ID before the operation. Using this format, the URLs that need to be specified for our module include:

   /announcements
   /announcements/add
   /announcements/<id>/view
   /announcements/<id>/edit
   /announcements/<id>/delete 

where <id> is the ID of the node being viewed, edited, or deleted. The definition of our announcement_menu function is shown in Listing 7.


Listing 7. Announcement_menu hook implementation
function announcement_menu($may_cache) {
   $items = array();
      if ($may_cache) {
         $items[] = array('path'     => 'announcements/add',
                          'title'    => t('Add a new Announcement'), 
                          'access'   => node_access('create', 'announcement'),
                          'type'     => MENU_CALLBACK,
                          'callback arguments' => array('announcement'),
                          'callback' => 'node_add');			 
         $items[] = array('path'     => 'announcements',
                          'title'    => t('Announcements'),
                          'access'   => user_access('access content'),
                          'type'     => MENU_CALLBACK,
                          'callback' => 'announcement_all');				 
      }
      else {
         if(is_numeric(arg(1))) {			
            $node = node_load(arg(1));
            $items[] = array('path'     => 'announcements/' . arg(1),
                             'title'    => t('View an Announcement'),
                             'access'   => node_access('view', $node),
                             'type'     => MENU_CALLBACK,
                             'callback' => 'node_page');                      
            $items[] = array('path'     => 'announcements/' . arg(1) . '/view',
                             'title'    => t('View an Announcement'),
                             'access'   => node_access('view', $node),
                             'type'     => MENU_CALLBACK,
                             'callback' => 'node_page');                      
            $items[] = array('path'     => 'announcements/' . arg(1) . '/edit', 
                             'title'    => t('Edit an Announcement'),
                             'access'   => node_access('edit', $node),
                             'type'     => MENU_CALLBACK,
                             'callback' => 'node_page');           
            $items[] = array('path'     => 'announcements/' . arg(1) . '/delete', 
                             'access'   => node_access('delete', $node),
                             'type'     => MENU_CALLBACK,
                             'callback' => 'node_delete_confirm');			
		}						 
	}
  	return $items;
}

The function returns an array of arrays, where each array includes:

path
The URL to match
title
Title of this menu item which is surfaced in a mouse hover operation
callback
Function to invoke when this URL is accessed
type
Type of menu item.
access
Access permissions to this menu item

Caching is used to make the production Web site more efficient. Here we can control portions of the menu definition to be cached or not using the $may_cache conditional. However, when things are cached, they may not reflect changes in your code during development. Cacheable items should not include variables, such as arg(1), in their path specification.

hook_link

One of the basic principles we tried to follow is to graphically place actions close to the item they act upon. In the case of announcements, we placed action links for adding, editing, deleting, and commenting next to the title of the announcement. Of course, these actions should only be presented if the current user has the appropriate access privileges. The actions are themed in a small, red font, as shown in Figure 6.


Figure 6. Action links next to title of an announcement
Action links next to title of an announcement

The link hook provides us with the mechanism to do this. Listing 8 shows our implementation of this hook. It lets us create the appropriate links based on access privileges, and then to theme these links later with the appropriate markup. This allows the CSS to style the text in a red, small font. The comment module also contributes to this set of links by providing the "Add Comment" action.

In the sequence l(t('Edit')...) in Listing 8, we use two utility functions to support link generation. The t function first translates the 'Edit' string to the current locale, then the l function formats that string to the appropriate internal Drupal link. Links are themed in the template engine with a call to theme('links', $links) and then mapped to the template variable $links.


Listing 8. Announcement_link hook implementation
function announcement_link($type, $node = NULL, $teaser = FALSE) {
   global $user;
   $links = array();
	
   if($type == 'node' && $node->type == 'announcement') {
      if (node_access('create', 'announcement')) {			
         $links[] = l(t('Add'), "node/add/announcement", 
                      array('title' => t('Add a new announcement'))); 
      }
      if (node_access('update', $node)) {
         $links[] = l(t('Edit'), "announcements/$node->nid/edit",
                      array('title' => t('Edit Announcement ') . $node->title)); 

         $links[] = l(t('Delete'), "announcements/$node->nid/delete",
                      array('title' => t('Delete Announcement ') . $node->title)); 
      }
   }
   return $links;
}

hook_block

Announcements appear in the center of the home page and in the navigation sidebar on every page, as shown in Figure 7. The sidebar content is enabled by the block hook, which allows a module to contribute content that can be displayed anywhere on the page. Usually the sidebar presentations are a synopsis of the full content. Notice that the highlighting of announcements for the current user is consistent in the sidebar and in the main content area. Figure 7 shows the sidebar for an administrator, so you see several Resources that can be used while logged in. These do not appear for a typical user.


Figure 7. Announcements in the sidebar using the block hook
Announcements listed in the sidebar

Once this hook is defined, the administrator can specify the placement of the block information through the user interface, as shown in Figure 8. The announcements are enabled and given a weight of -10 in the right sidebar. The smaller the number, the higher in the sidebar the content is presented. This ensures that announcements are the first block of information presented in the right sidebar.


Figure 8. Administrator's interface for specifying block placement
Interface for specifying block placement.

Listing 9 shows the implementation of our block hook for the announcement module. This function is capable of constructing more than one block. Drupal uses the $delta argument to index the block that is to be displayed as defined in the $block array. The first condition the function tests for is the list operation. That information is used in the administrator's interface shown in Figure 8.

The second operation is view, which collects the appropriate announcements to show in the information block we use in the right sidebar. It uses the announcement setting announcement_block_max_list_count defined in the announcement_settings hook to determine how many announcements to display. The view operation uses the announcement_block_list template file (announcement_block_list.tpl.php) to theme the announcements. (The theming of the announcements will be described in a future article.)


Listing 9. Announcement_block hook implementation
function announcement_block($op = 'list', $delta = 0, $edit = array()) {
   global $user;
   if ($op == 'list') {
      $blocks[0]['info'] = t('Recently updated announcements');	
      return $blocks;
   }
   else if ($op == 'view')	{
      $block	= array();
      $output	= '';
      switch ($delta) {
         case 0:
            $now = time();
            if (user_access('access content')) {
               $q = 'SELECT N.uid,N.nid,N.title,A.publish_date,N.status '.
			'FROM {node} N JOIN {announcement} A USING(nid) '.
			"WHERE N.type='announcement' ".
			'AND N.status = 1 '.
			'AND A.publish_date < ' . $now . ' '.
			'AND A.expiration_date > ' . $now . ' '.
			'ORDER BY A.publish_date DESC ';
               $items = variable_get('announcement_block_max_list_count', 3);
               if ($items) { $q .= "LIMIT 0,$items"; }
               $announcements = db_query($q);
               $announcement_items = array();
               while (db_num_rows($announcements) > 0 and $announcement =     
                      db_fetch_object($announcements)) {	
                  $announcement_items[] = $announcement;			
               }
         }
         $block['subject'] = t('Announcements');
         $block['content'] = theme('announcement_block_list',$announcement_items);
	  break;
      }
      return $block;
   }
}

hook_form

The form hook is called to generate the user interface to add or edit the contents of a node. This hook returns an array of arrays for each piece of content that needs to be edited by the user. An announcement node is edited with the interface shown in Figure 9. We provide input forms for the title, the publish and expiration dates, the abstract, and the body. A short description is provided below each form element to indicate to the user what the information is and how it will be used.


Figure 9. Editing form for an announcement node
Editing form for an announcement node

To generate the form element to edit the abstract of the announcement we create an array, as shown in Listing 10.


Listing 10. Produce a text area to edit the announcement abstract
$form['abstract'] = array(
   '#type' => 'textarea',
   '#title' => t('Abstract'),
   '#default_value' => $node->abstract,
   '#rows' => 3,
   '#description' => t('Short summary of the full announcement'),
   '#required' => TRUE,
   '#weight' => 9 
);

The index of the array, abstract, is the name of the element. The value is accessed from the node data structure as $node->abstract. The details for this element include:

type
Type of widget is a textarea (which affects the other attributes available).
title
Title displayed transformed by the translation function "t."
default_value
Value used in the widget when initially displayed, for example current value.
rows
Rows to display for the textarea.
description
Text to display below the widget in the interface.
required
Whether this input field is required or not.
weight
Affects the ordering of the widgets: smaller numbers appear higher in the interface.

Publication is defined as a fieldset to group the publish and expiration dates. The full announcement_form function that produces the interface for editing an announcement node is shown in Listing 11. The first two if clauses set up reasonable defaults, if none are provided, for the publish and expiration dates. We use the #prefix and #suffix elements in the publication array to insert additional HTML tags to enable simple CSS theming of the date picker. The string index to the $form array is used to subsequently de-reference the specific value from the node.


Listing 11. Complete announcement_form
function announcement_form(&$node) {
   if ($node->expiration_date == NULL) {
      $node->expiration_date = time() + (365 * 86400);
   }
   if ($node->publish_date == NULL) {
      $node->publish_date = time();
   }
   $form['title'] = array('#type' => 'textfield',
      '#title'         => t('Title'),
      '#default_value' => $node->title,
      '#description'   => t('Title of the announcement'),
      '#required'      => TRUE, 
      '#weight'        => 1
   );
   $form['publication'] = array('#type'=> 'fieldset',
      '#collapsible'   => FALSE,
      '#title'         => t('Publication dates'),
      '#weight'        => 5 
   ); 
   $form['publication']['publish_date'] = array(
      '#prefix'        => '<div class="date_widget">',
      '#suffix'        => '</div>',
      '#type'          => 'date',
      '#title'         => t('Publication date'),
      '#default_value' => _announcement_unixtime2drupaldate($node->publish_date)
   );
   $form['publication']['expiration_date'] = array(
      '#prefix'        => '<div class="date_widget">',
      '#suffix'        => '</div>',
      '#type'          => 'date',
      '#title'         => t('Expiration date'),
      '#default_value' => _announcement_unixtime2drupaldate($node->expiration_date)
   );
   $form['abstract'] = array('#type' => 'textarea',
      '#title'         => t('Abstract'),
      '#default_value' => $node->abstract,
      '#rows'          => 3,
      '#description'   => t('Short summary of the full announcement'),
      '#required'      => TRUE,
      '#weight'        => 9 
   );
   $form['body'] = array('#type' => 'textarea',
      '#title'         => t('Body'),
      '#default_value' => $node->body,
      '#description'   => t('Full content for the announcement which ' .
		      'is shown with the abstract on the details page'),
      '#required'      => TRUE, 
      '#weight'        => 10 
   );
   return $form;
}

One of the big changes from Drupal 4.6 to 4.7 is the form hook implementation. The Drupal Web site has a lot of relevant documentation, including:

hook_validate

At the end of the editing process, the validate hook is triggered before storing the node. This can be used to validate the data before it is stored in the database. For announcements, we use this hook by ensuring that the start date comes before the end date. A more interactive implementation using client-side scripting would not allow this condition to occur by constraining the two fields to be consistent with each other. If you want to make additional changes to the node before storing it, use the submit hook.

Listing 12 shows the implementation of the validate hook. We first convert the dates to integers that we can compare. If there is a problem that the user needs to address, use the form_set_error function, which uses Drupal's error handling mechanism. The first argument to this function, publish_date in Listing 12, refers to the form array name used in the form hook (Listing 11) and will highlight that element in the interface.


Listing 12. Announcement_validate hook
function announcement_validate($node) {
   if ($node) {
      $publish_date    = 
          _announcement_drupaldate2unixtime($node->publish_date);
      $expiration_date = 
          _announcement_drupaldate2unixtime($node->expiration_date);
      if ($publish_date >= $expiration_date) {
         form_set_error('publish_date', 
        t('The publish date of an announcement must be before its expiration date.'));
      }
   }
}

hook_submit

Once the node passes the validation phase, the submit hook is called where we can make additional changes to the node before the database is actually updated. In Listing 13, the submit hook updates the publish and expiration dates on the node and then modifies the node status based on the current date. If the current date is between the publish and expiration dates the status is set to 1 , else it is set to 0.


Listing 13. Announcement_submit hook
function announcement_submit(&$node) {
   $node->publish_date    = 
      _announcement_drupaldate2unixtime($node->publish_date);
   $node->expiration_date = 
      _announcement_drupaldate2unixtime($node->expiration_date);
	
   $now = time();
   if ($now >= $node->publish_date && 
       $now < $node->expiration_date) {
      $node->status = 1;
   }
   else {
      $node->status = 0;
   }
}

Database hooks

Drupal triggers a hook function on various events that occur in the environment when interacting with the database. The important events include load, insert, update, and delete. See more information about these hook events.

We'll discuss MySQL and the database abstraction layer in more detail in a subsequent article.

Drupal has a database abstraction layer that is used throughout this code. These functions, which begin with db_, are used in the database hooks to access this abstraction layer.

hook_load

The load hook is automatically called when a node of type announcement is loaded from the database. This function allows a custom module to load any additional content from the database to complete its definition. The return value from the function is an array of additional content to be merged into the node data structure. In our case, we need to load the three data items, the abstract, publish date, and expiration date from the new table. Listing 14 shows the code to load this additional information for our announcement node.


Listing 14. Announcement_load hook for loading a node of type announcement
function announcement_load(&$node) {
   $additions = db_fetch_object(db_query('SELECT * FROM {announcement} ' .
                                         'WHERE nid = %d', $node->nid));
   return $additions;
}

hook_insert

When an announcement node is created on the Web site, as shown in Listing 15, the insert hook is automatically invoked. This hook provides the opportunity for a new module to store any additional information into the database when a node is first created. For the announcement module, we create a new entry in the announcement table. The node object passed into the function contains all the data from the input forms. The publish and expiration dates are returned as an array with the month, day, and year. This is converted with the local function _announcement_drupaldate2unixtime. By convention, all local module functions are preceded by an underscore ("_") and the module name, such as _announcement. Then, we call the database abstract layer to insert the new row into the announcement table. The $node->nid is the primary key that links the announcement table to the node table.


Listing 15. Announcement_insert hook and support function for adding an announcement to the database
function _announcement_drupaldate2unixtime($drupal_date) {
	$year  = $drupal_date["year"];
	$month = $drupal_date["month"];
	$day   = $drupal_date["day"];
	return mktime(0,0,0, (int)$month, (int)$day, (int)$year);
}

function announcement_insert($node) {
 $publish_date    = _announcement_drupaldate2unixtime($node->publish_date);
 $expiration_date = _announcement_drupaldate2unixtime($node->expiration_date);
 db_query("INSERT INTO {announcement} (nid, abstract, publish_date, expiration_date) ".
          "VALUES (%d, '%s', '%d', '%d')", 
          $node->nid, $node->abstract, $publish_date, $expiration_date);
}

hook_update

The update hook Listing 16 is called when the information already exists in the database and the user is editing it. This is similar to the insert hook, but the database command issued is UPDATE.


Listing 16. Announcement_update hook to modify an existing announcement node
function announcement_update($node) {
   $publish_date    = _announcement_drupaldate2unixtime($node->publish_date);
   $expiration_date = _announcement_drupaldate2unixtime($node->expiration_date);

   db_query("UPDATE {announcement} SET abstract='%s', publish_date = '%s', " .
            "expiration_date = '%s' WHERE nid = %d", 
            $node->abstract, $publish_date, $expiration_date, $node->nid);
}

hook_delete

Finally, the delete hook (shown in Listing 17) is called when the user deletes an announcement node. This gives the module an opportunity to delete any auxiliary content from other tables in the database. Here we delete the one row of the announcement table that is associated with the node based on the nid.


Listing 17. Announcement_delete hook to delete an announcement node
function announcement_delete($node) {
   db_query('DELETE FROM {announcement} WHERE nid = %d', $node->nid);
}

hook_cron

The cron hook allows modules to schedule tasks to be run at regular intervals. The intervals can be set by the site administrator through a cron job that does a HTTP GET on http://<sitename.com>/cron.php. This invokes the defined cron hook on all modules. The status of the cron job can be seen in the administrators interface under Administer > Settings (for example, /admin/settings) in the cron jobs section.

The announcement module relies on the publish and expiration dates to determine if an announcement should be visible. However, an announcement could still be displayed through the standard node mechanism (/node/id/view) if its node status is not set to 0. We use the cron hook to set the status flag on all announcement nodes that have expired. Listing 18 shows the announcement_cron function that first queries the database for those announcements past their expiration dates and then sets the node status to 0 for those nodes.


Listing 18. Announcement_cron hook implementation
function announcement_cron() {
   $queryResult = db_query("UPDATE {node} AS n INNER JOIN {announcement} AS a " .
       "ON n.nid = a.nid SET n.status = 0 WHERE n.type='announcement' " .
       "AND n.status = 1 AND a.expiration_date < %d", time());
}

hook_search

The search hook allows a module to extend the search page with the ability to perform keyword searches on nodes the module creates. First the search module needs to be enabled through the administer > modules page. This lets us include the search block in the header using the administer > block page. By using the search hook, another tab is rendered in the search page when a simple search is presented. This search form is then used to look for keywords within nodes of the type created by your module.

The search module uses cron to build a table of indices of data found within the node, which allows Drupal to provide full-text searching against the node content.

For the announcements module, we want the search engine to pick up the new abstract field defined in the announcements table. We also want to change the action of the default search so that this abstract field is shown instead of the default content, and we don't want to use a separate tab for its presentation. Luckily, there is another alternative to the search hook for our needs.

hook_nodeapi

To make sure the default search form can find keywords within the abstract field in the announcements table, we implemented the nodeapi hook function. This lets us include this field during the update of the indices. Listing 19 shows our implementation of the nodeapi hook.


Listing 19. Announcement_nodeapi hook implementation
function announcement_nodeapi(&$node, $op) {
   switch ($op) {
      case 'update index':
         if ($node->type == 'announcement') {
            $text = ''; 
            $q = db_query('SELECT a.abstract FROM node n LEFT JOIN announcement a ' .
                          'ON n.nid = a.nid WHERE n.nid = %d', $node->nid);
		if ($r = db_fetch_object($q)) {
		   $text = $r->abstract;
		}
		return $text;
		}
	}
}

Within this function, we check for the update index operation indicating that Drupal is at the point of collecting extra data before indexing it in the database. If the node being indexed is of type announcement, the associated abstract field value is extracted from the announcement table and returned.

Now that Drupal can index this announcement abstract, we need to display this information, if it matches a keyword search, on the default search page results. We did this by overriding the theme_search_item function in the search module using the phptemplate_search_item function, as shown in Listing 20. Since this function can be considered a global theme change, we place it in the template.php file that sits in our theme directory.


Listing 20. phptemplate_search_item function
function phptemplate_search_item($item, $type) {
   return _phptemplate_callback('search_item', 
      array('node' => $item), 'search_item-' . strtolower($item['type']));
}

In this function we use the _phptemplate_callback function to associate the theming of the search item to a suggested template file. Rather like the way the phptemplate engine allows the use of node.tpl.php and node-<node-type>.tpl.php template files to customize the way a node is rendered, we are using this function to allow the use of search_item.tpl.php and search_item-<node-type>.tpl.php templates, as shown in Listing 21.

Now we can have a template for the default look of a search item, which is essentially a slightly modified version of the original themed search item found in the theme_search_item function in the search.module file. This useful technique could be applied to any themed entity.


Listing 21. search_item.tpl.php template
<dt class="title search_item">
  <a href="<?php print check_url($node['link']); ?>"><?php print 
     check_plain($node['title']) ?></a>
</dt>
<?php
$info = array();
if ($node['type']) $info[] = strtolower($node['type']);
if ($node['user']) $info[] = $node['user'];
if ($node['date']) $info[] = format_date($node['date'],'small');
if (is_array($node['extra'])) $info = array_merge($info, $node['extra']);
?>
<dd class="search_item">
	<p><?php print $node['snippet']; ?></p>
	<p class="search-info"><?php print implode(' - ', $info); ?></p>
</dd>

Using a search_item-announcement.tpl.php template we can theme the search item for an announcement type node and replace the default snippet with our own constructed abstract field. In Listing 22 notice we used the search_excerpt function to highlight the keywords in the abstract.


Listing 22. search_item-announcement.tpl.php template
<dt class="title search_item_announcement">
	<a href="<?php print check_url($node['link']); ?>">
	<?php print check_plain($node['title']) ?></a>
</dt>
<?php
$info = array();
if ($node['type']) $info[] = strtolower($node['type']);
if ($node['user']) $info[] = $node['user'];
if ($node['date']) $info[] = format_date($node['date'],'small');
if (is_array($node['extra'])) $info = array_merge($info, $node['extra']);
?>
<dd class="search_item_announcement">
	<p><?php print ($node['node']->abstract ? '<p>'. 
	search_excerpt(search_get_keys(),$node['node']->abstract) . 
	'</p>' : $node['snippet']); ?></p>
	<p class="search-info"><?php print implode(' - ', $info); ?></p>
</dd>


Figure 10. Search results page with search terms highlighted in output
Figure 10. Search results page with search terms highlighted in output

hook_node_info

Listing 23 describes a function that allows node modules to define multiple custom node types, the node_info hook. We use this function to tell Drupal to define the announcement node type. There are two values Drupal needs to associate with the node type; the human readable name of the node type, used in the user interface; and the base, a prefix used for functions that are associated with this node type. If we wanted to add another node type, we could add another array item.


Listing 23. Announcement_node_info to determine name and attributes of module's node types
function announcement_node_info() {
 return array('announcement' => array('name' => 'Announcement', 
                                      'base' => 'announcement'));
}



Theming the module output

Module files provide several theme functions that are used to display the module's information in different contexts. For most purposes, we find it useful to think of these contexts in the following way:

  • A detailed layout where all the information for a given node is presented
  • A compact or summary representation where only the salient parts are presented to give an overview of the node
  • A block that presents a smaller form of the node information that is typically embedded into the left or right sidebar

For the announcements module, we used a theme function for each of these contexts:

  • An announcement page used the theme_announcement function to create a detailed layout.
  • The front or home page of the site used the theme_announcement_compact function to create a list of announcements.
  • The announcement block in the right sidebar on all pages used the theme_announcement_block_list function.

As explained in Part 5: Getting started with Drupal, the theme functions can be used to create the default look of the announcement module output, regardless of the theme engine selected.

Since we use the phptemplate engine, we override these default theme functions, shown in Listing 24, using the phptemplate_announcement, phptemplate_announcement_compact, and phptemplate_announcenemt_block_list functions. We found it useful to separate the structure and style definitions from the logic of the module, and use the _phptemplate_callback function to associate a template file to each context.


Listing 24. Default theme functions and the PHP Template engine functions that override them
function theme_announcement($announcement) {
	// Put your default theme for the announcement detail here
	return '';
}

function theme_announcement_compact($announcement) {
	// Put your default theme for the announcement summary here
	return '';
}

function theme_announcement_block_list($announcement_list) {
	// Put your default theme for the announcement block here
	return '';
}

function phptemplate_announcement($announcement) {
  return _theme_phptemplate_announcement($announcement, 'announcement');
}

function phptemplate_announcement_compact($announcement) {
  return _theme_phptemplate_announcement($announcement, 'announcement_compact');
}

function phptemplate_announcement_block_list($announcement_list) {
	global $user;
  return _phptemplate_callback('announcement_block_list', 
                                    array('announcements' => $announcement_list, 
                                    'user' => $user));	
}

function _theme_phptemplate_announcement($announcement, $announcement_template) {
   $expired = FALSE;
   if ($announcement->expiration_date < time()) {
      $expired = TRUE;		
   }
   $variables = array(
      'title'     => $announcement->title,
      'body'      => $announcement->body,
      'links'     => $announcement->links ? 
                           theme('links', $announcement->links) : '',
      'abstract'  => $announcement->abstract,
      'published' => format_date($announcement->publish_date,'custom','j M, Y'),
      'expires'   => format_date($announcement->expiration_date,'custom','j M, Y'),
      'expired'  => $expired,
      'node'      => $announcement
   );
   return _phptemplate_callback($announcement_template, $variables);	
}

Notice the use of a helper function, _theme_phptemplate_announcement, which provides a common way to prepare variables passed to the selected template file.

We've described a simple theming example for our custom announcement module. In a subsequent article, we will discuss the styling of nodes in more detail.


Summary

In this article, you learned about the implementation of a simple custom module, the announcement module, for a Web site. This module provides announcements that automatically appear and then disappear from the Web site based on publication and expiration dates. Announcements appear in the main area of the home page and in the sidebar on all other pages. Many of the core functions, or hooks, are used to provide a functioning module.

In upcoming articles in this series, you'll get more detail about the styling of our fictional Web site, as well as the interface to SQL and the database abstraction layer.


Resources

Learn

Get products and technologies

Discuss

About the authors

Alister's photo

Alister Lewis-Bowen is a senior software engineer in IBM's Internet Technology Group. He has worked on Internet and Web technologies as an IBM UK employee since 1993. Alister was brought to the U.S. to work on the Web sites for the IBM-sponsored sports events, then as senior Webmaster for ibm.com. He is currently helping create semantic Web prototypes.

Stephen's photo

Stephen Evanchik is a software engineer in IBM's Internet Technology Group. He has been a contributor to many open source software projects, the most notable being his IBM TrackPoint driver in the Linux kernel. Stephen is currently working with emerging semantic Web technologies.

Louie's photo

Louis Weitzman is a senior software engineer in IBM's Internet Technology Group. For 30 years he has worked at the intersection of design and computation. He helped develop an XML, fragment-based content management system in use by ibm.com, and currently is involved with bringing the design process to emerging projects.

Comments (Undergoing maintenance)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Sample IT projects, Open source
ArticleID=158787
ArticleTitle=Using open source software to design, develop, and deploy a collaborative Web site, Part 6: Building a custom module in Drupal
publish-date=09122006
author1-email=alister.lewisbowen@gmail.com
author1-email-cc=
author2-email=evanchsa@gmail.com
author2-email-cc=
author3-email=louis.weitzman@gmail.com
author3-email-cc=

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).