Drupal 11.5 .module deprecated is an important change for developers maintaining custom and contributed modules. Starting with Drupal 11.5, the .module file extension is deprecated, and developers should begin moving procedural code toward modern object-oriented Drupal APIs before Drupal 13.
This guide explains what the Drupal 11.5 .module deprecation means, which code needs to be migrated, and how to prepare custom and contributed modules for Drupal 13.
Existing modules will not suddenly stop working when upgrading to Drupal 11.5. However, Drupal is moving toward the complete removal of automatic .module file loading, and autoloading of .module files will be removed in Drupal 13.
For custom and contributed module developers, this is the right time to start migrating procedural code to Drupal's modern object-oriented APIs.
What Changed?
Starting with Drupal 11.5.0, files such as:
my_module.module
are deprecated.
Drupal 11.5 still supports the existing mechanism, but developers are expected to migrate away from it.
The important part is:
Support for autoloading the
.modulefiles will be removed in Drupal 13.
Therefore, simply keeping procedural code such as:
function my_module_some_hook() {
// ...
}
is not a future-proof solution.
The code should be migrated to the appropriate object-oriented replacement.
Why Is Drupal Moving Away from .module Files?
Historically, Drupal relied heavily on procedural PHP code.
For example:
function example_node_insert(NodeInterface $node) {
// ...
}
The function would normally live in:
example.module
Drupal would discover it as a hook implementation.
This approach has been part of Drupal's architecture for many years.
Modern Drupal, however, increasingly uses object-oriented APIs, dependency injection, services, plugins, and PHP attributes.
For hooks, this means using classes with attributes such as:
#[Hook('hook_name')]
instead of procedural implementations in .module files.
From Procedural Hooks to #[Hook]
Consider a traditional implementation of hook_help():
<?php
/
* Implements hook_help().
*/
function example_help($route_name, $route_match) {
if ($route_name === 'help.page.example') {
return '<p>Example module.</p>';
}
return '';
}
The modern implementation can live in a dedicated hook class:
example/
└── src/
└── Hook/
└── ExampleHooks.php
For example:
<?php
namespace Drupal\example\Hook;
use Drupal\Core\Hook\Attribute\Hook;
final class ExampleHooks {
/
* Implements hook_help().
*/
#[Hook('help')]
public function help($route_name, $route_match): string {
if ($route_name === 'help.page.example') {
return '<p>Example module.</p>';
}
return '';
}
}
The hook is now represented by an object-oriented method instead of a procedural function.
What About hook_form_alter()?
Form alteration is another common use case for .module files.
An old implementation might look like this:
function example_form_node_form_alter(
&$form,
FormStateInterface $form_state,
$form_id
) {
$form['example'] = [
'#markup' => 'Example',
];
}
The implementation can be migrated to a hook class:
<?php
namespace Drupal\example\Hook;
use Drupal\Core\Hook\Attribute\Hook;
final class ExampleHooks {
#[Hook('form_node_form_alter')]
public function formNodeFormAlter(
array &$form,
$form_state,
$form_id,
): void {
$form['example'] = [
'#markup' => 'Example',
];
}
}
The exact method signature should always be checked against the API documentation for the Drupal version being targeted.
Do Not Move Every Function into a Hook Class
One of the most important points in this migration is that not every function in a .module file is a hook.
For example:
function example_calculate_price($price, $tax) {
return $price + ($price * $tax);
}
This is simply a helper function.
It should not automatically become another method in ExampleHooks.
Business logic and reusable functionality should normally be moved into a service.
For example:
example/
└── src/
└── Service/
└── PriceCalculator.php
<?php
namespace Drupal\example\Service;
final class PriceCalculator {
public function calculate(float $price, float $tax): float {
return $price + ($price * $tax);
}
}
The result is a much cleaner separation of responsibilities:
Drupal hook
↓
Hook class
↓
Service
↓
Business logic
The hook reacts to Drupal, while the service contains the actual application logic.
Avoid Creating a Giant Hook Class
A common migration mistake would be to take a large .module file and simply move all of its functions into one class:
example.module
↓
ExampleHooks.php
This technically moves the code toward OOP, but it does not necessarily improve the architecture.
Instead, organize the module by responsibility:
src/
├── Hook/
│ └── ExampleHooks.php
├── Service/
│ ├── ExampleManager.php
│ └── PriceCalculator.php
├── Form/
│ └── ExampleSettingsForm.php
└── Plugin/
└── ...
For example:
#[Hook('node_insert')]
public function nodeInsert(NodeInterface $node): void {
$this->exampleManager->processNode($node);
}
The hook remains small, while the service contains the actual business logic.
Special Case: hook_requirements()
hook_requirements() cannot simply be mechanically converted to a normal #[Hook] implementation.
Drupal provides different replacement mechanisms depending on when the requirement check needs to happen:
- installation;
- update;
- runtime.
When migrating hook_requirements(), first determine which phase the existing code actually belongs to.
This is particularly important for modules that check:
- configuration;
- external APIs;
- library versions;
- filesystem permissions;
- database state;
- third-party integrations.
Special Case: hook_module_implements_alter()
Another special case is:
hook_module_implements_alter()
This hook has historically been used to change the order in which other hooks are executed.
Drupal now provides dedicated mechanisms for:
- ordering hooks;
- removing hooks;
- reordering hooks;
- legacy compatibility through
#[LegacyModuleImplementsAlter].
Therefore, this hook should not simply be converted to a regular #[Hook] method without checking the appropriate modern replacement.
What About template_preprocess_HOOK()?
Preprocess functions are another important special case.
For example:
function example_preprocess_node(&$variables) {
// ...
}
These should not necessarily be treated like ordinary module hooks.
Drupal provides a modern mechanism for theme hook callbacks, so preprocess functions should be migrated according to that specific API.
What About Public Procedural Functions?
Some .module files contain functions that other projects may call directly.
For example:
example_do_something();
If this function is part of a public API, simply deleting it could break dependent modules.
Instead, the old function can become a deprecated compatibility layer that delegates to a service.
Conceptually:
Old public function
↓
Deprecated wrapper
↓
Service
↓
New implementation
This allows dependent projects time to migrate to the new API.
For public APIs, always use Drupal's current deprecation conventions and document the replacement API.
Raising the Minimum Drupal Version
The change record recommends setting the minimum supported Drupal version to Drupal 11.3.0 to simplify the conversion.
For example, a module that previously supported a broader range might eventually move toward:
core_version_requirement: ^11.3 || ^12
instead of maintaining compatibility with much older Drupal versions.
This is not mandatory for every project. Contributed modules may need to maintain broader compatibility depending on their support policy.
However, raising the minimum supported version can significantly simplify the migration because modern APIs become available without maintaining multiple compatibility paths.
Rector Can Help
Drupal also provides Rector-based tooling that can help automate hook conversion.
This is particularly useful for large modules containing many procedural hooks.
A typical migration workflow might look like:
1. Create a migration branch
↓
2. Run Rector
↓
3. Review generated classes
↓
4. Check namespaces and imports
↓
5. Review hook attributes
↓
6. Check special-case hooks
↓
7. Run static analysis
↓
8. Run automated tests
↓
9. Test on Drupal 11.5
↓
10. Remove the old .module file when possible
Rector can significantly reduce repetitive work, but the generated code still needs manual review.
Pay particular attention to:
- special hooks;
- callbacks;
- dynamic callbacks;
- public API functions;
- helper functions;
- hook execution order;
- installation and update code.
#[ExtensionFileIsConverted]
Drupal provides the:
#[ExtensionFileIsConverted]
attribute for transitional situations.
It can be used to suppress the relevant deprecation warning when a .module file still has to exist for backward compatibility.
However, this attribute does not make .module files compatible with Drupal 13.
It essentially tells Drupal that the developer is intentionally keeping the extension file during the transition.
The important distinction is:
ExtensionFileIsConverted
≠
Drupal 13 compatibility
The .module file will still no longer be automatically loaded in Drupal 13.
A Practical Migration Checklist
For an existing custom or contributed module, the following workflow is useful.
1. Find all .module files
For example:
find web/modules/custom -name "*.module"
For contributed modules:
find web/modules/contrib -name "*.module"
2. Find procedural functions
A simple first pass can be:
grep -R "^function " web/modules/custom
Then classify each function:
Hook implementation
↓
Hook class
Hook helper
↓
Hook class method or Service
Business logic
↓
Service
Form callback
↓
Modern form/callback API
Batch callback
↓
Modern batch callback API
Public API
↓
Deprecated wrapper → Service
3. Review the module's Drupal requirements
Check:
core_version_requirement: ...
Determine whether the project really needs to support older Drupal versions.
If the module can move to Drupal 11.3+, the migration may become considerably simpler.
4. Move hooks to classes
Create:
src/Hook/
and migrate appropriate hook implementations to classes using Drupal's hook attributes.
5. Move business logic to services
Do not turn the hook class into a replacement for the old .module file.
Instead:
Hook
↓
Service
↓
Business logic
This makes the code easier to test and maintain.
6. Review public functions
If other modules may depend on a procedural function, keep a deprecated compatibility wrapper until the supported migration period is complete.
7. Run tests
For example:
vendor/bin/phpunit
If the project uses PHPStan:
vendor/bin/phpstan analyse
Also run the project's Drupal-specific code-quality and compatibility checks.
Drupal 11 → 12 → 13
This change is part of a broader architectural evolution.
A simplified picture looks like this:
Drupal 7
│
├── *.module
├── procedural hooks
└── procedural helpers
│
▼
Drupal 8–10
│
├── *.module
├── services
├── plugins
├── dependency injection
└── gradual OOP adoption
│
▼
Drupal 11.5+
│
├── #[Hook]
├── Hook classes
├── services
└── *.module deprecated
│
▼
Drupal 13
│
└── no automatic *.module loading
The important point is that this is not simply a change to a file extension.
It is another step in Drupal's long-term transition from procedural extension code toward modern object-oriented architecture.
What Should Developers Do Today?
If you are creating a new custom module, there is little reason to build a large procedural .module file.
A modern module might look like:
my_module/
├── my_module.info.yml
├── my_module.routing.yml
├── my_module.services.yml
└── src/
├── Hook/
│ └── MyModuleHooks.php
├── Service/
│ └── MyModuleManager.php
├── Form/
│ └── MyModuleSettingsForm.php
└── Plugin/
For existing modules, migration should be incremental.
First determine what each piece of procedural code actually does, and then choose the appropriate modern replacement.
Conclusion
Drupal 11.5 marks an important step in the modernization of Drupal module development:
.module
↓
OOP APIs
↓
Attributes
↓
Services
↓
Dependency Injection
For ordinary hooks, #[Hook] is one of the most important tools in this transition.
However, migrating a .module file is not simply a matter of replacing every function with a class method.
Hooks, preprocess functions, requirements, hook ordering, callbacks, public APIs, and business logic can all have different migration paths.
If your module needs to be ready for Drupal 13, the best approach is to treat this deprecation as an architectural migration rather than simply a warning that needs to be hidden.
The goal is not merely to remove .module files.
The goal is to build Drupal modules around clear responsibilities, services, dependency injection, and modern object-oriented APIs.
Comments