Building an Admin Interface in Drupal 7 Module Development

Exclusive offer: get 50% off this eBook here
Drupal 7 Module Development

Drupal 7 Module Development — Save 50%

Create your own Drupal 7 modules from scratch

$26.99    $13.50
by Greg Dunlap | December 2010 | Drupal Open Source

In this article, by Greg Dunlap, co-author of Drupal 7 Module Development we will create a module with an administrative interface. This module will build upon many of the module creation concepts. Some of the concepts we will cover in this article are:

  • Mapping Drupal functions to menu items using hook_menu()
  • Creating basic forms with the Form API
  • Managing Drupal settings using variable_set() and variable_get()
  • Sending mail using drupal_mail() and hook_mail()
  • Using Drupal 7's new token system

After this article is finished you should have a good handle on many concepts that are at the core of almost every module you will write in the future.

 

Drupal 7 Module Development

Drupal 7 Module Development

Create your own Drupal 7 modules from scratch

  • Specifically written for Drupal 7 development
  • Write your own Drupal modules, themes, and libraries
  • Discover the powerful new tools introduced in Drupal 7
  • Learn the programming secrets of six experienced Drupal developers
  • Get practical with this book's project-based format
        Read more about this book      

(For more resources on this subject, see here.)

The User Warn module

In this article we will be creating the User Warn module. This module allows administrators to send users a warning via e-mail when that user violates a site's terms of service or otherwise behaves in a way that is inappropriate. The User Warn module will implement the following features:

  • The module will expose configuration settings to site administrators, including default mail text
  • This e-mail will include Drupal tokens, which allow the admin to replace and/or add site-specific variables to the e-mail
  • Site administrators will be able to send a user mail via a new tab on their user profile page
  • Warning e-mails will be sent using Drupal's default mail implementation

Starting our module

We will begin by creating a new folder for our module called user_warn in the sites/default/modules directory in our Drupal installation. We can then create a user_warn.info file as shown in the following:

;$Id$ name = User Warn description = Exposes an admin interface to send behavior warning e-mails to users. core = 7.x package = Drupal 7 Development files[] = user_warn.module

You should be pretty familiar with this now. We will also create our user_warn.module file and add an implementation of hook_help() to let site administrators know what our module does.

<?php // $Id$ /** * @file * User Warn module file * * This module allows site administrators to send a stock warning * e-mail to a specified user or users through the admin interface. * Administrators * can configure the default e-mail including token replacement. */ /** * Implement hook_help(). */ function user_warn_help($path, $arg) { if ($path == 'admin/help#user_warn') { return t('User Warn allows site adminitrators to send a standard e-mail to site users to notify them of improper behavior.'); } }

This is also nothing new so lets move on to the good stuff.

The Drupal menu system

Drupal's menu system is deceptively named. The name implies that it is responsible for the navigation of your site, and while this is true it does a great deal more. At its core, the menu system is responsible for mapping Drupal paths to the functions that generate the contents of the requested page. The menu system is also responsible for controlling access to Drupal pages, acting as one of the central gatekeepers of Drupal security.

Drupal module developers can map paths to Drupal functions by implementing hook_menu(), which adds paths to the menu system, assigns them access rules, and optionally creates navigational elements for them.

Defining a page callback with hook_menu

For our module we will need to implement two new pages—a configuration page for the User Warn module, and a tab in the user profile area where administrators can go to send the actual e-mails to a specific user. These will each require their own hook_menu() implementation as defined in the following example.

This example only scratches the surface of the options available in the menu system. For more details, developers should check out the API documentation at:

http://api.drupal.org/api/function/hook_menu/7

The example is as follows:

/** * Implement hook_menu(). */ function user_warn_menu() { $items = array(); $items['admin/config/people/user_warn'] = array( 'title' => 'User Warn', 'description' => 'Configuration for the User Warn module.', 'page callback' => 'drupal_get_form', 'page arguments' => array('user_warn_form'), 'access arguments' => array('administer users'), 'type' => MENU_NORMAL_ITEM, ); $items['user/%/warn'] = array( 'title' => 'Warn', 'description' => 'Send e-mail to a user about improper site behavior.', 'page callback' => 'drupal_get_form', 'page arguments' => array('user_warn_confirm_form', 1), 'access arguments' => array('administer users'), 'type' => MENU_LOCAL_TASK, ); return $items; }

Like many Drupal hook implementations, hook_menu() returns a structured associative array with information about the menu items being defined. The first item in our example defines the module configuration page, and the second one defines the user tab where administrators can go to send the actual e-mail. Let's look at the first item in more detail.

Menu items are keyed off their path. This is an internal Drupal path with no leading or trailing slashes. This path not only defines the location of a page, but also its place in the menu hierarchy, with each part of the URL being a child of the last. In this example, people is a child of config which is itself a child of admin.

If a requested path does not exist, Drupal will work its way up the hierarchy until it encounters a page that does exist. You can see this in action by requesting admin/config/people/xyzzy which displays the page at admin/config/people.

If you are creating a menu item for site administration it must begin with admin. This places it into Drupal's administrative interface and applies the admin theme defined by the site settings.

Module-specific settings should always be present under admin/config. Drupal 7 offers several categories which module developers should use to better organize their settings according to Drupal functional groups like People and Permissions or Content Authoring.

The value associated with this key is itself an associative array with several keys that define what action should be taken when this URL is requested. We can now look at those in detail. The first item defines your page title:

'title' => 'User Warn',

This is used in a variety of display contexts—as your page's heading, in the HTML &lttitle&gt tag and as a subheading in the administration interface in combination with the description (if you are defining an administrative menu item).

'description' => 'Configuration for the User Warn module.',

The description is just that—a longer text description of the page that this menu item defines. This should provide the user with more detailed information about the actions they can take on this page. This description is also used as the title tag when you hover over a link.

Menu item titles and descriptions are passed through t() internally by Drupal, so this is one case where we don't need to worry about doing that ourselves.

For an administration page, these two items define how your page is listed in Drupal's admin area as shown in the following:

The next two items define what will happen when your page is requested:

'page callback' => 'drupal_get_form', 'page arguments' => array('user_warn_form'),

'page callback' defines the function that will get called (without the parentheses) and 'page arguments' contains an array of arguments that get passed to this function.

Often you will create a custom function that processes, formats, and returns specific data. However, in our case we are calling the internal Drupal function drupal_get_form() that returns an array as defined by Drupal's Form API. As an argument we are passing the form ID of the form we want to display.

The fifth item controls who can access your page.

'access arguments' => array('administer users'),

'access arguments' takes an array containing a permissions strings. Any user who has been assigned one of these permissions will be able to access this page. Anyone else will be given an Access denied page. Permissions are defined by modules using hook_permission(). You can see a full list of the currently defined permissions at admin/people/permissions as shown:

You can see the 'administer users' permission at the bottom of this list. In the preceding example, only the Administrator role has this permission, and as a result only those users assigned this role will be able to access our page.

Note that the titles of the permissions here do not necessarily match what you will need to enter in the access arguments array. Unfortunately, the only good way to find this information is by checking the hook_perm() implementation of the module in question.

The final item defines what type of menu item we are creating:

'type' => MENU_NORMAL_ITEM,

The 'type' is a bitmask of flags that describe what features we want our menu item to have (for instance, whether it is visible in the breadcrumb trail). Drupal defines over 20 constants for menu items that should cover any situation developers will find themselves in. The default type is MENU_NORMAL_ITEM, which indicates that this item will be visible in the menu tree as well as the breadcrumb trail.

This is all the information that is needed to register our path. Now when Drupal receives a request for this URL, it will return the results of drupal_get_form(user_warn_form).

Drupal caches the entire menu, so new/updated menu items will not be reflected immediately. To manually clear the cache, visit Admin | Configuration | Development | Performance and click on Clear all caches.

Using wildcards in menu paths

We have created a simple menu item, but sometimes simple won't do the job. In the User Warn module we want to have a menu item that is tied to each individual user's profile page. Profile pages in Drupal live at the path user/&ltuser_id&gt, so how do we create a distinct menu item for each user? Fortunately the menu system allows us to use wildcards when we define our menu paths.

If you look at the second menu item defined in the preceding example, you will see that its definition differs a bit from our first example.

$items['user/%/warn'] = array( 'title' => 'Warn', 'description' => 'Send e-mail to a user about improper site behavior.', 'page callback' => 'drupal_get_form', 'page arguments' => array('user_warn_confirm_form', 1), 'access arguments' => array('administer users'), 'type' => MENU_LOCAL_TASK, );

The first difference is that the path is defined with % as one of the path entries. This indicates a wildcard; anything can be entered here and the menu item's hierarchy will be maintained. In Drupal, that will always be a user's ID. However, there is nothing stopping any user from entering a URL like user/xyzzy/warn or something else potentially more malicious. Your code should always be written in such a way as to handle these eventualities, for instance by verifying that the argument actually maps to a Drupal user. This would be a good improvement.

The other difference in this example is that we have added 1 as an additional argument to be passed to our page callback.

Each argument in a menu item's path can be accessed as an argument that is available to be passed to our page callback, starting with 0 for the root argument. So here the string user is item 0, and the user's ID is item 1. To use the user's ID as a page callback argument, we reference it by its number. The result in this case is that the user's ID will be passed as an additional argument to drupal_get_form().

We have one other difference in this second menu item:

'type' => MENU_LOCAL_TASK,

We have defined our type as MENU_LOCAL_TASK. This tells Drupal that our menu item describes actions that can be performed on the parent item. In this example, Warn is an action that can be performed on a user. These are usually rendered as an additional tab on the page in question, as you can see in the following example user profile screen:

Having defined the paths for our pages through hook_menu(), we now need to build our forms.

Form API

In standard web development, one of the most tedious and unrewarding tasks is defining HTML forms and handling their submissions. Lay out the form, create labels, write the submission function, figure out error handling, and the worst part is that from site to site much of this code is boilerplate—it's fundamentally the same, differing only in presentation. Drupal's Form API is a powerful tool allowing developers to create forms and handle form submissions quickly and easily. This is done by defining arrays of form elements and creating validation and submit callbacks for the form.

In past versions of Drupal, Form API was commonly referred to as FAPI. However, Drupal 7 now has three APIs which could fit this acronym—Form API, Field API and File API. We will avoid using the acronym FAPI completely, to prevent confusion, but you will still encounter it widely in online references.

Form API is also a crucial element in Drupal's security. It provides unique form tokens that are verified on form submission, preventing Cross-site Request Forgery attacks, and automatically validating required fields, field lengths, and a variety of other form element properties.

While Form API is one of the most useful and powerful tools in the module developer's toolbox, it can also be one of the most complicated. More detailed information beyond this simple example can be found at the following URLs:

Using drupal_get_form()

In our first menu implementation seen earlier, we defined the page callback as drupal_get_form(). This Form API function returns a structured array that represents an HTML form. This gets rendered by Drupal and presented as an HTML form for the user. drupal_get_form() takes a form ID as a parameter. This form ID can be whatever you want, but it must be unique within Drupal. Typically it will be &ltmodule_name&gt_&ltdescription&gt_form.

The form ID is also the name of the callback function drupal_get_form() will call to build your form. The specified function should return a properly formatted array containing all the elements your form needs.

Since the form ID also serves as the form's callback function, it must be a valid PHP variable name. Spaces and hyphens are not allowed. All form IDs should be prefaced by the name of your module followed by an underscore, in order to prevent name collision.

Other parameters can be passed into drupal_get_form() in addition to the form ID. These extra parameters simply get passed through to the callback function for its own use.

In Drupal 6, drupal_get_form() returned a fully rendered HTML form. This has been changed in Drupal 7 in order to allow more flexibility in theming and easier form manipulation. drupal_get_form() now returns an unrendered form array which must be passed to drupal_render() for final output. In the preceding example the menu system handles the change transparently, but other code converted from Drupal 6 may need to be changed.

Drupal 7 Module Development Create your own Drupal 7 modules from scratch
Published: December 2010
eBook Price: $26.99
Book Price: $44.99
See more
Select your format and quantity:
        Read more about this book      

(For more resources on this subject, see here.)

Building a form callback function

For the User Warn module we need a form that allows the site administrator to enter the following items:

  • A subject line for our outgoing e-mail
  • The text of our outgoing e-mail
  • A checkbox indicating whether or not the administrator should be sent a Bcc on outgoing e-mails
  • A submit button

Our menu definition specified user_warn_form as the page arguments, so we need to create that function and define our form within it.

This function takes two parameters—$form and $form_state. We will not be using these parameters in the context of just displaying a form. But, for more information on their usage see the Form API Quickstart Guide.

