Scaffold a New Drupal 11 Module
You are creating a new Drupal module that follows this team's coding standards. Getting the structure right from the start prevents a class of technical debt — wrong namespaces, missing service definitions, procedural hooks that should be OOP, and missing config schemas all compound over time.
References library: references/
Step 1: Gather requirements
If the user hasn't provided enough information, ask:
- Module machine name (lowercase, underscores, e.g.
my_module) - What it does — one sentence
- What components it needs — check which apply:
- Services (injectable business logic)
- Routes and controllers
- Forms
- Plugins (Block, FieldType, QueueWorker, etc.)
- Content entity (custom node-like thing)
- Config entity (exportable configuration)
- Event subscribers
- Hooks (node presave, entity insert, etc.)
- Drush commands
- Tests
Step 2: Load relevant reference files
Always read these before generating:
01-module-architecture.md— structure, info.yml, naming conventions14-oop-hooks.md— if the module needs hooks15-modern-php.md— PHP 8.4/8.5 patterns that apply to all generated code
Then read the relevant component references:
02-services-dependency-injection.md— if services are needed03-plugins.md— if plugins are needed04-entities.md— if entities are needed05-routing-access.md— if routes/controllers are needed06-forms.md— if forms are needed13-deployment.md— if Drush commands are needed
Step 3: Generate the module
Create files in the current working directory under [module_name]/. Generate only what is needed for the stated requirements — don't create empty placeholder files.
Files to always generate
[module_name].info.yml
name: '[Human-readable name]'
type: module
description: '[One-sentence description]'
core_version_requirement: ^11
package: Custom
hooks_converted: true
dependencies: []
src/ directory with PSR-4 classes — namespace Drupal\[module_name]
Conditional files
Generate these only when the module needs them:
| Component | Files to create |
|---|---|
| Services | [module].services.yml, src/[ServiceName].php with constructor property promotion |
| Controller + route | [module].routing.yml, src/Controller/[Name]Controller.php |
| Form | src/Form/[Name]Form.php extending FormBase or ConfigFormBase |
| Block plugin | src/Plugin/Block/[Name]Block.php with #[Block(...)] attribute |
| Queue worker | src/Plugin/QueueWorker/[Name]Worker.php with #[QueueWorker(...)] attribute |
| Content entity | src/Entity/[Name].php with #[ContentEntityType(...)] attribute, interface, list builder, form handlers |
| Config entity | src/Entity/[Name].php with #[ConfigEntityType(...)] attribute |
| Hooks | src/Hook/[ModuleName]Hooks.php with #[Hook('hook_name')] methods; register in services.yml |
| Drush commands | src/Commands/[ModuleName]Commands.php with #[AsCommand(...)] and AutowireTrait |
| Tests | tests/src/Unit/, tests/src/Kernel/, tests/src/Functional/ as needed |
| Config schema | config/schema/[module].schema.yml — required if module introduces any configuration |
PHP patterns to apply in all generated code
These aren't optional — they're the team's current standard and what code review will check for:
Services and injection
// Always constructor property promotion + readonly for injected services
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly ConfigFactoryInterface $configFactory,
) {}
// Static factory for service container
public static function create(ContainerInterface $container): static {
return new static(
$container->get('entity_type.manager'),
$container->get('config.factory'),
);
}
Hooks — OOP only
// src/Hook/MyModuleHooks.php
namespace Drupal\my_module\Hook;
use Drupal\Core\Hook\Attribute\Hook;
class MyModuleHooks {
#[Hook('node_presave')]
public function nodePresave(NodeInterface $node): void {
// logic here
}
}
Register in *.services.yml:
services:
my_module.hooks:
class: Drupal\my_module\Hook\MyModuleHooks
tags:
- { name: 'module_hook' }
Never put hooks as procedural functions in *.module (except hook_schema, hook_install, hook_update_N).
Plugin attributes — never Doctrine annotations
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\StringTranslation\TranslatableMarkup;
#[Block(
id: 'my_block',
admin_label: new TranslatableMarkup('My Block'),
category: new TranslatableMarkup('Custom'),
)]
class MyBlock extends BlockBase { ... }
Type declarations everywhere
// All parameters, return types, and properties must be typed
public function process(int $id, string $label): array { ... }
public function save(): void { ... }
Step 4: After generating
Tell the user:
- What files were created and why
- What to do next (e.g., "Run
drush crto clear caches after enabling the module") - Which reference files to read for the next steps (e.g., "See
03-plugins.mdif you need to add more plugin types") - Any decisions that were made on their behalf (e.g., "I used
EditorialContentEntityBasebecause you mentioned publishing workflow — change toContentEntityBaseif you don't need revision/publishing support")