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.
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
Developing a Drupal content module requires several steps:
- 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.
- 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.
- Write an .info file to identify the module and its purpose. This metadata defines prerequisites, the proper name of the module, and more.
- Write the code for the module, including a module installer, a data-entry form, and a display template.
- 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
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 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
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.
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
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
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
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.
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
-
Check out the Drupal Schema API.
-
Check out the Drupal Form API.
-
Browse the Drupal modules list by name or category.
-
Download the Drupal Administration menu.
-
Innovate your next open source development project with IBM trial software, available for download or on DVD.
- Download
IBM product evaluation versions
or explore
the online trials in the IBM SOA Sandbox and get your hands on application development tools and middleware products from
DB2®, Lotus®, Rational®, Tivoli®, and WebSphere®.
Discuss
-
Participate in developerWorks blogs and get involved in the developerWorks community.