/**
* Form builder; Create and display the User Warn configuration
* settings form.
*/
function user_warn_form($form, &$form_state) {
// Text field for the e-mail subject.
$form['user_warn_e-mail_subject'] = array(
'#type' => 'textfield',
'#title' => t('Warning e-mail subject'),
'#description' => t('The subject of the e-mail which will be sent
to users.'),
'#size' => 40,
'#maxlength' => 120,
'#required' => TRUE,
);

// Textarea for the body of the e-mail.
$form['user_warn_e-mail_text'] = array(
'#type' => 'textarea',
'#rows' => 10,
'#columns' => 40,
'#title' => t('Warning e-mail text'),
'#required' => TRUE,
'#description'=> t('The text of the e-mail which will be sent to users.'),
);

// Checkbox to indicate if admin should be sent a Bcc on e-mails.
$form['user_warn_bcc'] = array(
'#type' => 'checkbox',
'#title' => t('BCC admin on all e-mails'),
'#description' => t("Indicates whether the admin user
(as set in site configuration) should be sent on all warning e-mails."),
);

// Submit button
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Save settings'),
);

return $form;
}

The properties of a form element always begin with a # sign in order to distinguish them from nested form fields. For more information visit the Form API Quickstart Guide.

This is very similar to what we did earlier while implementing hook_menu(). We create a specially formatted associative array and return it to the calling function. In this case, each element in the array corresponds to an element in our form.

Lets look at the subject field first as an example.

$form['user_warn_e-mail_subject'] = array(

Each element is keyed by a unique string, which will become the element's name attribute when the form is rendered. This element is then assigned an array of attributes.

For a complete matrix of all the form elements defined by Drupal as well as the properties each one implements, visit:
http://api.drupal.org/api/drupal/developer--topics--forms_api_reference.html/7

'#type' => 'textfield',

The first attribute is '#type' which defines what form element will be rendered. All the standard HTML form elements have types, as well as some Drupal-specific elements defined. In this case we are creating a basic textfield.

'#title' => t('Warning e-mail subject'),
'#description' => t(
'The subject of the e-mail which will be sent to users.'),

The next two attributes, '#title' and '#description' define the element's label and an optional description.

Any attribute that a standard HTML element has are available as Form API properties or attributes as well. For instance, see the following two lines of code.

'#size' => 40,
'#maxlength' => 120,

As you would expect, these define the size and maxlength attributes of our text field. One of the nice things about Form API is that it will automatically validate many of the element's attributes when the form is submitted. In this case Drupal will throw an error if any text is submitted with a length greater than the element's maxlength. All this happens transparently with no extra code from the developer.

Form API also adds some convenience properties for validation purposes, like '#required'.

'#required' => TRUE,

When '#required' is set to TRUE, Drupal will throw an error if the form is submitted without a value in that element. Required fields are also marked with an asterisk in their labels. Again, this happens transparently without any extra code. Drupal will even highlight the field when an error applies to it! This on-the-fly error handling and form validation is one of the reasons Form API is such a boon to developers. It really reduces the amount of drudgery involved in creating and handling HTML forms.

The following is how this text field will appear when rendered by Drupal in the default admin theme:

Moving on to the following elements, you can see this pattern repeat itself. For instance, the e-mail body field of type '#textarea' implements the '#rows' and '#columns' properties, just like the matching HTML attributes for a textarea. The checkbox element (indicating whether the admin should be sent a BCC on outgoing e-mails) and submit button are equally straightforward to operate.

When we visit the URL that we registered earlier (admin/config/people/user_warn), we get the form rendered as seen in the following screenshot:

Drupal also offers several custom form elements in addition to the standard HTML fields. You can see an example of one of these in the drupal_get_form() callback for our second menu item:

/**
* Form builder; display the e-mail confirmation form.
*/
function user_warn_confirm_form($form, &$form_state, $uid) {
$form['account'] = array(
'#type' => 'value',
'#value' => user_load($uid),
);

return confirm_form(
$form,
t('Are you sure you want to send a warning e-mail to this
user?'),
'user/' . $uid,
t('This action can not be undone.'),
t('Send e-mail'),
t('Cancel')
);
}

We will revisit this function in more detail later in the article, but for now we will focus on the highlighted area. We use a wildcard in the menu item path to grab the user's ID and pass it into the page callback. As you can see now, this is being passed as the third parameter into our callback function (after the required $form and $form_state parameters). We can now use this ID to retrieve data about the user for future use.

Also, you can see that we are defining a new form element of type 'value'. The value element is similar to the HTML hidden fields with two distinct advantages. First, value elements can contain any data you want as opposed to just strings. Arrays, objects, or any other complex data structure can be stored and passed in a value element.

The second advantage is that value elements are not printed back to the browser in the HTML source. This can improve the security of your data by preventing users from viewing and/or modifying it on a local instance.

In this code sample we are assigning the value element 'account' with the value of a Drupal user object. This object will be passed on when the form is submitted, and the receiving function will be able to use it as needed. Value elements are extremely useful and developers should always consider using them in places where they would otherwise use hidden fields.

Drupal also offers a Form API element of type 'hidden', should developers prefer to use it.

Form API makes form building incredibly simple, but right now this form has two problems. First, the module should provide some reasonable default settings for system administrators. Second, when you submit the form, none of the submitted data is actually handled in any way. Let's take a brief detour from form handling and look at how Drupal manages persistent system data.

Managing persistent data

Drupal provides a mechanism by which data, which needs to persist semi-permanently (like system settings), can be saved and retrieved. These items are somewhat confusingly referred to as 'variables' (we will refer to them specifically as persistent variables from here on to avoid confusion). Persistent variables are stored in a database table, keyed by a unique name provided by the module that implements them.

Persistent variables are saved using variable_set(), and retrieved using variable_get(). These variables can be any type of data that a developer needs, be it a simple string, an associative array, or a PHP object. The Drupal API for setting/getting them takes care of all the serialization/unserialization that is necessary behind the scenes.

variable_get() can also provide a default value, which is useful for situations where you need a variable which has not already been set, for instance, after a module is installed for the first time. We can use this to our advantage in our configuration form as shown in the following snippet:

$form['user_warn_e-mail_subject'] = array(
'#type' => 'textfield',
'#default_value' => variable_get('user_warn_e-mail_subject',
'Administrative Warning'),
'#title' => t('Warning e-mail subject'),
'#size' => 40,
'#maxlength' => 120,
'#required' => TRUE,
'#description' => t(
'The subject of the e-mail which will be sent to users.'),
);

This is the same Form API element we created above, but with a new line added. This line adds the '#default_value' property of the form element. This property tells the form what data the element should contain when the form is first loaded.

We are assigning this property the results of a call to variable_get() using two parameters. The first parameter is the unique key associated with this data. It is common practice to give a persistent variable the same name as the form element it is associated with, and we have done so here.

Like menu items, persistent variables are cached by Drupal, so you will often need to clear your caches after modifying them.

The second parameter specifies the data that should be returned if this variable has never been explicitly set. In this example we have set that to be the string 'Administrative Warning'. If this variable had been explicitly set sometime previously, then that data will be returned by variable_get() instead. Otherwise, the default value will be returned.

Now the first time the form loads, whatever data is in the persistent variable 'user_warn_e-mail_subject' will be set as the value of the e-mail subject form element. We can also do this to our other form elements as desired. In the end our function will be as follows. Note that we have also added a constant containing the default text of our e-mail. Removing this large block of text from our array definition makes our code more readable and maintainable down the road.

Drupal constants are typically defined at the top of a .module file, but for the sake of clarity this example includes the constant definition with the function:

define('USER_WARN_MAIL_TEXT',
'Hello,

We have been notified that you have posted comments on our site that
are in violation of our terms of service.
If this behavior continues your account will be suspended.

Sincerely,
The administrative staff');

function user_warn_form($form, &$form_state) {
$form = array();

// Text field for the e-mail subject.
$form['user_warn_e-mail_subject'] = array(
'#type' => 'textfield',
'#default_value' => variable_get('user_warn_e-mail_subject',
'Administrative Warning'),
'#title' => t('Warning e-mail subject'),
'#size' => 40,
'#maxlength' => 120,
'#required' => TRUE,
'#description' => t(
'The subject of the e-mail which will be sent to users.'),
);
// Textarea for the body of the e-mail.
$form['user_warn_e-mail_text'] = array(
'#type' => 'textarea',
'#rows' => 10,
'#columns' => 40,
'#default_value' => variable_get('user_warn_e-mail_text',
USER_WARN_MAIL_TEXT),
'#title' => t('Warning e-mail text'),
'#required' => TRUE,
'#description' => t(
'The text of the e-mail which will be sent to users. '),
);
// Checkbox to indicate whether admin should be sent a Bcc
// on e-mails.
$form['user_warn_bcc'] = array(
'#type' => 'checkbox',
'#default_value' => variable_get('user_warn_bcc', FALSE),
'#title' => t('BCC admin on all e-mails'),
'#description' => t('Indicates whether the admin user
(as set in site configuration) should be BCC\'d on all warning e-mails.'),
);
// Submit button
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Save settings'),
);

return $form;
}

Persistent variables are an excellent way to store module settings and other user-configurable information. However, having given our configuration settings reasonable defaults, we are still left with the issue of how to save changes to these defaults.

Form submission process

When an HTML form built with Form API is submitted, Drupal looks for two specifically named functions—a validate function and a submit function. These functions are named by taking the form ID and appending either _validate() or _submit() depending on which function you are writing.

The validate function does additional validation beyond what Drupal provides. For instance if you wanted to check to see if a zip code is valid. Even if validation fails on one element, all validation functions are still called, so Drupal can return multiple errors in a single form. However, if any validation function fails, execution never proceeds to the submit function.

Validate functions are optional. If you don't need any additional validation, then you don't have to write one. In this case, Drupal will just do its default validation. For more information on how to write validate functions.

Once the form passes validation, the submit function is called. This is where the real work is done—saving settings, sending e-mail, and creating content among other things. The form submit function is one of the main workhorses of Drupal modules.

As a module writer you will spend an inordinate amount of time writing submit functions and support code for submit functions. This is good, because it means you are spending time on the code that is unique to your project, and not recreating the wheel every time you want to turn a field red because a required field is missing.

So let's apply this to the User Warn configuration form. The form ID for our configuration form is user_warn_form, so our submit function will be named user_warn_form_submit().

Form submit functions take two arguments. $form is the original Form API array for the submitted form, and $form_state is an associative array containing information specific to this submission. In particular, $form_state['values'] contains all the submitted form values keyed on their name properties. In general, $form_state['values'] is the only thing you will need to worry about in validate and submit functions.

/**
* Save configuration settings for User Warn module.
*/
function user_warn_form_submit($form, &$form_state) {
variable_set('user_warn_e-mail_subject',
$form_state['values']['user_warn_e-mail_subject']);
variable_set('user_warn_e-mail_text',
$form_state['values']['user_warn_e-mail_text']);
variable_set('user_warn_bcc',
$form_state['values']['user_warn_bcc']);

drupal_set_message(t('The settings have been saved'));
}

After all that, our submit function is pretty simple. We are saving our submitted data using variable_set(), then setting a simple message indicating that the values have been saved successfully. Our needs for validation are handled by Drupal's built-in form validation, so we don't even need a validate function for this data.

The function drupal_set_message() sets a simple message that is displayed in a specific area at the top of a Drupal page. For more details see http://api.drupal.org/api/function/drupal_set_message/7.

After a form is submitted, it will reload the form submission page. Since we have saved new data in our persistent variables through the submit function, and since the form is pre-loaded with default data based on the data in those variables, we should now be able to submit new data for these items and see them reflected after the form has been submitted.

If you want Drupal to redirect to a different page after form submission, you can set $form_state['redirect'] to the desired path in your submit function. If this isn't working, check whether you have specified for $form_state to be passed by reference by adding an ampersand to it in your function signature.

Form API provides us with a great deal of power with a pretty small amount of work, but Drupal offers some shortcuts which make common forms even easier.

A shortcut for system settings

The need to save configuration settings into persistent variables through a standard form is pretty common. Thankfully, once again Drupal has provided us with some magic to simplify this task. That magic is the system_settings_form() function. When you pass a standard Form API form array through this function and return the results, you get several benefits. Take a look at the following modified version of user_warn_form():

/**
* Form builder; Build the User Warn settings form.
*/
function user_warn_form($form, &$form_state) {

// Text field for the e-mail subject.
$form['user_warn_e-mail_subject'] = array(
'#type' => 'textfield',
'#default_value' => 'Administrative Warning',
'#title' => t('Warning e-mail subject'),
'#size' => 40,
'#maxlength' => 120,
'#required' => TRUE,
'#description' => t(
'The subject of the e-mail which will be sent to users.'),
);
// Textarea for the body of the e-mail.
$form['user_warn_e-mail_text'] = array(
'#type' => 'textarea',
'#rows' => 10,
'#columns' => 40,
'#default_value' => USER_WARN_MAIL_TEXT,
'#title' => t('Warning e-mail text'),
'#required' => TRUE,
'#description' => t(
'The text of the e-mail which will be sent to users.'),
);
// Checkbox to indicate whether admin should be sent a Bcc
// on e-mails.
$form['user_warn_bcc'] = array(
'#type' => 'checkbox',
'#default_value' => FALSE,
'#title' => t('BCC admin on all e-mails'),
'#description' => t('Indicates whether the admin user (as set in
site configuration) should be BCC\'d on all warning e-mails.'),
);

return system_settings_form($form);
}

The first thing you'll notice is that we no longer have a submit button element. That is because system_settings_form() adds one in for us automatically. It gets the label 'Save settings'.

Additionally, system_settings_form() uses its own custom submit handler system_settings_form_submit(), which automatically saves all form elements into persistent variables of the same name. You don't have to write a submit function at all, Drupal takes care of everything behind the scenes.

It might seem silly to use an API function for something as simple as adding a submit button and automating the handling of persistent variables. However, the less code you have to write the less bugs you introduce. With just around 30 lines of code, we now have a fully functional form with extensive validation, customizable default settings, and the ability for users to change the default settings as they wish.

Having set up our module's configuration form, we now need to add a function that enables administrators to actually send this e-mail to users.

A shortcut for confirmation forms

Earlier in the article, we added a 'Warn' tab to user profile pages. System administrators should be able to click this tab to send the warning e-mail to users. However, it would be nice if we could add a confirmation step here, to prevent e-mails from being sent inadvertently.

This is another situation where Drupal offers a convenient shortcut function. Let's revisit the callback function we looked at earlier while discussing 'value' form elements.

/**
* Form builder; display the e-mail confirmation form.
*/
function user_warn_confirm_form($form, &$form_state, $uid)
{
$form['account'] = array(
'#type' => 'value',
'#value' => user_load($uid),
);
return confirm_form(
$form,
t(
'Are you sure you want to send a warning e-mail to this user?'),
'user/' . $uid,
t('This action can not be undone.'),
t('Send e-mail'),
t('Cancel')
);
}

The confirm_form() function allows developers to easily create confirmation forms associated with specific actions. It takes seven arguments, which seems intimidating but they are actually pretty intuitive.

The first argument contains additional form elements that we want merged into the resulting confirmation form. As we saw earlier, we have created a value element containing a user account object. We need this to be passed on to the form's submit function, so we set it to be added with all the other elements that confirm_form() creates on its own.

The second argument specifies the question you want to ask when the user is presented with the confirmation option. This is pretty straightforward and we have an appropriate message there.

The third argument indicates what URL the user should be sent to if the user cancels the form. Usually this will be an internal Drupal path without leading or trailing slashes. Typically site administrators will get to this page from a user profile page, so it is appropriate that when this form is canceled the administrators are returned to this profile page.

The final three arguments specify various captions used in the form. They are the additional description text to be displayed above the confirm button, the text of confirm button, and the text of the cancel link. All of these messages are optional, and Drupal will use sensible defaults if you don't change them here explicitly.

The code above displays the following:

Forms generated by confirm_form() only call their submit functions if the form is actually confirmed, so developers don't need to check for this themselves. If the form is canceled, then the user is simply redirected to the URL specified in the function call.

We have now gotten a pretty thorough introduction to Drupal's Form API. We can create forms from scratch, write, validate, and submit handlers, and use some of Drupal's internal functions to create common form types. We're two-thirds of the way into this article and we still haven't touched on the module's central purpose—sending an e-mail to a user!

Drupal 7 Module Development Create your own Drupal 7 modules from scratch
Published: December 2010
eBook Price: $26.99
Book Price: $44.99
See more
Select your format and quantity:
        Read more about this book      

(For more resources on this subject, see here.)

Sending mail with drupal_mail() and hook_mail()

Drupal implements a custom e-mail templating system. Initially it may appear that this system is overly complicated, but it allows an enormous amount of flexibility for module developers.

Sending e-mail in Drupal is a multi-step process:

  1. drupal_mail() is called, specifying what mail is being sent and what options are unique to this specific message (the recipient's e-mail address, the language the mail should be sent in, and so on).
  2. Drupal then builds an e-mail message with standard headers combined with the information submitted to drupal_mail().
  3. The hook_mail() implementation specified in drupal_mail() is called. This is where the subject and body of the mail are added.
  4. The fully composed mail array is then passed to hook_mail_alter(), allowing other modules to modify it (for instance, to add a common signature to all outgoing e-mails.).
  5. The mail is passed to drupal_send_mail() for delivery.

That is a pretty long process just for sending a simple e-mail! However, in most cases developers will only have to worry about two of the above steps—calling drupal_mail() and implementing hook_mail().

PHP mail configuration
In order for Drupal to send an e-mail, your server must be configured so that PHP's mail() function will work. Typically this involves installing and configuring a local mail server. Most shared hosting providers are already properly configured to do this, but if you are on a VPS or other custom-built server you may have to handle it yourself. This process varies wildly depending on your operating system and a variety of other factors. Searching for php mail setup on Google will most likely start you in the right direction.

Calling drupal_mail()

The following is the function that gets called when our confirmation form is submitted (indicating that we should in fact send an e-mail warning to the user in question).

/**
* Send a warning e-mail to the specified user.
*/
function user_warn_confirm_form_submit($form, &$form_state) {
$account = $form_state['values']['account'];

drupal_mail(
'user_warn',
'warn',
$account->mail,
user_preferred_language($account),
$form_state['values'],
variable_get('site_mail', NULL),
TRUE
);
}

As you can see, drupal_mail() requires that we pass it quite a bit of information. Let's look at each of these arguments in detail:

  • The first argument indicates what module should invoke hook_mail() to send this message. We are setting this to 'user_warn' since we are doing our own hook_mail() implementation. However, you can send a mail implemented by another module if you need to. We will look at the user_warn_mail() implementation in a bit.
  • The second argument, warn, is a key which is passed to hook_mail(). Any hook_mail() implementation can define several e-mails, uniquely identified by a text key. (Drupal's user module implements eighteen (!) for things like account confirmation and forgotten passwords). We specify which specific mail we want to send with this parameter.
  • The third argument contains the recipient's address. We pull this out of the user object for the user whose profile we visited, as passed by the confirmation form above.
  • The fourth argument specifies what language the mail should be sent in. This is important because individual users can specify a language preference that is different from the site's default language. We should honor this choice if possible when sending our e-mail to this user. The user_preferred_language() function makes this task easy by taking a user object and returning the user's language choice.
  • The fifth argument is an associative array of parameters to be passed to hook_mail(). Any custom information needed to build the e-mail should be put here. In our case, any custom information we need to build the e-mail is already in the data submitted from the confirmation form, so we will just use $form_state['values'] here.
  • The sixth argument contains the e-mail address from whom this mail should be sent. When you first installed Drupal you had to specify an administrative e-mail address. This address is already being used as the source for other system e-mails (like account verification) so it makes sense to use it as our sender e-mail as well. The e-mail is stored as a persistent variable with the key 'site_mail', so we can easily grab it using variable_get().
  • Finally, the last variable indicates whether or not the mail should actually be sent. It will come as no surprise to learn that a mail message in Drupal is built in a specially structured associative array. At the end of the mail building process, this array is typically passed to the function drupal_mail_send() which handles the actual delivery of the mail. However, by setting this parameter to FALSE, drupal_mail() will bypass the delivery step, allowing you to take the structured array it returns and handle delivery yourself.

Implementing hook_mail()

In the last section, when we called drupal_mail(), we indicated that it should invoke hook_mail() in the user_warn module. This means that Drupal will be looking for a function named user_warn_mail(). This function is as follows:

/**
* Implement hook_mail().
*/
function user_warn_mail($key, &$message, $params) {
switch ($key) {
case 'warn':
$account = $params['account'];
$subject = variable_get('user_warn_e-mail_subject',
'Administrative Warning');
$body = variable_get('user_warn_e-mail_text',
'You\'ve been warned!');

if (variable_get('user_warn_bcc', FALSE)) {
$admin_mail = variable_get('site_mail', NULL);
$message['headers']['bcc'] = $admin_mail;
}

$message['to'] = $account->mail;
$message['subject'] = $subject;
$message['body'][] = $body;
break;
}
}

As you can see the preceding function receives three arguments:

  • The key we passed in parameter two of our call to drupal_mail(), indicating what message should be sent.
  • The structured array that Drupal creates to represent an e-mail message. At this point this array has already been populated with the mail's default header information.
  • The data we passed from drupal_mail() in the $params argument (in this case, a user's account object.)

As discussed earlier, it is possible for hook_mail() to handle multiple different e-mails as indicated by the key passed in from drupal_mail(). Even though we are only sending one e-mail with a key of 'warn', we still put it into a switch/case structure to make it easier to manage more mails later on if needed.

Now we can get on with the real purpose of our hook implementation—adding details to the $message array that are unique to our mail. Typically this is the subject, body, and any additional headers that we might need.

Our e-mail's subject and text have been set via the module's configuration page, so we retrieve them via variable_get() and set them to the $message['subject'] and $message['body] properties here.

Note that we do not pass the subject and body strings through t() as we have done in other contexts. These strings are supplied by the site administrator through the User Warn module's configuration form, and as such are not translatable. Only hardcoded system strings need to be passed through t().

The other thing we need to do is to Bcc the site admin if that configuration setting has been set.

if (variable_get('user_warn_bcc', FALSE)) {
$admin_mail = variable_get('site_mail', NULL);
$message['headers']['bcc'] = $admin_mail;
}

As with the other configuration settings, we retrieve it using variable_get(). If it is TRUE, then we need to set the site admin to be Bcc'd. Unlike the e-mail recipient, Cc and Bcc are set by adding headers to the $message array. The headers are themselves an associative array held under the 'headers' key, and we need to add a new header with the key 'Bcc'. We assign this to the site admin's e-mail in the same manner as we did in drupal_mail() while setting the mail's sender.

This is all we need to do! $message is passed by reference, so we don't even need to return it. Drupal will just proceed on from here. After other modules get a chance to alter the mail through hook_mail_alter(), the $message array will be passed to drupal_mail_system() where the final mail message will be formatted and delivered (if you specified this option when you called drupal_mail()).

Debugging mail problems
There are a variety of reasons why an e-mail might not be delivered. If the the recipient's address does not exist or there is another problem on the receiving end, the mail will be bounced back to the e-mail address specified in the sixth argument of drupal_mail() (the site administrator in this example.). In the case of a misconfigured local system, you may be able to find more information in PHP's error logs. The Reroute Mail module can be helpful if you are having problems sending mail on your development server: http://drupal.org/project/reroute_e-mail

This is all good, and we actually have a fully functional module now. However, there is one more issue we should look at addressing.

The token system

It would be nice if we could include some personalized information in the mail text without having to hardcode it in the module configuration form. For instance, we should be able to include the login of the user being warned, or the name of the site admin. This leads us into our final topic, using Drupal's token system.

What are tokens?

A token is a small piece of text that can be placed into a piece of text via the use of a placeholder. When the text is passed through the function token_replace(), then the tokens are replaced with the appropriate information. Tokens allow users to include data that could change in text blocks, without having to go back and change it everywhere they're referenced.

In previous versions of Drupal, tokens were implemented using the contributed module named, not surprisingly, Token. This functionality proved to be so popular and widely used that it was included in core for Drupal 7.

A sample token is [site:name]. When text containing this token is passed through token_replace(), it is replaced with your site's name as defined in Home | Administer | Configuration | Site information. If you change your site's name, then in the future all text containing this token will reflect this change. Drupal exposes a variety of tokens containing information on users, nodes, site-wide configuration, and more.

Tokens can also be 'chained'—a token can refer to another token which can refer to yet another one. As an example, the token [node:author] contains the name of a node's author, and the token [user:e-mail] contains the e-mail address of a given user. To retrieve the e-mail address of a node's author, you can chain the two together with the token [node:author:e-mail].

Module developers can also expose their own tokens for other module developers to take advantage of. For more information on how to expose tokens in your module, see the following sites:
http://api.drupal.org/api/function/hook_token_info/7
http://api.drupal.org/api/function/hook_tokens/7

Drupal's token system is extremely flexible and prevents site builders and developers from having to replace information in site text every time it changes. So let's see how we can use tokens in our module.

How do we know what tokens are available?
Drupal 7 does not include a user interface for browsing available tokens, however the contributed Token module implements a very nice JavaScript tree-like browser for them. You can download and install it from the following site:
http://drupal.org/project/token
Additionally module developers can use the function token_info() to get a structured array containing all the tokens in the system. This can be parsed and/or displayed as desired.

Implementing tokens in your text

The obvious place where User Warn could use tokens is in the text of the outgoing e-mails. Let's expand the very simple default text we included above, and also put it into a constant, for easier module readability and maintainability. This will require updating some of the previous code, but in the future we will only need to change this information in one place.

define('USER_WARN_MAIL_TEXT',
'Hello [user:name],

We have been notified that you have posted comments on [site:name]
that are in violation of our terms of service. If this behavior
continues your account will be suspended.

Sincerely,
[site:name]');

This text contains three tokens:

  • [site:name]: the site's name as described earlier
  • [site:mail]: the administrative e-mail address (this is the same e-mail address returned by variable_get('site-mail')
  • [user:name]: the login name of a specified user

In order to make this work, we have to implement token_replace() in our hook_mail() implementation as highlighted below:

/**
* Implement hook_mail().
*/
function user_warn_mail($key, &$message, $params) {
switch ($key) {
case 'warn':
$account = $params['account'];
$subject = variable_get('user_warn_e-mail_
subject','Administrative Warning');
$body = variable_get('user_warn_e-mail_text',
USER_WARN_MAIL_TEXT);
if (variable_get('user_warn_bcc', FALSE)) {
$admin_mail = variable_get('site_mail', NULL);
$message['headers']['bcc'] = $admin_mail;
}
$message['to'] = $account->mail;
$message['subject'] = $subject;
$message['body'][] = token_replace($body,
array('user' => $account));
break;
}
}

As you can see, we're now setting the e-mail body to the return value from token_replace(). This function is pretty simple, it only takes two arguments:

  • The text with tokens in place.
  • An array of keyed objects to be used in the token replacement process. In this case, the user object for the recipient of this e-mail as passed in the $params argument from drupal_mail(). If you need other replacements (like for a node) you would add additional objects into this array.

That's it! The text returned from token_replace() will now look something like this:

Hello eshqi,

We have been notified that you have posted comments on The Coolest
Site In The World that are in violation of our terms of service. If
this behavior continues your account will be suspended.

Sincerely,
The Coolest Site In The World

This e-mail is much better and personalized for both the sender and the recipient.

Summary

In reality the User Warn module is probably of limited utility, but it does help to introduce many of the core concepts that Drupal developers will use on a day-to-day basis. You are now able to create pages at a specific URL using hook_menu(), and implement forms on those pages using the Form API. The values submitted from this form can be saved using functions like system_settings_form(), confirm_form(), or your own custom submit handler. You can also send the results of a form submission as a custom email using dynamic tokens for text replacement.


Further resources on this subject:


About the Author :


Greg Dunlap

Greg Dunlap is a software engineer based in Stockholm, Sweden. Over the past 15 years Greg has been involved in a wide variety of projects including desktop database applications, kiosks, embedded software for pinball and slot machines, and websites in over a dozen programming languages. Greg has been heavily involved with Drupal for three years, and is the maintainer of the Deploy and Services modules as well as a frequent speaker at Drupal conferences. Greg is currently Principal Software Developer at NodeOne.

Several people played crucial roles in my development as a Drupal contributor, providing support and encouragement just when I needed it most. My deepest gratitude to Gary Love, Jeff Eaton, Boris Mann, Angie Byron and Ken Rickard for helping me kick it up a notch. Extra special thanks to the lovely Roya Naini for putting up with lost nights and weekends in the service of finishing my chapters.

Books From Packt


Drupal 7
Drupal 7

Drupal 6 Attachment Views
Drupal 6 Attachment Views

Drupal 6 Search Engine Optimization
Drupal 6 Search Engine Optimization

Drupal 6 Performance Tips
Drupal 6 Performance Tips

Drupal 6 Panels Cookbook
Drupal 6 Panels Cookbook

Drupal E-commerce with Ubercart 2.x
Drupal E-commerce with Ubercart 2.x

Flash with Drupal
Flash with Drupal

Learning Drupal 6 Module Development
Learning Drupal 6 Module Development


Your rating: None Average: 5 (2 votes)

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
z
5
w
y
j
F
Enter the code without spaces and pay attention to upper/lower case.
Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software