Tutorial on writing modules.

This is my first tutorial written for the Drupal site. It took me over a day to do, but it was a good learning experience. I can't say it's perfect, but at least fewer people will have to suffer through my pain after this gets through the moderation queue on the site. And, frustratingly enough, it looks like crap on my site. Argh.

Introduction

This tutorial describes how to create a module for Drupal-CVS (i.e. Drupal version > 4.3.1). A module is a collection of functions that link into Drupal, providing additional functionality to your Drupal installation. After reading this tutorial, you will be able to create a basic block module and use it as a template for more advanced modules and node modules.

This tutorial will not necessarily prepare you to write modules for release into the wild. It does not cover caching, nor does it elaborate on permissions or security issues. Use this tutorial as a starting point, and review other modules and the Drupal handbook and Coding standards for more information.

This tutorial assumes the following about you:

  • Basic PHP knowledge, including syntax and the concept of PHP objects
  • Basic understanding of database tables, fields, records and SQL statements
  • A working Drupal installation
  • Drupal administration access and webserver access

This tutorial does not assume you have any knowledge about the inner workings of a Drupal module. This tutorial will not help you write modules for Drupal 4.3.1 or before.

Getting Started

To focus this tutorial, we'll start by creating a block module that lists links to content such as blog entries or forum discussions that were created one week ago. The full tutorial will teach us how to create block content, write links, and retrieve information from Drupal nodes.

Start your module by creating a PHP file and save it as 'onthisdate.module'.

<?php

?>

As per the Coding standards, use the longhand <?php tag, and not <? to enclose your PHP code.

All functions in your module are named {modulename)_{hook}, where "hook" is a well defined function name. Drupal will call these functions to get specific data, so having these well defined names means Drupal knows where to look.

Telling Drupal about your module

The first function we'll write will tell Drupal information about your module: its name and description. The hook name for this function is 'help', so start with the onthisdate_help function:

function onthisdate_help($section) {

}

The $section variable provides context for the help: where in Drupal or the module are we looking for help. The recommended way to process this variable is with a switch statement. You'll see this code pattern in other modules.

/* Commented out until bug fixed */
/*
function onthisdate_help($section) {
  switch($section) {
    case "admin/system/modules#name":
      return "onthisdate";
      break;
    case "admin/system/modules#description":
      return t("Display a list of nodes that were created a week ago.");
      break;
  }
}
*/

You will eventually want to add other cases to this switch statement to provide real help messages to the user. In particular, output for "admin/help#onthisdate" will display on the main help page accessed by the admin/help URL for this module (/admin/help or ?q=admin/help).

The t() function in the second case is used to provide localized content to the user. Any string that presents information to the user should be enclosed in at t() call so that it can be later translated.

Note:This function is commented out in the above code. This is on purpose, as the current version of Drupal CVS won't display the module name, and won't enable it properly when installed. Until this bug is fixed, comment out your help function, or your module may not work.

Telling Drupal who can use your module

The next function to write is the permissions function. Here, you can tell Drupal who can access your module. At this point, give permission to anyone who can access site content or administrate the module.

function onthisdate_perm() {
  return array("administer onthisdate");
}

If you are going to write a module that needs to have finer control over the permissions, and you're going to do permission control, you may want to define a new permission set. You can do this by adding strings to the array that is returned:

function onthisdate_perm() {
  return array("access onthisdate", "administer onthisdate");
}

You'll need to adjust who has permission to view your module on the administer » accounts » permissions page. We'll use the user_access() function to check access permissions later.

Be sure your permission strings must be unique to your module. If they are not, the permissions page will list the same permission multiple times.

Announce we have block content

There are several types of modules: block modules and node modules are two. Block modules create abbreviated content that is typically (but not always, and not required to be) displayed along the left or right side of a page. Node modules generate full page content (such as blog, forum, or book pages).

We'll create a block content to start, and later discuss node content. A module can generate content for blocks and also for a full page (the blogs module is a good example of this). The hook for a block module is appropriately called "block", so let's start our next function:

function onthisdate_block($op='list', $delta=0) {
  
}

The block function takes two parameters: the operation and the offset, or delta. We'll just worry about the operation at this point. In particular, we care about the specific case where the block is being listed in the blocks page. In all other situations, we'll display the block content.

function onthisdate_block($op='list', $delta=0) {

  // listing of blocks, such as on the admin/system/block page
  if ($op == "list") {
    $block[0]["info"] = t("On This Date");
    return $block;
  } else {
  // our block content
  }
}

Generate content for a block

Now, we need to generate the 'onthisdate' content for the block. In here, we'll demonstrate a basic way to access the database.

Our goal is to get a list of content (stored as "nodes" in the database) created a week ago. Specifically, we want the content created between midnight and 11:59pm on the day one week ago. When a node is first created, the time of creation is stored in the database. We'll use this database field to find our data.

First, we need to calculate the time (in seconds since epoch start, see http://www.php.net/manual/en/function.time.php for more information on time format) for midnight a week ago, and 11:59pm a week ago. This part of the code is Drupal independent, see the PHP website (http://php.net/) for more details.

function onthisdate_block($op='list', $delta=0) {

  // listing of blocks, such as on the admin/system/block page
  if ($op == "list") {
    $block[0]["info"] = t("On This Date");
    return $block;
  } else {
  // our block content

    // Get today's date
    $today = getdate();

    // calculate midnight one week ago
    $start_time = mktime(0, 0, 0, 
                         $today['mon'], ($today['mday'] - 7), $today['year']);

    // we want items that occur only on the day in question, so calculate 1 day
    $end_time = $start_time + 86400;  // 60 * 60 * 24 = 86400 seconds in a day
    ...
  }
}

The next step is the SQL statement that will retrieve the content we'd like to display from the database. We're selecting content from the node table, which is the central table for Drupal content. We'll get all sorts of content type with this query: blog entries, forum posts, etc. For this tutorial, this is okay. For a real module, you would adjust the SQL statement to select specific types of content (by adding the 'type' column and a WHERE clause checking the 'type' column).

Note: the table name is enclosed in curly braces: {node}. This is necessary so that your module will support database table name prefixes. You can find more information on the Drupal website by reading the Table Prefix (and sharing tables across instances) page in the Drupal handbook.

  $query = "SELECT nid, title, created FROM {node} WHERE created >= %d AND created <= %d", $start_time, $end_time);

Drupal uses database helper functions to perform database queries. This means that, for the most part, you can write your database SQL statement and not worry about the backend connections.

We'll use db_query() to get the records (i.e. the database rows) that match our SQL query, and db_fetch_object() to look at the individual records:


  // get the links
  $queryResult =  db_query($query);

  // content variable that will be returned for display
  $block_content = '';

  while ($links = db_fetch_object($queryResult)) {
    $block_content .= '<a href="' . url('node/view/' . $links->nid ) . '">' . 
                       $links->title . '</a><br />';
  }

  // check to see if there was any content before setting up the block
  if ($block_content == '') {
    /* No content from a week ago.  If we return nothing, the block 
     * doesn't show, which is what we want. */
    return;
  }

  // set up the block
  $block['subject'] = 'On This Date';
  $block['content'] = $block_content;
  return $block;
}

Notice the actual URL is enclosed in the url() function. This adjusts the URL to the installations URL configuration of either clean URLS: http://sitename/node/view/2 or http://sitename/?q=node/view/2

Also, we return an array that has 'subject' and 'content' elements. This is what Drupal expects from a block function. If you do not include both of these, the block will not render properly.

You may also notice the bad coding practice of combining content with layout. If you are writing a module for others to use, you will want to provide an easy way for others (in particular, non-programmers) to adjust the content's layout. An easy way to do this is to include a class attribute in your link, and not necessarily include the <br /> at the end of the link. Let's ignore this for now, but be aware of this issue when writing modules that others will use.

Putting it all together, our block function looks like this:

function onthisdate_block($op='list', $delta=0) {

  // listing of blocks, such as on the admin/system/block page
  if ($op == "list") {
    $block[0]["info"] = t("On This Date");
    return $block;
  } else {
  // our block content

    // content variable that will be returned for display
    $block_content = '';

    // Get today's date
    $today = getdate();

    // calculate midnight one week ago
    $start_time = mktime(0, 0, 0, 
                         $today['mon'], ($today['mday'] - 7), $today['year']);

    // we want items that occur only on the day in question, so calculate 1 day
    $end_time = $start_time + 86400;  // 60 * 60 * 24 = 86400 seconds in a day

    $query = "SELECT nid, title, created FROM {node} WHERE created >= %d AND created <= %d", $start_time, $end_time);

    // get the links
    $queryResult =  db_query($query);

    while ($links = db_fetch_object($queryResult)) {
      $block_content .= '<a href="'.url('node/view/'.$links->nid).'">'. 
                        $links->title . '</a><br />';
    }

    // check to see if there was any content before setting up the block
    if ($block_content == '') {
      // no content from a week ago, return nothing.
      return;
    }

    // set up the block
    $block['subject'] = 'On This Date';
    $block['content'] = $block_content;
    return $block;
  }
}

