Converting from .module-file to an object oriented class method

Last updated on
8 October 2026

This page has not yet been reviewed by Creating modules maintainer(s) and added to the menu.

This documentation needs work. See "Help improve this page" in the sidebar.

The .module in modules, and .theme-file in themes have historically been used as the homes for many functions. These have been phased out in favor of Object Oriented Programming (OOP) concepts.

The .module- and .theme-file extension have been deprecated, and support for autoloading the .module and .theme-files will be removed in Drupal 13, see issues and their Change Records:

There are scripts to assist with converting the hooks in modules and themes, but here we document how to manually convert hooks and other functions.

ToDo: Add as a menu item, name could be "Convert from .module-file to OOP"?

Convert an existing hook from a .module file into OOP

Switching from using .module functions to OOP is described well in the original change record Support for object oriented hook implementations using autowired services; some ModuleHandler methods deprecated which you should read before continuing.

The recent change record The .module file extension has been deprecated is a great high level document, with links to scripts which can convert the module's code with rector, automating the conversion. The scripts/automations is for sites with dozens of hooks in many modules where it gets tedious. They are not perfect, and may omit several functions.

Here is a basic example of converting two hooks in a custom module, from functions in a .module file, into an OOP Hook-driven solution. Please read Support for object oriented hook implementations [...] for an explanation of the syntax.

Say you have a module called my_module with two hook functions in the my_module.module file. The first hook function adds a variable for Twig files in a theme, and the second hook function adds support for consistent transliteration of certain characters:

<?php

/*
 * Implements HOOK_preprocess() for all Twig templates
 * Note: theme_preprocess_page and theme_preprocess_html
 * only affects html.html.twig and page.html.twig
 */
function my_module_preprocess(&$variables) {
  $current_language = \Drupal::languageManager()->getCurrentLanguage()->getId();
  switch ($current_language) {
    case $current_language == 'en':
      break;
    case $current_language == 'it':
      break;
    default:
      $current_language = 'da';
    }
  $variables['page_language'] = 'lang-' . $current_language;
}

/**
 * Implements hook_transliteration_overrides_alter().
 * Danish characters ø and å are not transliterated correctly
 * https://www.drupal.org/project/pathauto/issues/1811856
 */
function my_module_transliteration_overrides_alter(&$overrides, $langcode) {
// Transliterate Danish letters æ å ø for English language.
  if ($langcode == 'en' || $langcode == 'it') {
    $overrides = [
      0xC5 => 'Aa',
      0xD8 => 'Oe',
      0xE5 => 'aa',
      0xF8 => 'oe',
      0xC6 => 'Ae',
      0xE6 => 'ae',
    ];
  }
}

To convert them into OOP hooks, you can split them into multiple files, but how to group them is up to you. A use statements at the top cost nothing, and the class size is a minor cost factor. The only measurable cost is doing a lot of dependency injection in the constructor, specifically things that only one/some of the hooks need and isn't otherwise needed.

The naming recommendations for hook classes is ModuleName{Group}Hooks. Group comes from the {Group}.api.php file that defines the hook. In this case the class for my_module_preprocess would be called MyModuleThemeHooks and the class formy_module_transliteration_overrides_alter would be MyModuleLanguageHooks.

You need to create a new file for the class in the my_module/src/Hook folder matching the class name. This way, the file and its classes will be auto-discovered. For a simple example we will use MyModuleHooks.php

So this should be the structure, with the soon redundant my_module.module file already deleted:

my_module
  ├── my_module.info.yml
  └── src
      └── Hook
          └── MyModuleHooks.php

... with this content. Notice how the class matches the file name:

<?php

namespace Drupal\my_module\Hook;

use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\StringTranslation\StringTranslationTrait;

/**
 * Hook implementations for my_module.
 */
class MyModuleHooks {
  use StringTranslationTrait;

  /**
   * Implements hook_transliteration_overrides_alter().
   * Danish characters ø and å are not transliterated correctly
   * https://www.drupal.org/project/pathauto/issues/1811856
   */
  #[Hook('transliteration_overrides_alter')]
  public function transliterationOverridesAlter(array &$overrides, string $langcode): void {
    // Transliterate Danish letters æ å ø for English language.
    if ($langcode == 'en' || $langcode == 'it') {
      $overrides = [
        0xc5 => 'Aa',
        0xd8 => 'Oe',
        0xe5 => 'aa',
        0xf8 => 'oe',
        0xc6 => 'Ae',
        0xe6 => 'ae',
      ];
    }
  }

