Drupal Development

Drupal Development Guide

This comprehensive guide covers Drupal CMS development, including installation, module development, theming, security, and best practices for building robust Drupal applications.

Drupal Overview

Drupal is a free, open-source content management system (CMS) written in PHP. It’s known for its flexibility, scalability, and extensive customization capabilities through modules and themes.

Key Features

  • Modular Architecture: Extensive module ecosystem
  • Flexible Content Types: Custom content structures
  • Powerful Taxonomy: Hierarchical classification system
  • User Management: Role-based permissions
  • Multilingual Support: Built-in internationalization
  • API-First: Headless capabilities with JSON:API
  • Security: Strong security track record

Installation and Setup

System Requirements

  • PHP: 8.1 or higher
  • Database: MySQL 5.7.8+ / MariaDB 10.3.7+ / PostgreSQL 12+
  • Web Server: Apache/Nginx with mod_rewrite
  • Memory: Minimum 256MB, recommended 512MB+

Installation Methods

# Install Drupal using Composer
composer create-project drupal/recommended-project my-drupal-site
cd my-drupal-site

# Install dependencies
composer install

# Set up web server to point to web/ directory

Drush Installation

# Install Drush globally
composer global require drush/drush

# Download Drupal core
drush dl drupal

# Install Drupal
drush site-install standard \
  --db-url='mysql://username:password@localhost/databasename' \
  --site-name='My Drupal Site' \
  --account-name=admin \
  --account-pass=admin

Directory Structure

my-drupal-site/
├── composer.json
├── vendor/           # Composer dependencies
├── web/             # Drupal core and sites
│   ├── core/        # Drupal core
│   ├── modules/     # Contributed modules
│   │   ├── contrib/ # Downloaded modules
│   │   └── custom/  # Custom modules
│   ├── themes/      # Themes
│   │   ├── contrib/ # Downloaded themes
│   │   └── custom/  # Custom themes
│   ├── sites/       # Multisite configuration
│   └── index.php
├── config/          # Configuration export
└── drush/           # Drush configuration

Module Development

Creating a Custom Module

Module Structure:

modules/custom/my_module/
├── my_module.info.yml    # Module metadata
├── my_module.module      # Main module file
├── my_module.routing.yml # Route definitions
├── src/                  # PHP classes
│   ├── Controller/
│   ├── Plugin/
│   ├── Form/
│   └── Service/
├── templates/            # Twig templates
├── css/                  # Stylesheets
├── js/                   # JavaScript files
└── config/               # Configuration schemas

Module Info File (my_module.info.yml):

name: 'My Custom Module'
description: 'Provides custom functionality for my site.'
package: 'Custom'
type: module
core_version_requirement: ^9 || ^10
dependencies:
  - drupal:node
  - drupal:user
configure: my_module.settings

Basic Module File (my_module.module):

<?php

use Drupal\Core\Routing\RouteMatchInterface;

/**
 * @file
 * Contains my_module.module.
 */

/**
 * Implements hook_help().
 */
function my_module_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.my_module':
      return '<p>' . t('Help text for My Module.') . '</p>';
  }
}

/**
 * Implements hook_theme().
 */
function my_module_theme() {
  return [
    'my_custom_template' => [
      'variables' => [
        'title' => NULL,
        'content' => NULL,
      ],
    ],
  ];
}

Controllers

Basic Controller:

<?php

namespace Drupal\my_module\Controller;

use Drupal\Core\Controller\ControllerBase;

/**
 * Returns responses for My Module routes.
 */
class MyModuleController extends ControllerBase {

  /**
   * Builds the response.
   */
  public function build() {
    $build['content'] = [
      '#type' => 'item',
      '#markup' => $this->t('Hello, Drupal!'),
    ];

    return $build;
  }

}

Routing

Routing File (my_module.routing.yml):

my_module.content:
  path: '/my-module'
  defaults:
    _controller: '\Drupal\my_module\Controller\MyModuleController::build'
    _title: 'My Module'
  requirements:
    _permission: 'access content'

Forms

Custom Form:

<?php

namespace Drupal\my_module\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * Class MyModuleForm.
 */
class MyModuleForm extends FormBase {

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'my_module_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['name'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Name'),
      '#required' => TRUE,
    ];

    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->messenger()->addMessage($this->t('Form submitted successfully!'));
  }

}