Installing, enabling and testing the module

At this point, you can install your module and it'll work. Let's do that, and see where we need to improve the module.

To install the module, you'll need to copy your onthisdate.module file to the modules directory of your Drupal installation. The file must be installed in this directory or a subdirectory of the modules directory, and must have the .module name extension.

Log in as your site administrator, and navigate to the modules administration page to get an alphabetical list of modules. In the menus: administer » configuration » modules, or via URL:

    http://.../admin/system/modules or http://.../?q=admin/system/modules

Note: You'll see one of three things for the 'onthisdate' module at this point:

  • You'll see the 'onthisdate' module name and no description
  • You'll see no module name, but the 'onthisdate' description
  • You'll see both the module name and the description

Which of these three choices you see is dependent on the state of the CVS tree, your installation and the help function in your module. If you have a description and no module name, and this bothers you, comment out the help function for the moment. You'll then have the module name, but no description. For this tutorial, either is okay, as you will just enable the module, and won't use the help system.

Enable the module by selecting the checkbox and save your configuration.

Because the module is a blocks module, we'll need to also enable it in the blocks administration menu and specify a location for it to display. Navigate to the blocks administration page: admin/system/block or administer » configuration » blocks in the menus.

Enable the module by selecting the enabled checkbox for the 'On This Date' block and save your blocks. Be sure to adjust the location (left/right) if you are using a theme that limits where blocks are displayed.

Now, head to another page, say select the module. In some themes, the blocks are displayed after the page has rendered the content, and you won't see the change until you go to new page.

If you have content that was created a week ago, the block will display with links to the content. If you don't have content, you'll need to fake some data. You can do this by creating a blog, forum topic or book page, and adjust the "Authored on:" date to be a week ago.

Alternately, if your site has been around for a while, you may have a lot of content created on the day one week ago, and you'll see a large number of links in the block.

Create a module configuration (settings) page

Now that we have a working module, we'd like to make it better. If we have a site that has been around for a while, content from a week ago might not be as interesting as content from a year ago. Similarly, if we have a busy site, we might not want to display all the links to content created last week. So, let's create a configuration page for the administrator to adjust this information.

The configuration page uses the 'settings' hook. We would like only administrators to be able to access this page, so we'll do our first permissions check of the module here:

function onthisdate_settings() {
  // only administrators can access this module
  if (!user_access("admin onthisdate")) {
    return message_access();
  }
}

If you want to tie your modules permissions to the permissions of another module, you can use that module's permission string. The "access content" permission is a good one to check if the user can view the content on your site:

  ... 
  // check the user has content access
  if (!user_access("access content")) {
    return message_access();
  }
  ...

We'd like to configure how many links display in the block, so we'll create a form for the administrator to set the number of links:

function onthisdate_settings() {
  // only administrators can access this module
  if (!user_access("admin onthisdate")) {
    return message_access();
  }

  $output .= form_textfield(t("Maximum number of links"), "onthisdate_maxdisp",
             variable_get("onthisdate_maxdisp", "3"), 2, 2,
             t("The maximum number of links to display in the block."));

  return $output;
}

This function uses several powerful Drupal form handling features. We don't need to worry about creating an HTML text field or the form, as Drupal will do so for us. We use variable_get to retrieve the value of the system configuration variable "onthisdate_maxdisp", which has a default value of 3. We use the form_textfield function to create the form and a text box of size 2, accepting a maximum length of 2 characters. We also use the translate function of t(). There are other form functions that will automatically create the HTML form elements for use. For now, we'll just use the form_textfield function.

Of course, we'll need to use the configuration value in our SQL SELECT. Because different databases have slightly different ways of limiting the amount of data returned, Drupal provides a database independent function to query the database: db_query_range. Get the saved maximum number and use db_query_range():

  $limitnum = variable_get("onthisdate_maxdisp", 3);

  $query = "SELECT nid, title, created FROM {node} WHERE created >= %d AND created <= %d", $start_time, $end_time);

  // get the links, limited to just the maxium number:
  $queryResult =  db_query($query, 0, $limitnum);

You can test the settings page by editing the number of links displayed and noticing the block content adjusts accordingly.

Navigate to the settings page: admin/system/modules/onthisdate or administer » configuration » modules » onthisdate. Adjust the number of links and save the configuration. Notice the number of links in the block adjusts accordingly.

Note:We don't have any validation with this input. If you enter "c" in the maximum number of links, you'll break the block.

Adding menu links and creating page content

So far we have our working block and a settings page. The block displays a maximum number of links. However, there may be more links than the maximum we show. So, let's create a page that lists all the content that was created a week ago.

