Drupal 8 API: routing API and page controllers

Pre-requisites

How Drupal works out what content to serve on what URL

When Drupal is asked to return the content for a given URL, it is the job of routing to work out which code should be asked to handle the URL; does the URL correspond to:

  • a path alias, for a known node? If so, route to the entity module, via node module.
  • a user's profile page? If so, route to the entitiy module, via user module.
  • a view listing's page display? If so, route to the views module.
  • a path registered by custom code? If so, route to that custom module.
  • ...
  • (ultimately, route to the 404 handler.)

Routing in Drupal 8 is actually powered by a Symfony component. This reduces the maintenance overhead required by Drupal's core contributors, because they can expect to rely on the Symfony project to maintain its own code.

Routes are configured using YAML files in modules, which need to both define paths (or path patterns, using placeholders to define parameters for e.g. a node ID that might vary from path to path) and the actual PHP code that will handle any matches to those paths.

Creating a custom route handler

Let's say we know what the URL is that we want to wire up to some custom code. Before we can create a YAML file that works, we also need to write the custom code. Then, when we reference it, we don't get any errors.

Let's imagine we want to handle three path patterns:

  • The path /tutorial as a kind of "index page".
  • The path /tutorial/{name}, where the path is a pattern and provides the parameter $name so that the site visitor is given the message "Hello, $name" when they visit the path.
  • The path /tutorial/druplicon, an exception to the tokenized path, where an alternative message is given to Druplicon, should he visit the site!

Our controller just needs to implement the correct methods and so could just be a brand new class. However, let's assume we also want to do basic tasks like permit our responses to be translatable, so that in future our site could serve an audience that spoke a different language. So we're going to use inheritance to bring in the translation t() function as a property of our object. We'll talk more about translation, much later.

Here's the resulting class, which we put in the d8api module folder, and locate it at src/Controller/TutorialController.php:

<?php
 
namespace Drupal\d8api\Controller;
 
use Drupal\Core\Controller\ControllerBase;
 
/**
 * Controller: /tutorial/*.
 */
class TutorialController extends ControllerBase {
  /**
   * Controller: index page.
   */
  public function index() {
    return [
      '#type' => 'markup',
      '#markup' => $this->t('This is the tutorial index.'),
    ];
  }
 
  /**
   * Controller: Druplicon page.
   */
  public function subPageForDruplicon() {
    return [
      '#type' => 'markup',
      '#markup' => $this->t('Oh, Druplicon! I know you.'),
    ];
  }
 
  /**
   * Controller: page for anyone who isn't Druplicon.
   */
  public function subPageWithParameter($name) {
    return [
      '#type' => 'markup',
      '#markup' => $this->t('Hello, @name', ['@name' => $name]),
    ];
  }
}

The naming of these methods is not significant; instead, we'll configure the routing next, to recognize each method in turn.

You might notice that each controller method returns an array of data. These are called "render(able) arrays" in Drupal, and we don't discuss them here (they're part of the much wider topic of theming, which we'll discuss later). For now, just note that you can return an array with two elements: the #type key set to a markup value, and then a corresponding #markup key which can contain any HTML markup.

You should also spot that the $this->t() method is wrapping any text where it might (a) vary based on user input, which we might want to strip of any accidental or malicious HTML markup, and (b) need to be translatable at a later date.

Wiring up paths to routes

Now we've got class methods ready and waiting to handle our routes. We have all the elements in place to link path patterns with custom code.

As discussed, each route needs to specify the path pattern and the location of the class method that will handle it. In addition, the route specification should provide both the page title (what will go in the <h1> and <title> tags in the HTML) and a description of permissions. In Drupal 8, permissions are a subset of "requirements": so you could demand not just "the site visitor must have this permission to see this route", but also "the site visitor must have this role", or merely "the site itself must have certain other modules enabled".

Finally, the specification has a "machine name", which is the first line of each specification (the "top-level key" in YAML.) We discussed these when we were first creating a module, so let's try to standardize on route names of the form:

[MODULE MACHINE NAME].[ROUTE NAME]

We can then say that all the route names within one module are "namespaced" by that module i.e. their machine names begin with the module's machine name. You might also want to try to make route name match the class and method names for the controller code, so that you can tell at a glance how they relate to each other.

Whew. With all that in mind, here's the three specifications, for our three routes, defining three path patterns and hooking them up to the three methods on our class above. These specifications should all go in a file called d8api.routing.yml:

d8api.index:
  path: '/tutorial'
  defaults:
    _controller: '\Drupal\d8api\Controller\TutorialController::index'
    _title: 'Tutorial index'
  requirements:
    _permission: 'access content'
 
d8api.sub_page_for_druplicon:
  path: '/tutorial/druplicon'
  defaults:
    _controller: '\Drupal\d8api\Controller\TutorialController::subPageForDruplicon'
    _title: 'Tutorial sub-page for Druplicon'
  requirements:
    _permission: 'access content'
 
d8api.sub_page_with_parameter:
  path: '/tutorial/{name}'
  defaults:
    _controller: '\Drupal\d8api\Controller\TutorialController::subPageWithParameter'
    _title: 'Tutorial sub-page with name parameter'
  requirements:
    _permission: 'access content'

You should hopefully see a simple repeated structure among all three specifications:

  • Machine name, namespaced by the module name
  • Path
  • Controller method, and title for the page
  • Permission requirements

This is, broadly, the minimum required for a route specification.

What you should see

First, you probably need to clear cache. This can be done in the admin UI ("Configuration" > "Performance" > "Clear all caches") or using Drush (drush cr).

Once you've done that, then if you navigate to different path patterns, you should see results as follows:

/tutorial

/tutorial/druplicon

/tutorial/J-P

If so, then congratulations! you've just created and registered some custom route handlers in Drupal.

Further reading etc.

Comments

Dear JP Stacey:

Thank you *very* kindly for the tutorial!  I know that you put in much work building it.  Your efforts are much appreciated.

I believe I found an error in your example.  Please update your code so that it will not stump others like it did me.  Line 18 should be:

    _controller: '\Drupal\d8api\Controller\TutorialController::subPageForDruplicon'

And line 29 should be:

    _controller: '\Drupal\d8api\Controller\TutorialController::subPageWithParameter'

Thanks again for your diligence.  If you ever publish a book, I would be very interested in a complementary copy!  :)

 

Thanks again, Fred. As per your other comment, some edits have been lost in translation to these blogposts. That should be fixed now.

I'm not sure about a book—there's not much money in publishing these days, at least not for the authors!—although you'd be welcome to a feedback freebie! Regardless, at some point I might make the full (working) codebase for the d8api module available, so people can work around these small typos.

What I don't want, though, is to have just basically re-created the Examples module. The focus does need to be on people writing this stuff for themselves, not looking up the answers in the back.

Anyway, thanks again,
J-P