Services

Service Definition (my_module.services.yml):

services:
  my_module.custom_service:
    class: Drupal\my_module\Service\CustomService
    arguments: ['@entity_type.manager', '@current_user']

Service Class:

<?php

namespace Drupal\my_module\Service;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountProxyInterface;

/**
 * Class CustomService.
 */
class CustomService {

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountProxyInterface
   */
  protected $currentUser;

  /**
   * Constructs a new CustomService object.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, AccountProxyInterface $current_user) {
    $this->entityTypeManager = $entity_type_manager;
    $this->currentUser = $current_user;
  }

  /**
   * Custom business logic.
   */
  public function doSomething() {
    // Implementation here
  }

}

Theming

Theme Structure

themes/custom/my_theme/
├── my_theme.info.yml      # Theme metadata
├── my_theme.libraries.yml # Asset libraries
├── my_theme.breakpoints.yml # Responsive breakpoints
├── css/                   # Stylesheets
├── js/                    # JavaScript files
├── images/                # Theme images
├── templates/             # Twig templates
├── includes/              # PHP includes
└── config/                # Theme configuration

Theme Info File (my_theme.info.yml):

name: 'My Custom Theme'
description: 'A custom theme for my Drupal site.'
type: theme
base theme: stable9
core_version_requirement: ^9 || ^10
libraries:
  - my_theme/global
regions:
  header: 'Header'
  content: 'Content'
  sidebar: 'Sidebar'
  footer: 'Footer'

Twig Templates

Custom Node Template (node–article–full.html.twig):

{#
/**
 * @file
 * Theme override for a node in full view mode.
 */
#}
<article{{ attributes.addClass('node--type-' ~ node.bundle|clean_class, 'node--view-mode-' ~ view_mode|clean_class) }}>

  {{ title_prefix }}
  <h1{{ title_attributes }}>
    <a href="{{ url }}" rel="bookmark">{{ label }}</a>
  </h1>
  {{ title_suffix }}

  {% if display_submitted %}
    <div class="node__meta">
      {{ author_picture }}
      <span{{ author_attributes }}>
        {% trans %}Submitted by {{ author_name }} on {{ date }}{% endtrans %}
      </span>
      {{ metadata }}
    </div>
  {% endif %}

  <div{{ content_attributes.addClass('node__content') }}>
    {{ content }}
  </div>

</article>

Asset Libraries

Libraries File (my_theme.libraries.yml):

global:
  version: VERSION
  css:
    theme:
      css/style.css: {}
  js:
    js/script.js: {}
  dependencies:
    - core/jquery
    - core/drupal

custom-page:
  css:
    theme:
      css/custom-page.css: {}

Preprocessing Functions

template.php or .theme file:

<?php

/**
 * @file
 * Functions to support theming in the My Theme theme.
 */

/**
 * Implements hook_preprocess_node().
 */
function my_theme_preprocess_node(&$variables) {
  $node = $variables['node'];

  // Add custom classes based on content type
  $variables['attributes']['class'][] = 'node--type-' . $node->bundle();

  // Add custom variables
  $variables['custom_field'] = $node->get('field_custom')->value;
}

/**
 * Implements hook_preprocess_page().
 */
function my_theme_preprocess_page(&$variables) {
  // Add body classes
  $variables['attributes']['class'][] = 'page-' . \Drupal::routeMatch()->getRouteName();
}

Content Types and Fields

Creating Content Types Programmatically

Hook Implementation:

<?php

use Drupal\node\Entity\NodeType;

/**
 * Implements hook_install().
 */
function my_module_install() {
  // Create content type
  $node_type = NodeType::create([
    'type' => 'article',
    'name' => 'Article',
    'description' => 'Use articles for time-sensitive content like news, press releases or blog posts.',
  ]);
  $node_type->save();

  // Add body field
  $field_storage = \Drupal::entityTypeManager()
    ->getStorage('field_storage_config')
    ->create([
      'field_name' => 'body',
      'entity_type' => 'node',
      'type' => 'text_with_summary',
    ]);
  $field_storage->save();

  $field = \Drupal::entityTypeManager()
    ->getStorage('field_config')
    ->create([
      'field_name' => 'body',
      'entity_type' => 'node',
      'bundle' => 'article',
      'label' => 'Body',
    ]);
  $field->save();
}

Custom Fields

Field Type Creation:

<?php

namespace Drupal\my_module\Plugin\Field\FieldType;

use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;

/**
 * @FieldType(
 *   id = "my_custom_field",
 *   label = @Translation("My Custom Field"),
 *   description = @Translation("A custom field type."),
 *   category = @Translation("Custom"),
 *   default_widget = "my_custom_widget",
 *   default_formatter = "my_custom_formatter"
 * )
 */
class MyCustomField extends FieldItemBase {

  /**
   * {@inheritdoc}
   */
  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
    $properties['value'] = DataDefinition::create('string')
      ->setLabel(t('Value'));

    return $properties;
  }

  /**
   * {@inheritdoc}
   */
  public static function schema(FieldStorageDefinitionInterface $field_definition) {
    return [
      'columns' => [
        'value' => [
          'type' => 'varchar',
          'length' => 255,
        ],
      ],
    ];
  }

}