  /**
   * Implements HOOK_preprocess() for all Twig templates
   * Note: theme_preprocess_page and theme_preprocess_html
   * only affects html.html.twig and page.html.twig :)
   */
    #[Hook('preprocess')]
    public function preprocess(array &$variables): void {
    $current_language = \Drupal::languageManager()->getCurrentLanguage()->getId();
    switch ($current_language) {
      case $current_language == 'en':
        break;
      case $current_language == 'it':
        break;
      default:
      $current_language = 'da';
    }
    $variables['page_language'] = 'lang-' . $current_language;
  }

}

It is an option to split functions into separate classes and files. In that case, you need to create two files. The classes and file names are using the recommended naming conventions describe above:

my_module
  ├── my_module.info.yml
  └── src
      └── Hook
          └── MyModuleLanguageHooks.php
          └── MyModuleThemeHooks.php

... with this content in MyModuleThemeHooks.php:

<?php

namespace Drupal\my_module\Hook;

use Drupal\Core\Hook\Attribute\Hook;

/**
 * Hook implementations for my_module.
 */
class MyModuleThemeHooks {

  /**
   * Implements HOOK_preprocess() for all Twig templates
   * Note: theme_preprocess_page and theme_preprocess_html
   * only affects html.html.twig and page.html.twig :)
   */
    #[Hook('preprocess')]
    public function preprocess(array &$variables): void {
    $current_language = \Drupal::languageManager()->getCurrentLanguage()->getId();
    switch ($current_language) {
      case $current_language == 'en':
        break;
      case $current_language == 'it':
        break;
      default:
      $current_language = 'da';
    }
    $variables['page_language'] = 'lang-' . $current_language;
  }

}

... and in MyModuleLanguageHooks.php:

<?php

namespace Drupal\my_module\Hook;

use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\StringTranslation\StringTranslationTrait;

/**
 * Hook implementations for my_module.
 */
class MyModuleLanguageHooks {
  use StringTranslationTrait;

  /**
   * Implements hook_transliteration_overrides_alter().
   * Danish characters ø and å are not transliterated correctly
   * https://www.drupal.org/project/pathauto/issues/1811856
   */
  #[Hook('transliteration_overrides_alter')]
  public function transliterationOverridesAlter(array &$overrides, string $langcode): void {
    // Transliterate Danish letters æ å ø for English language.
    if ($langcode == 'en' || $langcode == 'it') {
      $overrides = [
        0xc5 => 'Aa',
        0xd8 => 'Oe',
        0xe5 => 'aa',
        0xf8 => 'oe',
        0xc6 => 'Ae',
        0xe6 => 'ae',
      ];
    }
  }

}

After converting the my_module.module functions into class-based OOP, you can delete the the file, if there is nothing relevant left inside it.

LegacyHook

If you want to keep compatibility with Drupal 10, you can keep the my_module.module file and add LegacyHook references, but this is mostly relevant for contrib modules. Add something like this in the my_module.module file, which will point to the new OOP Hook-class:

/**
 * Implements hook_transliteration_overrides_alter().
 * Danish characters ø and å are not transliterated correctly
 * https://www.drupal.org/project/pathauto/issues/1811856
 */
#[LegacyHook]
function my_module_transliteration_overrides_alter(&$overrides, $langcode) {
  \Drupal::service(MyModuleHooks::class)->transliterationOverridesAlter($overrides, $langcode);
}

/**
 * Implements hook_preprocess().
 */
#[LegacyHook]
function my_module_preprocess() {
  \Drupal::service(MyModuleHooks::class)->preprocess(&$variables);
}

... and create my_module.services.yml (or use an existing) to activate them, by adding this. This is only needed if you are using the LegacyHook attribute.

services:
  Drupal\my_module\Hook\MyModuleHooks:
    class: Drupal\my_module\Hook\MyModuleHooks
    autowire: true

Create a new function using OOP principles

How to write a new function to solve a common task, and describe the process of figuring out where to put it.

  • ToDo: Figure out which common task to cover.
  • ToDo: Write the content

Help improve this page

Page status: Needs work

You can: