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 corecontrib/→ downloaded modulescustom/→ 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)
- User requests URL
- Routing matches
- Controller runs
- Controller uses services
- Render array returned
- Theme renders output
Common beginner mistakes (exam traps)
- Writing logic in controllers
- Using
newinstead 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.