Drupal 11.4 quietly shipped a developer experience improvement that will change how we write custom controllers going forward: routes can now be declared using PHP attributes directly on the controller class, no separate YAML file required. It's not flashy, but it's the kind of structural cleanup that compounds across every module you build.
What shipped in Drupal 11.4
Prior to 11.4, defining a route in Drupal meant maintaining two files: the controller PHP class and a corresponding module_name.routing.yml entry. The two had to stay in sync manually — change the controller method signature or path and you'd need to update the YAML separately. It was a source of small-but-real bugs during refactors, and a friction point for developers coming from Symfony or Laravel where route definition lives closer to the code.
Drupal 11.4 introduces support for defining those routes using native PHP attributes directly on the controller method. Instead of a YAML block referencing a controller class and method, you annotate the controller method itself with the route path, HTTP methods, permissions, and any other parameters the routing system needs. Drupal's routing layer reads those attributes and registers the routes at the same point in the request lifecycle as before — so caching, access checking, and path aliasing all work exactly as expected.
Backward compatibility is fully maintained. Existing YAML-based routing continues to work without modification. You can migrate routes incrementally, method by method, module by module — there's no big-bang conversion required. The change also aligns Drupal more closely with how Symfony handles routing natively, which matters because Drupal's dependency on Symfony has deepened across major versions. Developers who know Symfony already will find the mental model familiar immediately.
The feature landed in 11.4.0 and is available now on any site running that release or newer. No contributed modules or experimental flags required — it's a stable, core-supported pattern.
Our take
This is the kind of improvement that looks minor on a release blog but actually accumulates meaningful time savings across a full project. On a typical client engagement where we're building 8–15 custom routes across several modules, maintaining parity between controller classes and routing YAML adds up — not in hours, but in the cognitive overhead of context-switching between file types and the debugging time when they drift out of sync.
Attribute-based routing closes that gap. The route definition lives next to the code it describes. If you rename a method, the route moves with it. If you're reviewing a PR, you can see the route and its handler in the same diff. That's genuinely useful.
The Symfony alignment is worth calling out separately. One of the challenges of Drupal as a hiring target is explaining its conventions to PHP developers who know the wider ecosystem. "We use YAML for routing because that's how Drupal has always done it" is a hard sell to a developer who's been writing Symfony for three years. "We use PHP attributes, same as Symfony" removes a whole category of onboarding friction. That matters when you're staffing a team or evaluating whether a developer can be productive on a Drupal project quickly.
What we'd actually change in our workflow today:
New modules get attribute-based routes from day one. We've already updated our internal module scaffold template to use attributes by default. For existing client sites, we're not touching routing YAML proactively — the risk-to-benefit ratio doesn't justify it. But any module that goes through a meaningful refactor gets its routes migrated as part of that work, not as a separate task.
The honest trade-off: YAML routing has one advantage that attributes don't replicate cleanly — it's easy to grep all routes across a codebase in one place. With attributes, routes are distributed across controller files. If you're trying to audit all routes on a large site, you'll need tooling (PHPStan, IDE support, or a Drush command) rather than a simple file search. That's not a blocker, but it's a real workflow change worth thinking through before you migrate a large legacy module.
What this signals about Drupal's direction: The team is consistently making choices that reduce the distance between Drupal conventions and the broader PHP ecosystem. Attribute-based routing, PHP 8.x type system adoption, Symfony component upgrades — these aren't dramatic features, but they represent a sustained effort to make Drupal feel less idiosyncratic to developers who aren't Drupal specialists. For agencies like ours that work across multiple frameworks, that matters. A Drupal codebase that reads more like Symfony is easier to staff, easier to hand off, and easier to maintain long-term.
Practical recommendations
- New custom modules: Use PHP attribute routing from day one. Update your scaffold or starter templates now.
- Existing modules under active development: Migrate routes opportunistically during refactors, not as a standalone task.
- Large legacy codebases: Leave routing YAML in place until there's a clear reason to change it. Backward compatibility is solid.
- Team onboarding: If you're bringing Symfony developers onto a Drupal project, point them at attribute-based routing first — it's the fastest way to establish a familiar anchor point.
- Tooling: Make sure your IDE (PhpStorm, VS Code with Intelephense) is running a version that understands PHP attribute routing so you get proper autocomplete and navigation.
Originally referenced: Drupal 11.4 Introduces PHP Attribute-Based Routing for Controllers on The Drop Times.
If you're running Drupal 11 and want help modernizing your custom module patterns — or planning an upgrade from Drupal 10 — get in touch.
Originally published by The Drop Times. Read the full announcement here.


