Getting Hooked on Attributes at Southwestern Ontario Drupal Camp

Southwestern Ontario Drupal Camp logo
Caption

Southwestern Ontario Drupal Camp will happen in Kitchener on October 23-24, 2026.

Posted by maursilveira on

Read time: 3 minute(s)

Body

The Southwestern Ontario Drupal Camp brings the region’s Drupal community together in Kitchener-Waterloo, and this year it grows to two full days, October 23 and 24. Friday is packed with sessions and networking at the Kitchener Public Library, covering everything from development and performance to AI-driven orchestration. Saturday brings Drupal in a Day, a hands-on introduction for newcomers, and dedicated contribution time to give back to the project and community. It’s one of those events where you’ll learn something new, exchange ideas over lunch with the people who help shape Drupal, and leave with a few new friends.

This year, I’m honoured to be part of the lineup. My session, Hooked on Attributes: Migrating Your Module’s Hooks to OOP Classes Before They’re Gone, is a practical, code-first walk-through of one of the most common modernization tasks Drupal module maintainers will face over the next couple of years: moving hooks out of the .module file and into proper classes.

I’ve spent a good amount of time this year doing exactly that, both on client work at Kalamuna and on contrib modules I maintain, and I wanted to share what I learned to help other developers do it more confidently and avoid the last-minute rush.

Why this matters now

Since Drupal 11.1, hooks can be implemented as methods on a class, tagged with the #[Hook] attribute. Core has been converting its own hooks steadily, and contrib is following as fast as maintainers can. Procedural hooks still work today, but they’re deprecated and slated for removal in Drupal 13.

That changes hook migration from “nice cleanup when I have time” into a deadline. The good news is that it’s very manageable if you start early and go module by module. The bad news is that there are a few traps that won’t throw an error: your hook just quietly stops running.

A quick taste

Here’s a procedural hook most of us have written some version of:

// mymodule.module
use Drupal\Core\Form\FormStateInterface;

/**
 * Implements hook_form_BASE_FORM_ID_alter() for node_form.
 */
function mymodule_form_node_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
  if (isset($form['field_featured']) && !\Drupal::currentUser()->hasPermission('feature articles')) {
    $form['field_featured']['#access'] = FALSE;
  }
}

And here’s the same logic as an OOP hook, with the current user injected instead of pulled from the global container:

// src/Hook/FormHooks.php
namespace Drupal\mymodule\Hook;

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Session\AccountProxyInterface;

final class FormHooks {

  public function __construct(
    private readonly AccountProxyInterface $currentUser,
  ) {}

  #[Hook('form_node_form_alter')]
  public function nodeFormAlter(array &$form, FormStateInterface $form_state, string $form_id): void {
    if (isset($form['field_featured']) && !$this->currentUser->hasPermission('feature articles')) {
      $form['field_featured']['#access'] = FALSE;
    }
  }

}

On Drupal 11.1+, classes in src/Hook are discovered and autowired automatically. If your module still supports Drupal 10, you keep a thin procedural wrapper marked with #[LegacyHook], so older cores call it while newer ones skip it and use the class directly, with no double execution:

// mymodule.module
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Hook\Attribute\LegacyHook;
use Drupal\mymodule\Hook\FormHooks;

#[LegacyHook]
function mymodule_form_node_form_alter(array &$form, FormStateInterface $form_state, $form_id): void {
  \Drupal::service(FormHooks::class)->nodeFormAlter($form, $form_state, $form_id);
}
# mymodule.services.yml (needed for Drupal 10)
services:
  Drupal\mymodule\Hook\FormHooks:
    class: Drupal\mymodule\Hook\FormHooks
    autowire: true

That’s just an easy case. The session builds from here into the hooks that need more than a straight port.

What we’ll cover

  • The conversion itself: moving a familiar hook out of .module, wiring up dependency injection, then tackling trickier hooks that don’t port one-to-one.
  • Backward compatibility: using #[LegacyHook] so a single codebase serves Drupal 10.1, 11 and 12 without firing hooks twice, and registering hook classes across core versions.
  • Letting tools do the heavy lifting: drupal-rector and a lightweight DDEV-based converter, plus a repeatable playbook you can run module by module.

The session is aimed at intermediate developers, but everyone is welcome, whatever your experience level. If you maintain a custom or contrib module, or you’re responsible for keeping a site’s custom code upgrade-ready, this one is for you.

See you in Kitchener

I’m on Friday, October 23, from 1:15 to 2:00 pm, in the Woolwich Web Works Room E. Sessions and networking run all Friday at the Kitchener Public Library, followed by Drupal in a Day and contribution time on Saturday, October 24.

If you’re around, come say hi. I love chatting, whether it’s about Drupal, code, or anything else. And if you’ve already started converting your own modules, I’d love to hear how it went for you. Bring your migration stories!

Important links

— Mauricio Silveira

Comments

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.