Drupal 8 Routing

| | 7 min read

Drupal 8's routing system works with the Symfony HTTP Kernel. To do basic route operations, you don't need to learn very much about the Symfony HTTP Kernel.

Routing system in Drupal 8 is introduced by replacing the routing parts of hook_menu() in Drupal 7, and its parts, used for creating tabs, menu entries, contextual links, access arguments and actions are taken over by other sub-systems of YAML files (.yml) that provide metadata about each item and its corresponding PHP classes that serves the underlying logic.

A route is a defined path, which Drupal uses to return some sort of content. For example, the default front page for Drupal, '/node' is a defined route. When Drupal receives a page request, the routing system is responsible for matching requested paths to controllers and checks the route's definition to return content. If route doesn't matches, leads to 404 (Page not found).

In Drupal 7 we can use hook_menu to set up a custom page with a given path. In Drupal 8, paths are managed using a MODULE_NAME.routing.yml file to describe each route and a corresponding controller class that extends from a base controller. Each controller class have in its own file, where the file should be named to match the class name. This controller logic might have accommodated in a separate MODULE_NAME.pages.inc file in Drupal 7. A sample code in Drupal 7 might look like this:

function example_menu() {
  $items = array();
  $items['home_page'] = array(
    'title' => 'Home Page',
    'page callback' => example_home_page',
    'access arguments' => array('access home page content'),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'example.pages.inc'
  );
  return $items;
}

function example_home_page() {
  return t(‘Some data can be returned.’);
}

In Drupal 8 we have to define the route information into a file named MODULE_NAME.routing.yml. Routes have unique names that don’t necessary have anything to do with their paths, but should be unique identifiers. They should be prefixed with your module name to keep uniqueness there by avoiding name clashes from other modules. You can use _content or _form instead of _controller in this YAML file to specify the callback. You should now always use _controller to identify the related controller and method name.

example.home_page_controller:
  path: '/home'
  defaults:
    _controller: '\Drupal\example\Controller\HomePageController::homePage'
    _title: 'Home Page'
  requirements:
    _permission: 'access home page content'

We now use a preceding slash on paths! In Drupal 7, the path would have been home, and in Drupal 8 /home is required! The page callback goes into a controller class, named HomePageController.php, and is located at MODULE/src/Controller/HomePageController.php. The file name need to match the class name of the controller, and all your module’s controllers should been placed in that /src/Controller directory. Drupal has adopted this location that is dictated by the PSR-4 standard. Anything that is located in the expected place in /src directory will be autoloaded when needed, without using method module_load_include() or listing file locations in the .info file, as we did in Drupal 7.

The method used inside the controller to manage this route can have any name, 'homePage' is an arbitrary choice for the function in the example. The function used in the controller file should match the YAML (.yml) file, where it is described as CLASS_NAME::METHOD. The Contains line in the class @file documentation content, matches the _controller entry in the YAML file above.

A controller class can manage one or more routes, as long as each has a method (function) for its callback and its own entry in the routing YAML (.yml) file. For example, the core nodeController manages four of the routes listed in node.routing.yml. Another change from Drupal 7 is that, the controller should always return a render array, not text or HTML.

Drupal 8's routes includes placeholder elements which designate places where the URL contains dynamic values (slug). In the controller callback method this specified value will be available, when a variable with the same name is used in the callback method. For example see below example.routing.yml.

example.name:
  path: '/example/{myname}'
  defaults:
    _controller: '\Drupal\example\Controller\ExampleController::content'
  requirements:
    _permission: 'access content'

The {myname} element in the URL is called a slug and is available as $myname in the controller callback method. Translation is available within the controller as $this->t() instead of t(). This works because ControllerBase has added the StringTranslationTrait.

/**
 * @file
 * Contains \Drupal\example\Controller\HomePageController.
 */
namespace Drupal\example\Controller;

use Drupal\Core\Controller\ControllerBase;

