Understanding Drupal 8, part 3: Routing

Part 3: Routing

This is the 3rd part of the article ‘Understanding Drupal 8’. During the previous parts we learned how Drupal 8 is structured compared to Symfony. Now it is a good idea to get an understanding on how a request is handled by Drupal 8. First, we’ll have a look at the bootstrapping phase and the general flow of control. Then, we’ll learn about event subscribers, an important concept you need to understand before learning about the request handling itself.

Flow of control

Now we have learned about the Drupal 8, we'll have a look at what happens when a request enters Drupal 8:

  1. Bootstrap configuration:
    • Read the settings.php file, generate some other settings dynamically, and store them both in global variables and the Drupal\Component\Utility\Settings singleton object.
    • Start the class loader, that takes care of loading classes.
    • Set the Drupal error handler.
    • Detect if Drupal is actually installed. If it is not, redirect to the installer script.
  2. Create the Drupal kernel.
  3. Initialize the service container (either from cache or from rebuild).
  4. Add the container to the Drupal static class.
  5. Attempt to serve page from static page cache (just like Drupal 7).
  6. Load all variables (variable_get).
  7. Load other necessary (procedural) include files.
  8. Register stream wrappers (public://, private://, temp:// and custom wrappers).
  9. Create the HTTP Request object (using the Symfony HttpFoundation component).
  10. Let the DrupalKernel handle it and return a response.
  11. Send the response.
  12. Terminate the request (modules can act upon this event).

The interesting part now is what happens during the request handling phase. To understand this, you will have to know about event subscribers first.

Event subscribers

Previously we have had a look at compiler passes. One particularly important usage of tagged services are the event_subscriber-tagged services. These services should implement the EventSubscriberInterface, and are basically event listeners. The event subscribes have method getSubscribedEvents that defines which events should be mapped to which methods. A priority can be set to define in what order the actions should be executed. In Drupal core only a handful of events are being used:

  • kernel.request
    Occurs at the very beginning of request dispatching.
  • kernel.response
    Occurs once a response was created for replying to a request.
  • routing.route_dynamic
    Is fired to allow modules to register additional routes.
  • routing.route_alter
    Is fired on a route collection to allow changes to routes. This is used in core to add some checks and parameter conversions.

Any Drupal developer should know about event subscribers. Especially the kernel.request is important as it basically takes over the role of hook_init. Another important event is the routing.route_dynamic event. Because the usual routing configuration is static in Drupal 8 (in YAML), this event was introduced to be able to create dynamic routes. Previously this was all done in hook_menu, but that hook is now only used to generate menu items. For example, the block module uses the routing.route_dynamic event to register menu routes for the (per-theme) blocks configuration page in \Drupal\block\Routing\RouteSubscriber.

You might wonder why these event listeners are not just implemented using module hooks. The reason for this is that this solution is way more efficient as the available listeners is compiled into the service container configuration, which is cached! Also, it was attempted to make the Drupal core as object-oriented as possible.

From request to response

When a request enters Drupal, the system is bootstrapped and the DrupalKernel is booted. The handle method of the DrupalKernel is called, which delegates the call to (Symfony2’s) HttpKernel, which further handles the request.

The HttpKernel dispatches the 'kernel.request' event. Several subscribers listen to this event in the following order:

  • AuhtenticationSubscriber
    Loads the session and sets the global user.
  • LanguageRequestSubscriber
    Detects the current language.
  • PathSubscriber
    Converts the url to a system path (url aliases, etc).
  • LegacyRequestSubscriber
    Allows setting a custom theme and initializes it.
  • MaintenanceModeSubscriber
    If in maintenance mode, show the maintenance page.
  • RouteListener
    Gets the a fully loaded router object.
  • AccessSubscriber
    Checks if client has access to the router object.

The RouterListener is where the routing work happens. It asks the router service to get the active route properties. Drupal uses a different router service than Symfony: the DynamicRouter that was provided the Symfony CMF (http://cmf.symfony.com/) extension. The main difference with Symfony’s regular router is that DynamicRouter supports enhancers, which are described in the following section. The DynamicRouter then delegates the task of finding the active route (Drupal 7’s equivalent of the current_path) to the NestedMatcher, which in turn delegates it to the RouteProvider. The RouteProvider finds a collection of matching routes in the 'router' table, which contains a cached list of all existing routes in the CMS. The table is filled by the route builder, which will be described later. The selected routes are ordered by path length: the longest the matching path is deemed the most important. Then, the NestedMatcher asks the UrlMatcher to select the one specific route in the collection to be used as the active one. In practice, the UrlMatcher does this by selecting the first (so longest matching) route in the collection that have the correct method (get/post) and scheme (http/https) as required by the route. More about route requirements later. In practice there is usually just one matching path so the UrlMatcher is of limited importance. The end result is the id of the active route, such as node.add_page.

I feel I should also mention route filters, even though you are unlikely to run into them in practice. These route filters, which can be added as service with the tag 'route_filter', are called by the NestedMatcher and can filter the route collection directly after they were returned by the RouteProvider. The Drupal core only uses this mechanism to filter routes that do not respond in the MIME type as requested in the request HTTP headers (MimeTypeMatcher), but only if the '_format' route requirement is explicitly specified!

After finding the active route, the DynamicRouter continues by calling the route enhancers. These will finalize the router properties and convert route parameters. This process is described in the next section. Then, the found router properties are returned to the RouterListener, which sets the properties in the request object. After that, the access checking is performed (AccessSubscriber) and, finally, the controller method is called with the correct arguments. The response returned by the controller method is sent back to the client.

Now that we know about the request handling process, we’ll take a detailed look at routes.

Routes

In Drupal 7, hook_menu was used to register page callbacks along with their titles, arguments and access requirements. In Drupal 8, the routing process of Symfony has been adopted, which is much more flexible.. and complex!

In Drupal 8 page callbacks are no longer functions. Instead, they are methods in controller classes. The available routes are now configured in a file called {module}.routing.yml in the module folder. For example, the user logout page route configuration looks like this:

user.logout:
  path: '/user/logout'

 defaults:
    _controller: '\Drupal\user\Controller\UserController::logout'

  requirements:
    _user_is_logged_in: 'TRUE'

Notice that every route has an id (user.logout) and a path. The path defines when may contain parameters, but we'll look at that later. The 'defaults' section is very important as it can be used to control what needs to be done in case the request matches the path. The 'requirements' section defines if the request should be handles at all. The latter usually contains information related to access checks, the Symfony alternative to the Drupal 7 'access arguments' and 'access callback'.

Available 'defaults' keys:

  • _controller
    The specified method is simply called with the specified route parameters, and is expected to return a response.
  • _content
    If specified, the _controller is set based on the request's mime type, and fills the content of the response with the result of the specified method (usually a string or render array).
  • _form
    If specified, the _controller is set to HtmlFormController::content, which responds with the specified form. This form must be a fully qualified class name (or service id) that implements FormInterface and usually extends FormBase. Indeed, form building has also become object oriented!
  • _entity_form
    If specified, the _controller is set to HtmlEntityFormController::content, which responds with the specified entity form (specified as {entity_type}.{add|edit|delete}).
  • _entity_list
    If specified, the _controller is set to HtmlFormController::content, and _content to EntityListController::listing, which renders a list of entities based on the entity type's list controller.
  • _entity_view
    If specified, the _controller is set to HtmlFormController::content, and _content to EntityViewController::view, which renders the entity based on the entity type's view controller.
  • _title
    The title of the page (string).
  • _title_callback
    The title of the page (method callback).

As you can see, route keys are altered during the routing process based on other route keys. This makes it easy to simply output an entity without having to set the _controller, _content and other keys; this is done automatically. This feature is implemented using route enhancers, which are services tagged with 'route_enhancer'. They implement the RouteEnhancerInterface. There are a handful of important enhancers in the Drupal core. If you'd like to explore them, have a look at ContentControllerEnhancer, FormEnhancer and EntityRouteEnhancer. It seems unlikely that you need to add your own custom enhancers.

Available 'requirements' keys:

  • _permission
    The current user must have the specified permission.
  • _role
    The current user must have the specified role.
  • _method
    The allowed HTTP methods (GET, POST, etc).
  • _scheme
    Set to https or http. The request scheme must be the same as the specified scheme. This property is also taken into account when generating urls (Drupal::url(..)) rather than routing. If set, urls will have this scheme set fixed.
  • _node_add_access
    A custom access check for adding new nodes of some node type.
  • _entity_access
    A generic access checker for entities.
  • _format
    Mime type formats.

Most (but not all) of the requirements listed above are checked by access checkers that we'll describe shortly. In practice you may find that you need create a custom access checker, because Drupal 7 'access callback' is no longer available. The requirement key '_node_add_access' for example, is listened to by NodeAddAccessCheck, a custom access check added by the node module.

Access checking is performed by the AccessManager, which subscribes to the kernel.request event. It invokes the access checks, which must be classes implementing AccessInterface. We have seen before that they are registered as services with the access_check tag. The access checks have an access method which is used to test if the client should have access to the specified (active) route. If access is denied, an exception is raised which is handled by showing the 'access denied' page.

Path parameters

In practice you often need parameters (a.k.a placeholders) for your page callbacks. In the sample route definition below, the path contains one parameter named 'node'. Parameters are identified by the accolades around them in the route path. For every route certain options are stored. The controller method receives these parameters as arguments. The parameters are mapped to the arguments with the same name. So in this case, the page method of the NodeController has one argument: $node. There may be multiple parameters in a route, but their names should be unique.

node.view:
  path: '/node/{node}'

  defaults:
    _content: '\Drupal\node\Controller\NodeController::page'
    _title_callback: '\Drupal\node\Controller\NodeController::pageTitle'
  requirements:
    _entity_access: 'node.view'

The value that is passed as the argument is by default the value in the url (a string), but is often converted using a parameter converter. In the sample above, the NodeController will not get the node id, but a fully loaded node entity. Parameter conversion is performed by the ParamConverterManager, which subscribes to the kernel.request event. It contains registered implementations of ParamConverterInterface (services with tag paramconverter). When handling a request, the ParamConverterManager (which also is an enhancer) traverses the active route's parameters and calls the convert method of the parameter converter. If possible, a 'string' parameter value is then replaced with a full object. Notice that if the controller method's argument is not explicitly type hinted, the unconverted parameter is passed.

The EntityConverter is the only parameter converter in Drupal core, but also used extensively! If a parameter has a name which matches an entity type ('node', 'user', etc), it is automatically converted to a full entity object!. The value is regarded as the entity id. A 404 is automatically given if the conversion failed (the entity does not exist).

You can make a parameter optional by specifying a value for it in the defaults section. This only works for parameters that are not converted. Optional parameters may only occur after required parameters, not vice vera.

Route building

Routing is, obviously, done during a request. But then it makes use of a previously built 'router' table. The RouteBuilder service is responsible for filling the table when it is necessary to do so (when clearing the cache, for instance). During a request this router table is used to find the active request and complete it. This separation was done for performance reasons. The RouteBuilder works by collecting all of the configured (static) routes (yml-files, described below) and creating a collection containing them. It also calls the route.route_dynamic event, in which event subscribers may register additional routes (see the example above for the block module). Then the routing.route_alter event is called for the routes. There are several subscribers to this event. Some especially important operations here are related to access checks and parameter conversion.

We have just mentioned access checks. When handling a request, they test for a specific route if the client has access. In reality, not all available access checks are tested for every route. Instead, during the route building phase, it is determined per route which access checks apply. This is done by the AccessManager, which listens to the router.route_alter event. There are static and dynamic access checkers. The StaticAccessCheckInterface has a method appliesTo method, which returns an array of requirement keys that it applies to. Dynamic access checkers are tested per route by the applies method. The AccessManager adds an option '_access_checks' to the route in the router table. This information is used when actually checking access of the active route when handling a request. Notice that at least one access check should apply for a route, otherwise the route will never be accessible!

We have also mentioned parameter converters. It was described that a parameter is converted by a parameter converter. Per parameter, there may be just one (or none) applicable parameter converter. Just like with the access checks, this applicable parameter converter is found during the route building phase by the ParamConverterManager. This service checks for every existing route parameter the applies method of all existing parameter converters, and adds an option 'converter' to the route parameter definition in the router table. This information is used when actually converting the parameters of the active route when handling a request.

Conclusion

In this part we have learned about the request handling phase. Also, you should now have an understanding of routing in Drupal 8 and how it works internally! In fact, you know what happens from request to response. However, there are still some interesting concepts in Drupal 8 that you should know about. These will be described in the next and final part, which will be published next week.

Other parts

Part 1: The Structure of Drupal 8
Part 2: Service Container
Part 3: Routing
Part 4: Plugins and Entities

Click here for more info on Cipix Internet Agency.

Interesse? 

Delen