function onthisdate_all() {
  
}

We're going to use much of the code from the block function. We'll write this ExtremeProgramming style, and duplicate the code. If we need to use it in a third place, we'll refactor it into a separate function. For now, copy the code to the new function onthisdate_all(). Contrary to all our other functions, 'all', in this case, is not a Drupal hook. We'll discuss below.

function onthisdate_all() {

  // content variable that will be returned for display
  $page_content = '';

  // Get today's date
  $today = getdate();

  // calculate midnight one week ago
  $start_time = mktime(0, 0, 0, 
                       $today['mon'], ($today['mday'] - 7), $today['year']);

  // we want items that occur only on the day in question, so calculate 1 day
  $end_time = $start_time + 86400;  // 60 * 60 * 24 = 86400 seconds in a day

  // NOTE!  No LIMIT clause here!  We want to show all the code
  $query = "SELECT nid, title, created FROM " . 
           "{node} WHERE created >= '" . $start_time . 
           "' AND created <= '". $end_time . "'";

  // get the links
  $queryResult =  db_query($query);

  while ($links = db_fetch_object($queryResult)) {
    $page_content .= '<a href="'.url('node/view/'.$links->nid).'">'. 
                      $links->title . '</a><br />';
  }
  
  ...
}

We have the page content at this point, but we want to do a little more with it than just return it. When creating pages, we need to send the page content to the theme for proper rendering. We use this with the theme() function. Themes control the look of a site. As noted above, we're including layout in the code. This is bad, and should be avoided. It is, however, the topic of another tutorial, so for now, we'll include the formatting in our content:

    print theme("page", $content_string);

The rest of our function checks to see if there is content and lets the user know. This is preferable to showing an empty or blank page, which may confuse the user.

Note that we are responsible for outputting the page content with the 'print theme()' syntax. This is a change from previous 4.3.x themes.

function onthisdate_all() {

  ...

  // check to see if there was any content before setting up the block
  if ($page_content == '') {
    // no content from a week ago, let the user know 
    print theme("page", 
                "No events occurred on this site on this date in history.");
    return;
  }

  print theme("page", $page_content);
}

Letting Drupal know about the new function

As mentioned above, the function we just wrote isn't a 'hook': it's not a Drupal recognized name. We need to tell Drupal how to access the function when displaying a page. We do this with the _link hook and the menu() function:

function onthisdate_link($type, $node=0) {

}

There are many different types, but we're going to use only 'system' in this tutorial.

function onthisdate_link($type, $node=0) {
  if (($type == "system")) { 
    // URL, page title, func called for page content, arg, 1 = don't disp menu
    menu("onthisdate", t("On This Date"), "onthisdate_all", 1, 1);
  }
}

Basically, we're saying if the user goes to "onthisdate" (either via ?q=onthisdate or http://.../onthisdate), the content generated by onthisdate_all will be displayed. The title of the page will be "On This Date". The final "1" in the arguments tells Drupal to not display the link in the user's menu. Make this "0" if you want the user to see the link in the side navigation block.

Navigate to /onthisdate (or ?q=onthisdate) and see what you get.

Adding a more link and showing all entries

Because we have our function that creates a page with all the content created a week ago, we can link to it from the block with a "more" link.

Add these lines just before that $block['subject'] line, adding this to the $block_content variable before saving it to the $block['content'] variable:

  // add a more link to our page that displays all the links
   $block_content .= "<div class=\"more-link\">". l(t("more"), "onthisdate", array("title" => t("More events on this day."))) ."</div>";

This will add the more link.

And we're done!

We now have a working module. It created a block and a page. You should now have enough to get started writing your own modules. We recommend you start with a block module of your own and move onto a node module. Alternately, you can write a filter or theme.

Please see the Drupal Handbook for more information.

Further Notes

As is, this tutorial's module isn't very useful. However, with a few enhancements, it can be entertaining. Try modifying the select query statement to select only nodes of type 'blog' and see what you get. Alternately, you could get only a particular user's content for a specific week. Instead of using the block function, consider expanding the menu and page functions, adding menus to specific entries or dates, or using the menu callback arguments to adjust what year you look at the content from.

If you start writing modules for others to use, you'll want to provide more details in your code. Comments in the code are incredibly valuable for other developers and users in understanding what's going on in your module. You'll also want to expand the help function, providing better help for the user. Follow the Drupal Coding standards, especially if you're going to add your module to the project.

Two topics very important in module development are writing themeable pages and writing translatable content. We touched briefly on both of these topics with the theme() and t() calls in various parts of the module. Please check the Drupal Handbook for more details on these two subject.