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.