Drupal is a powerful CMS right out of the box. But let's be honest. The real reason that Drupal has gained such traction in the crowded PHP CMS space isn't because it is loaded with bells and whistles, but because it can be extended in seemingly infinite ways through add-on modules. In fact, there are well over seven thousand modules available from Drupal.org. Why is it that Drupal enjoys such a wealth of user-contributed add-on code?
The answer is that Drupal's API (Application Programmer Interface) provides the golden combination of simplicity and power. Almost every phase of Drupal's page building process can be intercepted and the data modified. But powerful modules can still be written in just a few dozen lines of code.
In the next few pages we are going to create a module from scratch. We will build a module using the Block API – the system used to generate block content. The module we create in this article is going to provide a block that displays the number of members (user accounts) on the site.
This is how we will proceed:
- We will create a new module directory
- Then we will create a .info (dot-info) file to describe the module
- From here, we will create our basic .module (dot-module) file and introduce Drupal programming
- Next, we will create a couple of “block hooks” to define the behaviors of our new block
- Using the administration interface, we will turn on our new module and enable the block
Prerequisites
For this article, it is assumed that you have the following:
- Basic PHP development knowledge
- A running Drupal 7 instance with admin access
- Access to the filesystem for your Drupal 7 instance
- A code editor or PHP IDE
To get started, you will need a working Drupal 7 instance that you can access as an administrator. You will also need to be able to create files and directories inside of that Drupal installation. The examples in this article are done on a standard install of Drupal 7.0 with no contributed modules.
Creating a Module
Typically, a Drupal module has at least these three pieces:
- A module directory that contains all module files
- An information file
- A module PHP code file
We will call our module membercount. In Drupal parlance, this name is called the machine name because it is the name Drupal (the machine) uses to reference the module.
Now that we have the machine name, we can create our module's directory. The default location for add-on modules is the sites/all/modules directory. In that directory, we will create a folder named with the module's machine name: membercount/.
Inside of this directory we will create two files. The first file, which we will see in the following section, will contain information about our module. The second one will contain the module's PHP source code.
The .info File
The first file in our module is the .info file. Drupal uses the .info file to determine details about the module, including whether the module can be installed and what other modules it depends upon. A .info file is always named with the machine name of the module, so our module must be named membercount.info.
The .info file contains a list of name/value pairs. This file isn't an ad hoc settings file where you can put any information that you wish. There are a dozen directives that can be placed inside of a module. We will be using only the few that are required.
description = Provides a block that shows the number of members.
core = 7.x
The name directive is required in all .info files. Its purpose is to provide a human-readable name for the module.
The description directive should have a one-sentence description of what the module does.
The next directive, core, has a functional role. Drupal refers to this to make sure that the module can be installed. Attempting to install this module on a Drupal 6 system, for example, would generate an error.
All three of these directives are required.
To see all .info directives refer to http://drupal.org/node/542202.
Starting the .module File
The next file that we are going to create is the .module file, membercount.module. This file is automatically loaded by Drupal, which expects the file to contain PHP code. Like .info files, the base name of a .module file is the module's machine name.
For the most part, Drupal modules are written as procedural code. Typically, the preferred unit of organization for Drupal code is the function. Our module file will have a three functions in it. Here is the first:
/**
* @file
* The membercount module provides a block with member count information.
*/
/**
* Implementation of hook_help().
*/
function membercount_help($path, $args) {
if ($path == 'admin/help#membercount') {
return t('To use this module, go to the block page and turn on the membercount block.');
}
}
There are a couple things that I will note in passing, but which we don't have the space to examine more closely.
The first is the <?php processor instruction. This is required for any PHP files. Note, however, that there is no closing ?>. This is not only legal in PHP, but is considered a best practice for a file that is entirely source code. It prevents errors caused by premature buffer flushes.
The second thing is that it follows a very strict set of coding conventions. These conventions, described at http://drupal.org/node/360052, cover everything from indent depth (two spaces, no tabs) to naming conventions (lowercase with underscores), operators (one space on each side), and comment styles. Drupal developers are expected to follow these conventions in order to maximize the readability across the codebase.
The next thing of note is the use of documentation blocks. Documentation blocks are a special kind of comment that begins with /** and ends with */. The purpose of these sections is to provide source code documentation that can be extracted by automated tools. Every PHP file should have a file-level documentation block (which is flagged with the @file tag). Each function should also have a documentation block.
Now we are ready to look at the function declared in the file above. The membercount_help() function provides help text for the module. By declaring this help function as above it is now available to Drupal. If we were to install the module as it is and go visit the help page, we would be able to see this help text under the Member Count module.
Drupal has picked up our help function and integrated it into its help system. How did it do this?
This is where Drupal's cornerstone feature comes into play. Drupal uses a callback-like system called hooks. At strategic times during page processing, Drupal looks for any functions that follow a specific naming pattern (hook functions) and it invokes all matching functions. For example, when generating help text Drupal looks for anything that implements hook_help(). It then executes all of the matching functions, gathers the returned data, and displays the requested help page. To implement hook_help(), all we need to do is declare a function where the hook part of the name is replaced with our module's machine name: membercount_help().
Declare a function according to a predefined hook pattern and Drupal will just pick it up and use it. This is why Drupal is so easy to extend.
Of course there is a little more to it then that. You need to know what each hook invocation will give as input, and what output your hook is expected to return. In our example above, hook_help() implementations will get two arguments:
- $path: The help path. A top level help screen will always get the path admin/help#MODULENAME.
- $arg: Additional arguments passed in the URL.
The hook will be executed for any call to the help system, so it is up to us to determine whether the help call is one that we should answer. That is why the body of our help function looks like this:
return t('To use this module, go to the block page and turn on the membercount block.');
}
In short, we only answer this request for help if the $path is set to 'admin/help#membercount'. In a more complex module we might have an entire help system where different paths contained information about different parts of the system.
Any request to hook_help() expects one of two things to be returned: a NULL value or a string of help text. While NULLs are ignored, strings are prepared and displayed. In practice, modules should have informative help text, but for the sake of illustration we have kept ours very short.
Notice that the string isn't simply returned. It's passed through the t() function. The t() function is primarily for translating strings into non-English languages. But t() performs other tasks as well. It can be used to insert values into strings (like the sprintf() family of functions) and to do some output filtering before data is sent to the client. The t() function is documented at http://api.drupal.org/api/drupal/includes--bootstrap.inc/function/t/7.
Five other Drupal functions you ought to know
The article introduces several important Drupal functions. Here are a few other commonly used Drupal functions.
- theme(): Pass data to the theme system for formatting
- l(): Transform a label and URI into a link
- module_invoke_all(): Execute a hook in all modules
- drupal_set_message(): Print a notice or error to the user
- watchdog(): Log a message to the Drupal log
Any string that contains output that is on its way to a user should be passed through the t() function. And this should happen when the string is declared. In other words, the following code is wrong:
$foo = 'This is my string.';
$bar = t($foo);In order for the translation system to function correctly, t() should always be used like this:
$bar = t('This is my string.');In the next section we are going to create a block.
Block Hooks
In Drupal, the block system is used for placing chunks of content around the periphery of the page. Blocks are administered through the Toolbar » Structure » Blocks page, where they can be positioned and configured.
Our module is going to provide a simple block that lists the number of accounts that exist on our site. To accomplish this, we are going to need to implement two hooks:
- hook_block_info(): Tell Drupal about the block that we declare.
- hook_block_view(): Answer a request to return a block's data.
There are other block hooks that we could implement, but for this example we can get by with just these two.
The first hook provides the information necessary to tell Drupal about all of the blocks that this module provides. Since our module only provides one block, our hook implementation is simple:
* Implements hook_block_info().
*/
function membercount_block_info() {
$blocks = array();
$blocks['count_members'] = array(
'info' => t('Count Members'),
'cache' => DRUPAL_NO_CACHE,
);
return $blocks;
}
This hook provides the settings for each block that the module declares. The block info hook takes no parameters and returns an associative array where the keys are the machine names of each block, and the values are an associative array of settings for each block.
Our module declares one block called count_members. And we are assigning it two settings:
- info: This is required for every block. It should contain a short phrase describing the block. It is displayed only on block administration pages.
- cache: This second flag uses one of the DRUPAL_ cache constants to tell Drupal whether it can cache the data, and if so, how it should cache the data. These flags are all documented at http://api.drupal.org/api/drupal/modules--block--block.api.php/function/.... We assign it the value DRUPAL_NO_CACHE, which tells Drupal not to cache the output of this block.
There are a few other possible settings, but these two are the ones typically used.
Now that we have provided this hook our block will be included in the list of available blocks. Next, we need to provide the content that our block will return by implementing hook_block_view().
* Implements hook_block_view().
*/
function membercount_block_view($name) {
if ($name == 'count_members') {
$count = db_query('SELECT COUNT(uid) - 1 FROM {users}')->fetchField();
$content = format_plural($count, 'This site has 1 user.', 'This site has @count users.');
$block = array(
'subject' => t('Members'),
'content' => $content,
);
return $block;
}
}
Just as the hook_block_info() function was expected to produce block information for all blocks that the module declares, hook_block_view() is expected to produce the viewable content for any block that this module is responsible for. Thus, our block hook only responds when the name passed into the function matches the name we declared in the $blocks array in membercount_block_info().
Inside of the if statement our block function does four things:
- Query the database to find out how many users we have.
- Format the string to be returned.
- Create the block data structure (an associative array).
- Return the block.
Our database query is simple:
SELECT COUNT(uid) - 1 FROM {users}This selects the number of user IDs (minus one) from the users table. The users table is the main location for information about Drupal users. There is one record for each user of the system, plus one for the "anonymous" user. Since we don't want to include the anonymous account in our tally we subtract one.
When writing SELECT queries in Drupal, the syntax used is a slightly modified SQL. Notice that in Drupal queries, table names are always surrounded with curly braces ({}). This is so that on hosts that have stricter naming conventions, database table name prefixing can be done without requiring a manual rewrite of all SQL statements. These and other details of Drupal database syntax are documented here: http://api.drupal.org/api/drupal/includes--database--database.inc/group/....
The database query is executed using the db_query() function. It executes a simple query and returns an instance of a DatabaseStatementInterface. Since our query returned only one field (the results of the SQL COUNT()) we can use the fetchField() method to get the data for that single field.
The next line of our block function takes the $count value and uses it to construct a string for display:
$content = format_plural($count, 'This site has 1 user.', 'This site has @count users.');This line shows another interesting facet of Drupal's translation subsystem. Along with t(), Drupal provides other functions to handle translation difficulties. Human languages construct plurals in different ways. The pattern used in English is not the same as that in French, Hungarian, or Chinese. So strings that need to be translated differently depending on a number can be passed through the format_plural() function. In the default English case it will see if the $count value is 1. If so, it will use the first string (This site has 1 user). Otherwise it will use the second (plural) construction, substituting $count for the placeholder @count.
The next thing to do is construct the data structure that Drupal is expecting this function to return: an associative array with two keys:
- subject: The title of the block, which may be shown to the user.
- content: The formatted body of the block.
For our block, we set the subject to Members (and we pass it through t() for translation). The content is simply the $content variable. For a more complex block, we might use the theme() function to theme the data before setting it here. Finally, we return the block, allowing Drupal to continue assembling the blocks and rendering the page.
Installing and Configuring Our Module
We now have a complete module. Since we created it in sites/all/modules, installing it is a breeze. We can simply navigate to Toolbar » Modules and find it in the list:
Notice that the Name and Description columns pull their data from the membercount.info file we created earlier.
Simply checking the box and clicking on the Save configuration button will enable our new module. It should then show up as enabled, and since we implemented hook_help(), we see a help icon in the right-hand column:
Now we are ready to test out our module. Go to Toolbar » Structure » Blocks and position the block. Initially, there should be an entry named Count Members in the Disabled blocks list. We will put it in the Header section:
Clicking Save blocks at the bottom of the page will save this change. Now we should be able to close the Administration overlay and see the results of our new block in the upper-right corner.
Conclusion
We have created our first working Drupal 7 module. At this point, you've gotten a small dose of coding in Drupal, though we have barely scratched the surface of what can be done. There is virtually no part of Drupal's page rendering that you can't modify through Drupal's APIs. And most of the time, modifying Drupal is just a matter of declaring a new module and writing the right hooks.
The module we have created here is as basic as they come. More complex modules may contain multiple PHP source files, JavaScript files, and even templates, CSS, and images. It is even possible to group several modules together to create larger bundles of related features.
Ready to move on? There are plenty of resources out there to help you get going, and Drupal.org is a great place to start.