Clone wiki

Drupal: The Right Way / Home

Drupal 7 guide for Professional Developers

Code standards, general good practices and hints for developing and maintaining successful, long-term Drupal projects.

NOTE: This guide is now officially abandoned.

The point of this was to try to create a good, consistent standard that can be followed by a team of developers.

Pull requests, patches, comments, insults, suggestions, and bribes are welcome. Feel free to edit and improve this page.


Pere Orga



Licensed under the Creative Commons Attribution-ShareAlike 4.0 (CC BY-SA 4.0) license

Recommended Drupal books

Awesome guides


  • PHP: The Right Way is a quick reference for PHP developers. A must-read for all Drupal back-end developers
  • PHP Sadness, a site by @ericwastl, contains a useful list of real, objective issues of current PHP versions. You can agree or not with them but it is helpful to take them into account
  • PHP Pitfalls is a similar article by Phabricator team
  • PHP Language Specifications by Facebook

Idiomatic and consistent code style

"All code in any code-base should look like a single person typed it, even when many people are contributing to it." - Nicolas Gallagher

Any Drupal module or theme, custom or contrib, should follow Drupal coding standards, that apart from PHP code standards, include CSS coding standards, JavaScript coding standards and the Drupal Markup Style Guide.

When developing a Drupal project, you should always follow the Drupal style guide. However, it may be useful to look at other resources:

Automated code review is a Bash script to review code. It uses the following components:

These components may be used individually too, and some integrations with your preferred IDE may exist.

For open source projects, an online version is available:

Directory layout & files

|-- sites
|   |-- all
|   |   |-- drush
|   |   |-- libraries
|   |   |-- modules
|   |   |   |-- contrib
|   |   |   |-- custom
|   |   |   `-- features
|   |   `-- themes
|   `-- default

Remove CHANGELOG.txt file. Visitors don't need to know which version of Drupal you are running. In most circumstances, the following files can be safely removed (add them to .gitignore after deleting them):


rm install.php xmlrpc.php CHANGELOG.txt COPYRIGHT.txt INSTALL.txt INSTALL.mysql.txt INSTALL.pgsql.txt INSTALL.sqlite.txt LICENSE.txt MAINTAINERS.txt UPGRADE.txt README.txt

Some browsers always request files such as favicon.ico in the server root, even when there is a meta tag specifying another one. To evade thousands of 404 errors that may not be cached, put the following files in the root directory:


Site settings

Site configuration should go in settings.php file. On a multi-site setup, sites/ will be used instead.

At the bottom of the file, load settings.local.php file:

if (file_exists(__DIR__ . '/settings.local.php')) {
  include __DIR__ . '/settings.local.php';
This file is used to apply and override environment-specific settings. Put all development settings in your local settings file and avoid using the UI for that.

An encouraged practice is to remove settings.php file from .gitignore, adding sites/*/settings.local.php pattern instead. That way settings.php will reside in the repository, and sensitive data like the database credentials can be put in settings.local.php file, which will not be committed.

Version control: Git

Use a modern version control system to develop your projects. Git is the most popular choice for Drupal developers, as is used for Drupal core development and contributed modules.


Drupal already has a .gitignore file, and these two exists too:


Drush is awesome. Learn some drush commands:

Module building

Use always a standardized template for creating modules.

name = Custom Module
description = Description of the module.
core = 7.x



 * @file
 * Short file description here.
 * Long description here.

 * Implements hook_menu().
function custom_module_menu() {

  $items['example'] = array(
    'title' => 'Example Page',
    'page callback' => 'custom_module_page',
    'access arguments' => array('access content'),
    'type' => MENU_NORMAL_ITEM,

  return $items;

 * Page example callback, referenced in custom_module_menu().
 * Description of the page.
 * @return string
 *   The content of the page.
function custom_module_page() {

  return 'example';


 * @file custom_module.js
 * Custom Module main JavaScript file.
 * For how to add this file to your module, see
 * See also

(function ($, Drupal, window, document) {

  'use strict';

  Drupal.behaviors.customModule = {

    attach: function (context, settings) {

      console.log('Hello world');

      $('span.custom-module', context).once().text(Drupal.t('Hello world'));
})(jQuery, Drupal, this, this.document);

A boilerplate is available to download here. See also Module Builder.

Sanitization of sensitive data

Custom modules or features that add data structures expected to store sensitive information should implement hook_sql_sync_sanitize() to sanitize or remove that data. These functions will execute when running drush sql-sanitize and drush sql-sync --sanitize. These Drush commands are useful when moving the database to development environments.


Features module is used to keep the configuration in code. With it, the configuration that resides in the database can be exported inside modules, so the deployment can be automated, and the development can be better tracked in a version control system. Most of the core components can be exported, but for some others, a few modules are required. Some contrib modules provide exporting functionality, and some others add support via separate modules.

Core components exportable using Features by default:

  • Content types
  • Fields
  • Image styles
  • Languages
  • Menus
  • Feature dependencies (check Master module too)
  • Permissions (check Secure Permissions module too)
  • Roles (but check Role Export)
  • Taxonomies
  • Text formats

Contrib modules that provide export functionality for other components :

Contrib modules that provide export functionality:

The Kit specification is a set of guidelines that facilitates building compatible and interoperable Features. It's usually a good practice to not put more than one content type per feature and put all reusable stuff (shared fields, image styles, views...) in separate features.

With the help of Master module, all site dependencies can be controlled and enforced with a single drush command - and no more need to write update_hook_n() implementations for automating enabling and disabling modules.

Features are modules too. Code specific to the feature should reside in the feature's directory or .module file.

Building multiple sites

Multi-headed Drupal, an article by Larry Garfield, describes multiple approaches for building (or simulating) multiple sites in Drupal: Separate installs, Multi-site, Domain Access, and Organic Groups.

Single-site (separate installs) vs Multi-site

To understand how multi-site setups work, see The Power of Drupal Multi-site - Part 1: Code Management. On the other side, see why a multi-site setup may not be the right approach in Why Drupal Multisite Is Not Enterprise Grade.

Domain Access [domain]

Domain Access suite of modules is often used when pointing multiple domains to a single Drupal install. When Domain Access is used, multiple sites can be built using as a single database and code base, and users and content can be shared easily. But because many things are shared between sites, it's not very flexible when developing websites with different needs.

Check its project page to see all available options and modules. There is also a list of all Domain Access contributed modules.

Drupal multiple sites cheat sheet

The following table is extracted from a presentation by Larry Garfield:

If you want... Probably means...
Multiple core versions Separate installs
Different content types Separate installs, multi-site
Single user base Domain Access, OG, Single-site
Subdomains Separate installs, multi-site, Domain Access
One domain OG, Single-site
Simplified server admin Domain Access, OG, Single-site
Shared content Domain Access, OG, Single-site
Per-department ACL Single-site, OG
Department autonomy Separate installs, multi-site (less so OG)
Per-department Views Separate installs, multi-site (less so OG)
Staggered migration Separate installs, multi-site, Domain Access, proxy server?
Per department theme Separate installs, Multi-site, Domain Access, OG
Inexperienced site admins Separate installs, Single-site

Localization and translation

Read Drupal 7's new multilingual systems compilation by Gábor Hojtsy and Multilingual Guide at

A good book of multilingual site building on Drupal is Drupal 7 Multilingual Sites by Kristen Pol.

Entity Translation [entity_translation]

Drupal 7 came with entity translation support in core, and you can now translate fields. Unfortunately, the user interface was not completed. Entity Translation module partially solves that problem. You will need the Title module to translate the titles of the nodes and the names of the taxonomies.

The legacy way to translate content, Content Translation module, is still available as part of Drupal core, and in some circumstances may even be the preferred option. I would evade using both at the same time.

There is some overlapping of functionality with i18n module.

Internationalization [i18n]

i18n is an indispensable collection of modules that provide multilingual capabilities. Some of the modules included are Path Translation, String Translation, Taxonomy Translation, Block Translation, and Variable Translation. Unfortunately, I have found Variable Translation module (and Variable module) buggy. There is Internationalization contributions module too.

Functionality in Core


String placeholders (t(), watchdog(), format_plural()...)

@variable: Escaped to plain-text using check_plain(). Most common.

%variable: Escaped to HTML and formatted as emphasized.

!variable: Inserted as is, with no sanitization or formatting.

DB API / PDO placeholders

:variable: Escaped and safe.

String formatting & translation

t() function

You should use t() almost everywhere. Except for some places, where it's already done implicitly: hook_menu() implementations expect the title and description to be literal untranslated strings, and will run through t() later.



// Simple text
$output = t('Home');

// Simple link
$output = l(t('Home'), '/');

// Text containing variables
$output = t('Hi @username! Welcome to @sitename.', array(
  '@username' => $username,
  '@sitename' => $sitename,

// Text containing safe HTML.
$here_link = l(t('here'), '/');
$output = t('Click !home to navigate to the home page.', array(
  '!home' => "<span class='home-link'>$here_link</span>",

// In simple cases like above, adding some simple html inside the string is may be ok or even preferred.
// It can give more context to the translator.
$external_link = t('Look at Drupal documentation at <a href="@drupal-handbook">the Drupal Handbooks</a>.', array(
  '@drupal-handbook' => '',
$internal_link = t('Go to <a href="@administer-page">the Administer screen</a> in the main menu.', array(
  '@administer-page' => url('admin'),

// BAD examples:

// Translatable strings should be passed through t()
$output = 'Home';

// Text inside t() must not contain variables
$output = t("Hi $username! Welcome to $sitename.");

// Phrases should not be splitted and variables not concatenated, as grammar may differ
// between languages and that would make the translation difficult or impossible
$output = t("Hi ") . $username . t("! Welcome to ") . $sitename;

Other important localization functions


format_plural() helps presenting strings that contain counters.


$output = format_plural($apples, 'You have 1 apple', 'You have @count apples');

// Allows the same type of placeholders and options than t() function:
$output = format_plural($apples, 'You have 1 %fruit', 'You have @count %fruits', array(
  '%fruit' => $fruit_name,
  '%fruits' => $fruits_name,

Javascript Libraries

Apart from jQuery, Drupal already includes a few JavaScript libraries.

To get all available libraries run drupal_get_library('system');.

Most popular libraries are available in contrib modules too, so it is a good idea to do some research first.

Front-end best practices

Most front-end good practices apply to Drupal as well. Learn about HTML5 Boilerplate project and its documentation, there are a few interesting pages on its wiki.

Check out Modernizr and normalize.css projects.

CSS and JavaScript

CSS architecture and organization

The following documents were written for Drupal 8, but they can be useful for 7 too:



The Drupal way to include JavaScript and CSS assets is via render arrays. It's also, apparently, the only way to do it in Drupal 8. Usage of drupal_add_js() and drupal_add_css() is still common in Drupal 7 though.

If only a few lines of code are required and it must be added in very uncommon circumstances, inline code is usually preferable. For most cases, however, the standard is to keep the code in separate files inside the module or theme involved (these end up aggregated and compressed in production anyway). Check that per-page CSS/JS aggregation is not happening, as that would increase the number of required HTTP requests for each page visit.

In modules, do not add CSS and JavaScript using the modules *.info files. As noted above, most assets should be added conditionally, adding them using multiple approaches would be confusing. By contrast, theme assets (especially CSS) are usually added via the .info file.

All production sites should have JavaScript and CSS compressed and minified.



 * Example 1: JavaScript on every page using `drupal_add_js().`
drupal_add_js(drupal_get_path('module', 'custom_module') . '/js/custom_module.js');

 * Example 2: Small inline code in a specific place using `drupal_add_js().`
drupal_add_js("console.log('hello world');", array('type' => 'inline'));

 * Example 3: Similar to example 2, inline code but getting it from a file.
$code = file_get_contents(drupal_get_path('module', 'custom_module') . '/custom_module.js');
$options = array(
  'type' => 'inline',
  'scope' => 'footer',  // Bottom of the file is usually better
drupal_add_js($code, $options);

 * Example 4: Code attached to a render array.
function custom_module_callback_example_page() {
  $module_dir = drupal_get_path('module', 'custom_module');

  return array(
    'output' => array(
      '#theme' => 'custom_module_theme_function_or_template',
      '#attached' => array(
        'js' => array(
          $module_dir . '/js/custom_module.js' => array(),
        'css' => array(
          $module_dir . '/css/custom_module.css' => array(),


Images are a big chunk of all bytes downloaded by a web browser and should be optimized. Especially for PNG images, lossy compression usually saves a considerable bandwidth with high-quality results (try it). Images that are part of the theme should always be manually compressed using tools such as ImageOptim, ImageAlpha and or FileOptimizer. For content managed images, server-side compression may be automated using tools such as pngquant and pngout.

To reduce the number of HTTP requests, background images can be added using image sprites, for example using Compass.

ImageMagick (or GraphicsMagick) should be used instead of PHP-GD. GD compresses with less quality, has fewer features and is not efficient.


Base theme

Writing a theme from scratch is an excellent way to learn Drupal. However, in most circumstances, using a suitable base theme for your project will be more appropriate.

A good base theme:

  • Follows Drupal standards and best practices
  • Follows Web best practices (standards compliance, accessibility, browser compatibility)
  • Has compatibility with common Drupal features (menus, breadcrumbs)
  • Has compatibility with common contrib modules (Panels, Display Suite)

Examples of good, well-maintained base themes are:

Administration theme

The default administration theme for Drupal 7, called Seven, is probably the best administration theme ever built for Drupal.

Useful modules

Entity Reference [entityreference]

Supersedes References module, providing fields, widgets, and formatters for nodes, users, and other entities. Note that taxonomy references support is already provided by core, inside the Taxonomy module itself.

Rabbit Hole [rabbit_hole]

If you have a bundle (e.g., a content type) that is not meant to be publicly available on its own - i.e., you don't want the "full content" page to be accessible - you can restrict the access to it using Rabbit Hole module. Page Manager module could also be used for that, although that would be slightly more complicated, as involves setting up conditions.

For these cases, make sure you remove the default Pathauto pattern for all content, and of course that you don't have any specific pattern for them. Otherwise, aliases for these nodes are going to be created automatically. The small Pathauto Enforce module can be used to mitigate this problem.

Ultimate Cron [ultimate_cron]

Ultimate Cron is a fork of Elysia Cron module, both alternatives that significantly improve Drupal cron core functionality. If you need to run cron often, or you have separate cron implementations that need to be run at different intervals, you are likely to use one of these modules. I find Ultimate Cron better maintained than Elysia Cron.

Devel [devel]

A must-have for any Drupal developer. Very useful for profiling and debugging database queries, page timings, and memory usage. Integrates with XHProf. Popular features are dpm() function, /devel/php path and Generate content functionality. Do not enable it on production.

Display Suite [ds]

The Display Suite module provides a nice way for creating view modes of entities using the graphical user interface. Display Suite is a compelling solution that supersedes a lot of other contrib modules. For example, Entity view modes is no longer necessary and the same functionality that Field Extractor provides can already be achieved using Display Suite out of the box.

Alongside with Field Group module, can also be used to create and configure layouts using the GUI.

The only circumstance where (maybe) I wouldn't use Display Suite is when its user interface is not used at all, and when all view modes, node layouts, and custom fields are done in code. Display Suite allows doing everything in code too, but if the user interface is not used, the module is probably overkill.

Check the bundled Display Suite Extras module to extend even more its functionality.

Panels [panels]

Panels module is popular and versatile, and is usually chosen when content managers need the power to control the layout of the pages. Unfortunately adds a lot of complexity to the site. Use it with Panels IPE (In-Place Editor) module, which comes bundled. Panels is the base of Panopoly and Demo Framework distributions. Is usually used alongside Mini Panels (bundled), Fieldable Panel Panes, Panelizer, Page manager existing pages, Panels everywhere or Panels Extra Styles modules. For a brew introduction to Panels, see Using Panels without Panelizer.

CKEditor [ckeditor]

I've found CKEditor + CKEditor module the best (but not perfect) solution to put a WYSIWYG editor in a Drupal site. For the best results, try restricting the editors as much as possible, for example allowing them to choose CSS classes from a predefined list (putting a ckeditor.styles.js file in your theme directory). Another popular option is WYSIWYG module.

Consider using markdown instead.

Redirect [redirect]

At the time of writing, Redirect module is still not a total replacement to Global Redirect module, which means that unfortunately, most sites will need both.

Secure Permissions [secure_permissions]

Consider using the Secure Permissions module to keep the permissions in code, disallowing content managers changing them.

Field API extensions and improvements

Provides a "link" field type. It's stable, but unfortunately, it's not a very thin implementation.

Date [date]

Provides a "date" field type. Includes Date Popup, and can be used with Date Popup Authored module. Using the Date (ISO) format for fields is recommended.

Telephone [telephone]

Provides a "phone" field type.

Email Field [email]

Provides an "email" field type.

Address Field [addressfield]

Yes, this one provides a field type to be used for storing physical addresses.

Field Group [field_group]

Use it to organize the fields in groups. Two different kinds of groups can be created, "Form groups", that can be used to better organize the entity edit pages (e.g. grouping the fields in vertical tabs) and "Display groups", which are used to control the layout and display of them (e.g. to output multiple fields wrapped in a single div). Plays nicely with Display Suite. See its module project page for extending its capabilities using other contrib modules. For example, Field Group Link allows you to output the containing fields as a link.

Field extra widgets [field_extrawidgets]

Formatter Field [formatter_field]

Block Reference [blockreference]

Useful to include blocks as part of the node displays.

Backports [backports]

Small usability improvements from Drupal 8 (mostly related to the Field UI).

Fences [fences]

Considerably reduces the default Drupal HTML output of fields.

Form API extensions

Elements [elements]

Extended Form API Elements. Most of them are available in Drupal 8.

Form API Validation [fapi_validation]

Clientside Validation [clientside_validation]

Other UX improvements

Simplified Menu Administration [simplified_menu_admin]

Simplifies the core Menu and Shortcut modules by merging the "List links" and "Edit menu" operations into a single administration page. Small backport of Drupal 8.

Mobile Friendly Navigation Toolbar [navbar]

A very simple mobile-friendly navigation toolbar introduced as part of the Spark project

Block visibility [block_visibility]

A minor enhancement of the Blocks listing page, displaying the block visibility.

Chosen [chosen]

Chosen uses the Chosen jQuery plugin to make your <select> elements more user-friendly. Autocomplete Deluxe it's another approach to do more of the same, but adds the dependency of the Taxonomy module.

Multiple Selects [multiple_selects]

Multiple Selects module provide the same functionality than Chosen, but displaying multiple dropdowns instead. It is not restricted to <select> elements (so allows sorting).

Escape Admin [escape_admin]

Pathologic [pathologic]

Input filter which that tries to correct paths in links and images.

Create custom contextual links.

Blockify [blockify]

Expose some things as Drupal blocks (logo, site name, etc.)

Transliteration [transliteration]

Restrict and convert filenames to US-ASCII characters. Can be used with Pathauto module to transliterate the generated URLs.

Webform [webform]

If content managers need the ability to create forms, consider using it. For developers, using Form API directly may be a better approach.

Metatag [metatag]

This indispensable suite of modules allows you to provide structured metadata to be used by search engines and social networks. This includes things such as Open Graph (Facebook) and Twitter cards. As it allows changing the metadata for each node, adds a bit of noise to the administration forms.

XML sitemap [xmlsitemap]

Menu block [menu_block]

Menu Position Rules [menu_position]

Taxonomy menu [taxonomy_menu]

Mollom [mollom]

Honeypot [honeypot]

Google Analytics [google_analytics]

Backup and Migrate [backup_migrate]

Style Guide [styleguide]

Stage File Proxy [stage_file_proxy]

Administration Menu [admin_menu]

Environment indicator [environment_indicator]

Icon Tabs [icon_tabs]

Git Status [git_status]

Flag [flag]

Voting API [voting_api]

Env [env]

Inline Entity Form [inline_entity_form]

Migrate [migrate]

Taxonomy Views Integrator [tvi]

Feeds [feeds]

Taxonomy Menu [taxonomy_menu]

Charts [charts]

ImageCache Actions [imagecache_actions]

Email Registration [email_registration]

reCAPTCHA [recaptcha]

CAPTCHA [captcha]

Logintoboggan [logintoboggan]

Enhance the login behaviour

Performance and Scalability modules


Unnecessary Core Modules

(most of the times)

  • Aggregator
  • Blog
  • Book
  • Comment
  • Contact
  • Dashboard
  • Forum
  • Help
  • RDF
  • Search
  • Shortcut
  • Statistics
  • Trigger
  • Tracker
  • Toolbar
  • Overlay
  • OpenID
  • PHP filter
  • Poll
  • Profile

See also: Modules and themes removed from core in Drupal 8

Deployment, Testing and Server stuff

HTTP Servers

See some useful HTTP server configuration examples in HTML5 Boilerplate Server Configurations project.


Making our site faster with Varnish is a good introductory article about Varnish in Drupal. For a good working example of a VCL configuration file see

When using Varnish or similar, this can be put in settings.php to disable Drupal's page cache completely:

    if (!class_exists('DrupalFakeCache')) {
      $conf['cache_backends'][] = 'includes/';
    // Rely on the external cache for page caching.
    $conf['cache_class_cache_page'] = 'DrupalFakeCache';

Apache Solr is the most common option to provide search functionality in big sites.

PHP Settings

All Drupal sites should have PHP configured with opcode cache.

Replacing Drupal's core cache with Memcached or Redis

Both Memcached and Redis can be used as a replacement for Drupal core's cache. Redis module does not work correctly with UTF-8 characters in paths.