class HomePageController extends ControllerBase {
  public function homePage() {
    return [
        '#markup' => $this->t('Something goes here!'),
    ];
  }

Some paths need additional arguments or parameters. In Drupal 7, If my page had a extra parameters, it would look like this.

function example_menu() {
  $items = array();
  $items[‘home/%/%’] = array(
    'title' => 'Home Page',
    'page callback' => 'example_home_page',
    ‘page arguments’ => array(1, 2),
    'access arguments' => array('access home content'),
    'type' => MENU_NORMAL_ITEM,
  );
  return $items;
}

function example_home_page($first, $second) {
  return t(‘Something goes here’);
}

In Drupal 8, the YAML file will look like this (adding the additional parameters to the path):

example.home_page_controller:
  path: '/home/{first}/{second}'
  defaults:
    _controller: '\Drupal\example\Controller\HomePageController::homePage'
    _title: 'Home Page’
  requirements:
    _permission: 'access content'

The controller looks like this (showing the parameters/arguments in the function signature).

/**
 * @file
 * Contains \Drupal\example\Controller\HomePageController.
 */
namespace Drupal\example\Controller;

use Drupal\Core\Controller\ControllerBase;

class HomePageController extends ControllerBase {
  public function homePage($first, $second) {
    // Do something with $first and $second.
    return [
        '#markup => $this->t('Something goes here!'),
    ];
  }
}

Anything in the path could be altered by a user, so we will want to test for valid values and ensure that the values are safe to use. If the system does any sanitization of the values, or if this is a straight pass-through of whatever is in the URL. So I would probably assume that. I need to type hint and sanitize these values, for my code to work.

The above code will work correctly, only for the specific path, with both parameters. Neither the path /home, nor /home/first will work, only /home/first/second. If you want the parameters to be optional, so /home, /home/first, and /home/first/second are all valid paths, you need to make some changes to the YAML(.yml) file. By adding the arguments to the defaults section of route, we are telling the controller, to treat the base path as the main route and the two additional parameters as the path alternatives. You can set default value for the parameters. The empty value says they are optional in the callback, or you could give them a fixed default value to be used if they are not present in the URL.

example.home_page_controller:
  path: '/home/{first}/{second}'
  defaults:
    _controller: '\Drupal\example\Controller\HomePageController::homePage'
    _title: 'Home Page'
    first: ''
    second: ''
  requirements:
    _permission: 'access home content'

Restricting Parameters

After setting up parameters you should also provide information about what values will be allowed for the parameters. You can do this by adding some more information to the YAML file. The below example indicates that $first can have only the values ‘Y’ or ‘N’, and $second must be numeric. Any parameters that don’t match these rules will return a 404 error code. Basically the code is expecting to evaluate a regular expression, to determine whether the path is valid.

example.home_page_controller:
  path: '/home/{first}/{second}'
  defaults:
    _controller: '\Drupal\example\Controller\HomePageController::homePage'
    _title: 'Home Page'
    first: ''
    second: ''
  requirements:
    _permission: 'access content'
    first: Y|N
    second: \d+

 

 

Entity Parameters

Like in Drupal 7, while creating a route, we can pass an entity id, with entity name specified, we can set the system to automatically pass the entity object to the callback instead of just the entity id. This is called upcasting. In Drupal 7, we did this by using %node instead of % alone. In Drupal 8, we just need to use the name of the entity type as the parameter name, for example, {user} or {node}.

example.home_page_controller:
  path: '/node/{node}'
  defaults:
    _controller: '\Drupal\example\Controller\HomePageController::homePage'
    _title: 'Node Page'
  requirements:
    _permission: 'access content'

In Drupal 8 the upcasting only happens, if we have type-hinted the entity object in controller parameter. Otherwise it will simply be the values of parameters.

JSON Callbacks

Render array will be converted to HTML automatically by the system. But what if you wanted that path to display JSON instead? There are some old documentation indicates that you need to add _format: json to the routing YAML file in the requirements section, but that is not required, if you do not want to provide alternate formats at the same path. Create the array of values that you want to return and then return it as a JsonResponse object. Be sure to include use Symfony\Component\HttpFoundation\JsonResponse at the top of your class.

/**
 * @file
 * Contains \Drupal\example\Controller\HomePageController.
 */
namespace Drupal\example\Controller;

use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\JsonResponse;

class HomePageController extends ControllerBase {
  public function homePage() {
    $return = array();
    // Create key/value array.
    return new JsonResponse($return);
  }
}

Access Control

Access control in Dripal 8 is handled by the MODULE.routing.yml file. There are many ways to control access to contents.

 

 

Allow Access by anyone to this Path

example.home_page_controller:
  path: '/home'
  requirements:
    _access: 'TRUE'

Limit access to users with ‘access content’ permission

example.home_page_controller:
  path: '/home'
  requirements:
    _permission: 'access content'

Limit Access to users with the ‘Admin’ Role

example.home_page_controller:
  path: '/home'
  requirements:
    _role: 'admin'

Limit Access to users who have ‘edit’ Permission on an Entity (when the Entity is Provided in the Path)

example.home_page_controller:
  path: '/node/{node}'
  requirements:
    _entity_access: 'node.edit'

Hook_menu_alter

What to do, if a route already exists (created by core or some other contributed modules) and you want to alter something about it? In Drupal 7 that is done with hook_menu_alter, but it is is also removed in Drupal 8. It’s a little more complicated now for Drupal 7 developers.

A class file at MODULE/src/Routing/CLASSNAME.php location extends RouteSubscriberBase that finds the route we wants to alter using the alterRoutes() function and changes the alterations as required.

/**
 * @file
 * Contains \Drupal\node\Routing\RouteSubscriber.
 */

namespace Drupal\node\Routing;

use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;

/**
 * Listens to the dynamic route events.
 */
class RouteSubscriber extends RouteSubscriberBase {

  /**
   * {@inheritdoc}
   */
  protected function alterRoutes(RouteCollection $collection) {
    // As nodes are the primary type of content, the node listing should be
    // easily available. In order to do that, override admin/content to show
    // a node listing instead of the path's child links.
    $route = $collection->get('system.admin_content');
    if ($route) {
      $route->setDefaults(array(
        '_title' => 'Content',
        '_entity_list' => 'node',
      ));
      $route->setRequirements(array(
        '_permission' => 'access content overview',
      ));
    }
  }

}

There is also a MODULE.services.yml file in the module to wire up the menu_alter, with an entry that points to the class that does the alterations.

services:
  node.route_subscriber:
    class: Drupal\node\Routing\RouteSubscriber
    tags:
      - { name: event_subscriber }

Hope this helps!