Create a Custom Controller in Drupal 11 with PHP Attributes
Learn how to create a controller using PHP attributes in Drupal 11.4.x. In this step-by-step tutorial, you'll build a custom module, define a controller with PHP attributes, create a custom route, and return content as a render array.
Step 1: Define the Module
Note: You can skip this step if you are adding a controller to an existing custom module.
First, we need to let Drupal know our module exists. Create a new directory in your custom modules folder named mint_controller (you can swap this out for whatever machine name fits your project). Inside that folder, create a mint_controller.info.yml file to provide Drupal with the necessary metadata.
1name: 'Mint Controller Example'
2type: module
3description: 'Test module with example controller'
4package: 'Mint'
5core_version_requirement: ^10 || ^11With the module defined, we can build the controller class that will handle incoming HTTP requests. Following Drupal's PSR-4 routing standards, create the following file structure inside your module: src/Controller/TestController.php.
Start by defining the basic class structure extending ControllerBase:
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\mint_controller\Controller;
6
7use Drupal\Core\Controller\ControllerBase;
8
9/**
10 * Defines the TestController class.
11 */
12class TestController extends ControllerBase {
13
14}Right now, this class is just a blank blueprint. To make it functional, we need to add a public method that returns a response. In Drupal, controllers typically return a render array rather than raw HTML or strings, allowing the theme system to handle the final output.
Let's add a handleRequest() method that builds a structured container element:
1/**
2 * Handles the incoming request and returns a render array.
3 *
4 * @return array
5 * A Drupal render array.
6 */
7 public function handleRequest(): array {
8 // Define the main wrapper container.
9 $build['my_custom_container'] = [
10 '#type' => 'container',
11 '#attributes' => [
12 'id' => 'custom-wrapper-id',
13 'class' => ['my-custom-wrapper'],
14 ],
15 // Child elements are nested using keys without a leading '#'
16 'heading' => [
17 '#type' => 'html_tag',
18 '#tag' => 'h2',
19 '#value' => $this->t('Container Heading'),
20 ],
21 'body_text' => [
22 '#type' => 'html_tag',
23 '#tag' => 'p',
24 '#value' => $this->t('This is some sample text inside the container wrapper.'),
25 ],
26 ];
27
28 return $build;
29 }Step 3: Map the Route Using PHP Attributes
Traditionally, mapping a URL path to a controller required creating a separate *.routing.yml file. However, modern Drupal versions allow you to declare your routes directly inside the PHP class using PHP Attributes. This keeps your routing logic tightly coupled with the code that executes it.
To attach a route to our handleRequest() method, place the #[Route] attribute directly above the function declaration:
1#[Route(
2 path: '/mint/test',
3 name: 'mint_controller.attribute_test',
4 defaults: [
5 '_title' => new TranslatableMarkup('Test Controller')
6 ],
7 requirements: [
8 '_permission' => 'access content'
9 ]
10 )]
11 public function handleRequest(): array {
12 // ... (method body remains the same)
13 }Required Use Statements
For this to compile correctly without errors, make sure you add the correct Symfony and Drupal namespaces at the very top of your file:
1use Symfony\Component\Routing\Attribute\Route;
2use Drupal\Core\StringTranslation\TranslatableMarkup;The Complete File
Here is how your entire src/Controller/TestController.php file should look once everything is wired up:
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\mint_controller\Controller;
6
7use Drupal\Core\Controller\ControllerBase;
8use Symfony\Component\Routing\Attribute\Route;
9use Drupal\Core\StringTranslation\TranslatableMarkup;
10
11class TestController extends ControllerBase {
12 /**
13 * Handles the incoming request and returns a render array.
14 *
15 * @return array
16 * A Drupal render array.
17 */
18 #[Route(
19 path: '/mint/test',
20 name: 'mint_controller.attribute_test',
21 defaults: [
22 '_title' => new TranslatableMarkup('Test Controller')
23 ],
24 requirements: [
25 '_permission' => 'access content'
26 ]
27 )]
28 public function handleRequest(): array {
29 // Define the main wrapper container.
30 $build['my_custom_container'] = [
31 '#type' => 'container',
32 '#attributes' => [
33 'id' => 'custom-wrapper-id',
34 'class' => ['my-custom-wrapper'],
35 ],
36 // Child elements are nested using keys without a leading '#'
37 'heading' => [
38 '#type' => 'html_tag',
39 '#tag' => 'h2',
40 '#value' => $this->t('Container Heading'),
41 ],
42 'body_text' => [
43 '#type' => 'html_tag',
44 '#tag' => 'p',
45 '#value' => $this->t('This is some sample text inside the container wrapper.'),
46 ],
47 ];
48
49 return $build;
50 }
51}Testing Your Controller
Before you can see your new route in action, Drupal needs to register it.
- For a brand new module: Enable it via the admin dashboard (
/admin/modules) or rundrush en mint_controllerin your terminal. - For an existing module: Because you are adding a brand new route to code that Drupal already knows about, you must rebuild the cache so the routing system can discover the new path. Run
drush cror navigate to Configuration > Development > Performance and click Clear all caches.
Once that is done, head over to /mint/test in your browser. You should see your custom container, heading, and body text rendered live on the page!
For further reading, take a look at the official Drupal.org Change Record: PHP Attributes can be used for route definition and discovery.