Introductory Drupal routes and controllers example

Last updated on
11 January 2024

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

If you want to expose content or functionality on your own custom URLs, you will need to define you own routes. Be sure to read the routing system overview first.

This guide demonstrates how to create a simple custom page that is shown when the path /example is requested. We'll first see how to display a page by returning simple HTML markup in a render array, and then render more complex content using a Twig template.

The steps involved

  1. Create a custom module.
  2. Add a [your_module].routing.yml file to the module that defines the route path and the page controller and method that should handle the request.
  3. Create the controller and method, and return a renderable array that will be displayed on your page.
  4. Use a Twig template to render the output.

Note: Instead of manually creating the required routing and controller files, you can use Drush to create a skeleton *.routing.yml and route controller class for you: drupal generate:controller. (drush generate controller docs).

1. Create a custom module

See creating modules. For the purposes of this guide, we will be using the module machine name example.

2. Add a *.routing.yml file to the module

Drupal routes are defined in the [module_name].routing.yml config files in the core, contrib and custom modules enabled on your site. When the cache is rebuilt, Drupal parses all of the routing files available to determine how the response for a particular path should be generated. For example, the commonly seen path /node/[nid] is handled by a controller in the core node module.

In our example, the routing file is named example.routing.yml and is stored in the root level of the custom module directory: /modules/custom/example/example.routing.yml

(Note - make sure to use single quotes in .yml files rather than double quotes, as double quotes can have unexpected results.)

example.routing.yml

example.content:
  path: '/example' 
  defaults: 
    _controller: '\Drupal\example\Controller\ExampleController::content' 
    _title: 'Hello World'
  requirements: 
    _permission: 'access content' 

This defines a route named 'example.content' (routes are typically named with the module name as a prefix) that handles the path /example on the site. When that path is accessed, the 'access content' permission is checked on the accessing user and, if access is granted, the content returned by the ExampleController::content method is displayed with the title 'Hello World'.

See the structure of routes for more information on route configuration options, such as access permissions.

3. Building a page controller

Drupal uses the Symfony HTTP Kernel, a system which gets the request and asks other systems to produce the requested output (a response object) and sends the response back to the client. In Drupal, the output is (usually) generated by a controller class. Read more about the underlying Routing system

The second part of creating a page that outputs our content is to create page controller. This class is in a file named exactly after the controller provided in the routing file: ExampleController.php

Drupal supports PSR-4, which means that, for the above route the class needs to be placed in example/src/Controller directory and with name ExampleController.php. So the full path will look like, example/src/Controller/ExampleController.php.

ExampleController.php

<?php

namespace Drupal\example\Controller;

use \Drupal\Core\Controller\ControllerBase;

/**
 * An example controller.
 */
class ExampleController extends ControllerBase {

  /**
   * Returns a renderable array for a test page.
   *
   * return []
   */
  public function content() {
    $build = [
      '#markup' => $this->t('Hello World!'),
    ];
    return $build;
  }

}

With just routing file and the page controller, we have made a page available on our site at /example that outputs 'Hello World!' on a page titled 'Hello World'.

See also: ControllerBase abstract class API docs.

4. Adding a Twig template to the page controller

Instead of returning simple HTML markup in a render array from the page controller, you can define a Twig template that will be used to generate your custom page.

The steps involved

  1. Create a Twig template.
  2. Add a theme hook to your module, that defines which Twig template to use.
  3. Invoke the theme hook from the page controller.

Create a Twig Template

The template file will usually in your module's ./templates/ directory. You can either choose a custom name, or use the name of the template hook, that will be created in the next step of this guide. Let's call our file example-hello-world.html.twig.

example-hello-world.html.twig

<!-- Let's output the three variables passed to this template -->

<h1>My lovely twig template.</h1>
<p>You passed this text: <b>{{ variable1 }}</b></p>
<p>You passed this number value: {{ variable2 }}</p>
<p>And you passed this array:</p>

<ul>
  {% for item in variable3 %}
    <li>
       {{ item }} 
    </li>        
  {% endfor %}    
</ul>

<p>Ciao!</p>

Add a theme hook

Create the example.module file and add a method that implements hook_theme. This will provide the theme hook that defines which Twig template to use and which variables are passed to the Twig template. It can also provide default values for the available variables.

Make sure to clear theme-registry cache after making changes in that hook. If you're using drush from the command line type drush cc theme-registry.

example.module

<?php

/**
 * Implements hook_theme().
 *
 * Register a module or theme's theme implementations.
 * The implementations declared by this hook specify how a 
 * particular render array is to be rendered as HTML.
 *
 * See: https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21theme.api.php/function/hook_theme
 *
 * If you change this method, clear theme registry and routing 
 * table 'drush cc theme-registry' and 'drush cc router'.
 */
function example_theme($existing, $type, $theme, $path) {

  return [
    // Name of the theme hook. This is used in the controller to trigger the hook.
    'example_hello_world' => [
      'render element' => 'children',
      // If no template name is defined here,
      // it defaults to the name of the theme hook,
      // ie. example-hello-world.html.twig
      'template' => 'example-hello-world',
      // Optionally define path to Twig template files.
      // Defaults to the module's ./templates/ directory.
      'path' => $path . '/templates',
      // Optionally define variables that will be passed to the Twig
      // template and set default values for them.
      'variables' => [
        'variable1' => 'Yet another default text.',
        'variable2' => 0,
        'variable3' => [0, 0, 0],
      ],
    ],
  ];

}

Invoke the theme hook from the page controller.

Finally we're back in our Controller. Instead of returning the markup created in our controller directly, we now return the theme hook in our render array. This will build the markup for our custom page from our Twig template.

ExampleController.php

<?php

namespace Drupal\example\Controller;

use \Drupal\Core\Controller\ControllerBase;

/**
 * An example controller.
 */
class ExampleController extends ControllerBase {

  /**
   * Returns a render array for a test page.
   *
   * @return []
   */
  public function content() {

    // Do something with your variables here.
    $my_text = 'Hello World!';
    $my_number = 1;
    $my_array = [1, 2, 3];

    return [
      // Your theme hook name.
      '#theme' => 'example_hello_world',
      // Your variables.
      '#variable1' => $my_text,
      '#variable2' => $my_number,
      '#variable3' => $my_array,
    ];
  }
}

if your changes don't seem to appear on your custom page, try clearing your routing and theme-registry cache, or clear the entire cache using the drush command drush cr.

Cache consideration

See Cacheability of render arrays for information on cacheing the response.

Help improve this page

Page status: Needs review

You can: