Module 4.2 – Custom Modules

This deep dive explains Drupal custom modules from absolute basics to real Drupal architecture but using correct technical terms so you grow into a Drupal SME.

If you understand this document, you understand how Drupal really works.

A custom module is a folder that tells Drupal:

“Hey Drupal, I have my own PHP code to extend your behavior.”

Drupal core already does a lot.
Modules are how developers add logic.

Themes = look
Modules = behavior


Where custom modules live

web/modules/custom/my_module/

Why:

  • core/ → Drupal core
  • contrib/ → downloaded modules
  • custom/your code

Exam signal:
If the question asks where custom code goes → custom module.


The smallest possible module

1. my_module.info.yml

name: My Module
type: module
description: 'My first custom module'
core_version_requirement: ^11
package: Custom

What this does (kid version):

  • Tells Drupal this module exists

What this does (developer version):

  • Registers module metadata

PHP file basics (from zero)

What is <?php

<?php

This tells the server:

“Everything below is PHP code.”

Without this, PHP will not run.


Comments (what and why)

Single line comment

// This is a comment

Multi-line comment

/*
 This is a comment
*/

Why comments matter:

  • Humans read code
  • Drupal uses comments for annotations

use keyword

use Drupal\Core\Controller\ControllerBase;

Kid explanation:
Instead of writing the full long name every time, we give it a shortcut.

Technical explanation:

  • Imports a class into the file
  • Avoids writing full namespace repeatedly

Namespaces (folder = name rule)

namespace Drupal\my_module\Controller;

Why this exists:

  • Avoids class name conflicts
  • Tells Drupal where the class lives

Rule:

src/Controller/MyController.php
→ namespace Drupal\my_module\Controller

Class (what is it)

class MyController {
}

Kid explanation:
A class is a blueprint.

Developer explanation:

  • Defines behavior
  • Can be reused

Drupal uses classes everywhere.


Extends (inheritance)

class MyController extends ControllerBase {
}

What it means:

  • MyController gets everything from ControllerBase

Why Drupal uses it:

  • Shared helpers
  • Service access

Exam signal:
Controllers almost always extend ControllerBase.


Properties (variables in a class)

protected $logger;

Kid explanation:
A variable that belongs to the class.

Why protected:

  • Child classes can use it
  • Outside code cannot

Other options:

  • public (avoid)
  • private (too strict sometimes)

Constructor (__construct)

public function __construct($logger) {
  $this->logger = $logger;
}

Kid explanation:
Constructor runs when the class is born.

Drupal explanation:

  • Receives services via DI
  • Stores them for later use

Dependency Injection (DI) – again, very simple

Wrong way:

$logger = new Logger();

Right way:

public function __construct(LoggerChannelFactoryInterface $logger_factory) {
  $this->logger = $logger_factory->get('my_module');
}

Why:

  • Drupal controls services
  • Easier testing

Service container (create() method)

public static function create(ContainerInterface $container) {
  return new static(
    $container->get('logger.factory')
  );
}

What this does:

  • Drupal calls this
  • Passes services

You never call create() yourself.


Routing (URL → code)

my_module.routing.yml

my_module.hello:
  path: '/hello'
  defaults:
    _controller: '\Drupal\\my_module\\Controller\\MyController::hello'
    _title: 'Hello'
  requirements:
    _permission: 'access content'

What happens:

  • User visits /hello
  • Drupal finds controller

Controller method

public function hello() {
  return [
    '#markup' => 'Hello Drupal',
  ];
}

Why return array:

  • Drupal uses render arrays
  • Supports caching

Hooks (procedural but still used)

my_module.module

function my_module_entity_presave($entity) {
  // Runs before entity save
}

Hooks are:

  • Event listeners
  • Global extension points

Services (logic lives here)

my_module.services.yml

services:
  my_module.helper:
    class: Drupal\my_module\MyHelper

Service class:

class MyHelper {
  public function doWork() {}
}

Controllers should be thin.
Services do work.


Full custom module flow (mental model)

  1. User requests URL
  2. Routing matches
  3. Controller runs
  4. Controller uses services
  5. Render array returned
  6. Theme renders output

Common beginner mistakes (exam traps)

  • Writing logic in controllers
  • Using new instead of DI
  • Mixing theme and module logic
  • Returning HTML instead of render arrays

How Acquia expects you to answer

Correct answers:

  • Use services
  • Use routing + controllers
  • Use Entity API
  • Separate concerns

Wrong answers:

  • Raw PHP scripts
  • Inline SQL
  • Logic in Twig

One‑sentence summary

A custom module extends Drupal by using routing, controllers, services, and hooks, written in OOP PHP, wired together by Dependency Injection, and rendered via render arrays.