Skip to main content
Home
Drupal life hacks

Main navigation

  • Drupal
  • React
  • WP
  • Contact
  • About
User account menu
  • Log in

Breadcrumb

  1. Home

Drupal 11.5: Defining Form Routes with PHP Attributes

By admin, 2 July, 2026
PHP Attributes for Form Routes

Drupal 11.5 introduces another significant step toward modern PHP development by allowing form routes to be defined directly on form classes using PHP attributes.

Until now, every custom form required a corresponding entry in a module's .routing.yml file. With this new feature, many of those YAML definitions can be eliminated, making code easier to maintain and keeping route configuration close to the class it belongs to.

This change builds upon Drupal's existing PHP attribute discovery system and follows the direction taken by Symfony and modern PHP applications.


How Route Discovery Works

During a cache rebuild, Drupal scans every enabled module for classes that:

  • are located in the src/Form directory,
  • belong to the module's Form namespace,
  • implement Drupal\Core\Form\FormInterface (directly or indirectly),
  • contain a #[Route] attribute from Symfony.

For every matching class, Drupal automatically registers the route.

For example, this file:

modules/custom/example/src/Form/SettingsForm.php

is automatically inspected. If it contains a Route attribute, Drupal creates the route without requiring any .routing.yml entry.


Before Drupal 11.5

Creating a settings form required two files.

First, the route definition:

example.settings:
  path: '/admin/config/example/settings'
  defaults:
    _form: '\Drupal\example\Form\SettingsForm'
    _title: 'Example settings'
  requirements:
    _permission: 'administer site configuration'

Then the PHP class:

class SettingsForm extends ConfigFormBase {

  public function buildForm(array $form, FormStateInterface $form_state) {
    ...
  }

}

The routing information and implementation were separated.

If you renamed the route, changed permissions, or updated the URL, you had to remember to modify the YAML file as well.


Drupal 11.5 Approach

Now the routing information lives directly on the class.

use Symfony\Component\Routing\Attribute\Route;
use Drupal\Core\StringTranslation\TranslatableMarkup;

#[Route(
  path: '/admin/config/example/settings',
  name: 'example.settings',
  requirements: [
    '_permission' => 'administer site configuration',
  ],
  defaults: [
    '_title' => new TranslatableMarkup('Example settings'),
  ],
)]
class SettingsForm extends ConfigFormBase {

}

The .routing.yml file is no longer needed for this route.

Everything required to expose the form is contained in a single file.


Example 1: Configuration Form

Suppose you're creating a module called Weather with a configuration page.

Directory structure:

weather/
 ├── weather.info.yml
 └── src/
      └── Form/
            WeatherSettingsForm.php

The form class:

use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\Routing\Attribute\Route;
use Drupal\Core\StringTranslation\TranslatableMarkup;

#[Route(
  path: '/admin/config/services/weather',
  name: 'weather.settings',
  defaults: [
    '_title' => new TranslatableMarkup('Weather Settings'),
  ],
  requirements: [
    '_permission' => 'administer site configuration',
  ],
)]
class WeatherSettingsForm extends ConfigFormBase {

  protected function getEditableConfigNames() {
    return ['weather.settings'];
  }

  public function getFormId() {
    return 'weather_settings_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state) {

    $form['api_key'] = [
      '#type' => 'textfield',
      '#title' => $this->t('API Key'),
    ];

    return parent::buildForm($form, $form_state);
  }

}

After rebuilding caches, visiting

/admin/config/services/weather

displays the form—even though the module contains no .routing.yml file.

This is probably the most common use case for the new feature.


Example 2: Standalone Contact Form

The feature also works for regular forms that don't extend ConfigFormBase.

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\Routing\Attribute\Route;
use Drupal\Core\StringTranslation\TranslatableMarkup;

#[Route(
  path: '/contact-us',
  name: 'example.contact',
  defaults: [
    '_title' => new TranslatableMarkup('Contact Us'),
  ],
  requirements: [
    '_access' => 'TRUE',
  ],
)]
class ContactForm extends FormBase {

  public function getFormId() {
    return 'example_contact_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state) {

    $form['name'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Your name'),
    ];

    $form['message'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Message'),
    ];

    return $form;
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {

  }

}

This creates a publicly accessible page at:

/contact-us

without any route defined in YAML.


What Can Be Defined in the Attribute?

The Route attribute supports the same routing options available in .routing.yml, including:

  • route name
  • URL path
  • defaults
  • permissions
  • access checks
  • route options
  • parameter requirements
  • HTTP methods
  • host requirements
  • schemes

For example:

#[Route(
  path: '/reports',
  name: 'example.reports',
  methods: ['GET'],
  requirements: [
    '_permission' => 'access content',
  ],
  options: [
    'no_cache' => TRUE,
  ],
)]

In other words, you're not losing routing capabilities by switching to attributes.


Things to Remember

There are several important rules.

The attribute belongs on the class

Only class-level attributes are discovered.

#[Route(...)]
class MyForm extends FormBase

This will not work:

public function buildForm(...) {

    #[Route(...)]
}

Drupal ignores method-level route attributes for forms.


The class must be in the Form namespace

Discovery only searches classes located under:

src/Form

Moving the class elsewhere prevents automatic discovery.


Cache rebuild is required

Like other discovered plugins and routes, new route attributes are detected only after rebuilding caches.

drush cr

Entity forms still use route providers

Entity add/edit/delete forms continue to rely on entity route providers defined by the entity type.

Nothing changes for content entities or configuration entities.


Should Existing Modules Be Updated?

There is no requirement to migrate existing modules.

The existing .routing.yml format remains fully supported.

However, for:

  • new modules,
  • administrative tools,
  • internal utilities,
  • custom configuration pages,

using attributes can significantly reduce boilerplate and improve readability.

Many developers will likely adopt attributes for new code while leaving older modules unchanged.


Final Thoughts

PHP attributes for form route discovery make Drupal development more cohesive by bringing routing metadata into the class itself. Instead of maintaining separate PHP and YAML files, developers can define a form, its URL, permissions, and page title in one place.

The result is cleaner code, fewer files to maintain, and a development experience that aligns more closely with modern PHP frameworks while remaining fully compatible with Drupal's existing routing system.

Tags

  • Drupal
  • Drupal 11.5
  • Drupal Core
  • PHP Attributes
  • Form Routes
  • Route Discovery
  • Symfony Attributes
  • Module Development
  • ConfigFormBase
  • FormBase
  • Drupal API
  • Modern PHP

Comments

About text formats

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.
Powered by Drupal