Drupal Security

Writing Secure Code

Input Sanitization:

// JavaScript sanitization
var rawInputText = $('#form-input').text();
var escapedInputText = Drupal.checkPlain(rawInputText);

// PHP sanitization
$safe_output = \Drupal\Component\Utility\Html::escape($user_input);
$admin_safe = \Drupal\Component\Utility\Xss::filterAdmin($input);

SQL Injection Prevention:

// Using query with placeholders
$result = \Drupal::database()->query(
  'SELECT foo FROM {table} t WHERE t.name = :name',
  [':name' => $_GET['user']]
);

// Using select API
$users = ['joe', 'poe', $_GET['user']];
$result = \Drupal::database()->select('foo', 'f')
  ->fields('f', ['bar'])
  ->condition('f.bar', $users)
  ->execute();

// LIKE queries with escaping
$conn = \Drupal::database();
$result = $conn->select('table', 't')
  ->condition('t.field', '%' . $conn->escapeLike($user), 'LIKE')
  ->execute();

Access Control

Permissions:

/**
 * Implements hook_permission().
 */
function my_module_permission() {
  return [
    'access my_module content' => [
      'title' => t('Access My Module content'),
      'description' => t('Allow access to My Module content.'),
    ],
  ];
}

Node Access:

/**
 * Implements hook_node_access().
 */
function my_module_node_access(\Drupal\node\NodeInterface $node, $op, \Drupal\Core\Session\AccountInterface $account) {
  if ($node->bundle() === 'restricted_content' && $op === 'view') {
    return \Drupal\Core\Access\AccessResult::forbiddenIf(!$account->hasPermission('view restricted content'));
  }

  return \Drupal\Core\Access\AccessResult::neutral();
}

CSRF Protection

Form CSRF Protection:

$form['#token'] = 'my_custom_form';
$form['actions']['submit'] = [
  '#type' => 'submit',
  '#value' => t('Submit'),
];

Route CSRF Protection:

my_module.custom_route:
  path: '/my-custom-path'
  defaults:
    _controller: '\Drupal\my_module\Controller\MyController::customAction'
  requirements:
    _csrf_token: 'TRUE'

XSS Prevention

Twig Auto-escaping:

{# Safe - auto-escaped #}
{{ user_input }}

{# Explicit escaping #}
{{ user_input|escape }}

{# Raw output (use with caution) #}
{{ user_input|raw }}

PHP XSS Protection:

// Use Drupal's sanitization functions
$safe_markup = \Drupal\Component\Utility\Xss::filter($user_input);
$admin_markup = \Drupal\Component\Utility\Xss::filterAdmin($user_input);

Drush Commands

Common Drush Commands

Site Management:

# Clear all caches
drush cr

# Rebuild permissions
drush php-eval 'node_access_rebuild();'

# Update database
drush updb

# Import/export configuration
drush config-export
drush config-import

Content Management:

# Create user
drush user-create username --mail="user@example.com" --password="password"

# Generate content
drush generate-content 10 node --types=article

# List users
drush user-list

Module Management:

# Enable module
drush en my_module

# Disable module
drush pmu my_module

# Uninstall module
drush pm-uninstall my_module

Performance Optimization

Caching

Cache Tags:

$build['#cache'] = [
  'tags' => ['node:' . $node->id()],
  'contexts' => ['user'],
  'max-age' => 3600,
];

Custom Cache:

$cid = 'my_module:data:' . $user->id();
$data = \Drupal::cache()->get($cid);

if (!$data) {
  $data = expensive_operation();
  \Drupal::cache()->set($cid, $data, time() + 3600);
}

Database Optimization

Efficient Queries:

// Use entity queries for better performance
$query = \Drupal::entityQuery('node')
  ->condition('type', 'article')
  ->condition('status', 1)
  ->range(0, 10);

$nids = $query->execute();
$nodes = \Drupal::entityTypeManager()
  ->getStorage('node')
  ->loadMultiple($nids);

Image Optimization

Responsive Images:

$responsive_image = [
  '#theme' => 'responsive_image',
  '#responsive_image_style_id' => 'my_responsive_style',
  '#uri' => $file_uri,
];

Testing

Unit Testing

Test Class:

<?php

namespace Drupal\Tests\my_module\Unit;

use Drupal\Tests\UnitTestCase;

/**
 * @coversDefaultClass \Drupal\my_module\Service\CustomService
 * @group my_module
 */
class CustomServiceTest extends UnitTestCase {

  /**
   * @covers ::doSomething
   */
  public function testDoSomething() {
    $service = new CustomService();
    $result = $service->doSomething();
    $this->assertEquals('expected', $result);
  }

}

Functional Testing

Functional Test:

<?php

namespace Drupal\Tests\my_module\Functional;

use Drupal\Tests\BrowserTestBase;

/**
 * @group my_module
 */
class MyModuleTest extends BrowserTestBase {

  /**
   * {@inheritdoc}
   */
  protected static $modules = ['my_module'];

  /**
   * Test custom functionality.
   */
  public function testCustomFunctionality() {
    $this->drupalGet('/my-module');
    $this->assertSession()->statusCodeEquals(200);
    $this->assertSession()->pageTextContains('Hello, Drupal!');
  }

}

Deployment

Configuration Management

Export Configuration:

drush config-export --destination=/path/to/config

Import Configuration:

drush config-import --source=/path/to/config

Environment-Specific Settings

settings.php:

<?php

// Database configuration
$databases['default']['default'] = [
  'database' => getenv('DB_NAME'),
  'username' => getenv('DB_USER'),
  'password' => getenv('DB_PASS'),
  'host' => getenv('DB_HOST'),
  'driver' => 'mysql',
];

// Trusted host patterns
$settings['trusted_host_patterns'] = [
  '^example\.com$',
  '^.+\.example\.com$',
];

// Environment indicator
$config['system.site']['name'] = getenv('SITE_NAME') ?: 'My Drupal Site';

Best Practices

Code Standards

  1. Follow Drupal Coding Standards: Use Drupal Code Sniffer
  2. Use Dependency Injection: Avoid static calls when possible
  3. Implement Proper Access Control: Check permissions appropriately
  4. Sanitize All Input: Never trust user input
  5. Use Render Arrays: Build output declaratively
  6. Cache Aggressively: Implement appropriate caching strategies

Module Development

  1. Keep Modules Focused: Single responsibility principle
  2. Use Hooks Appropriately: Prefer events for complex logic
  3. Document Your Code: Comprehensive inline documentation
  4. Test Thoroughly: Unit and functional tests
  5. Version Properly: Semantic versioning
  6. Provide Updates: Safe configuration updates

Performance

  1. Optimize Database Queries: Use indexes and efficient queries
  2. Implement Caching: Cache rendered output and expensive operations
  3. Use CDNs: Serve static assets from CDNs
  4. Minimize HTTP Requests: Combine and compress assets
  5. Monitor Performance: Use tools like New Relic or Blackfire

Security

  1. Regular Updates: Keep Drupal core and modules updated
  2. Strong Passwords: Enforce password policies
  3. Two-Factor Authentication: Enable 2FA for admin accounts
  4. Regular Backups: Automated backup strategies
  5. Security Modules: Use contributed security modules
  6. Audit Logs: Monitor and log security events

This guide provides a comprehensive foundation for Drupal development, covering everything from basic installation to advanced module development, theming, and security practices.