Part 3: Routing in Drupal (Spotlight on Symfony in Drupal)

In this installment of our series on Symfony's role in Drupal, we're focusing on the Routing component. Even if it may seem simple looking from the outside, routing in Drupal is a complex task with lots of customized parts. The routing component's job is to match incoming requests to the correct controller, which is then responsible for building the response.

If you want to get into the details of routing in Drupal, check out our in-depth course on Routes and Controllers in Drupal. But for now, let's take a high-level glance at how Drupal has built upon Symfony's Routing component.

Spotlight on Symfony in Drupal

  1. Part 1: HttpKernel in Drupal
  2. Part 2: EventDispatcher in Drupal
  3. Part 3: Routing in Drupal
  4. Part 4: Utility Components in Drupal

Symfony's routing component in Drupal

Find the heart of Symfony's routing component in Drupal in /vendor/symfony/routing/Router.php. There's a lot going on in this file, but we're going to take a look at its 2 primary methods: getRouteCollection and match.

    /**
     * @return RouteCollection
     */
    public function getRouteCollection()
    {
        return $this->collection ??= $this->loader->load($this->resource, $this->options['resource_type']);
    }

The getRouteCollection method loads all the route mappings defined by the application. As an example, Drupal might define routes like this:

system.admin:
  path: '/admin'
  defaults:
    _controller: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage'
    _title: 'Administration'

As you might imagine, loading all these routes is computationally expensive, so there is code here to help us cache and optimize this information.

Now let's look at the match method.

    public function match(string $pathinfo): array
    {
        return $this->getMatcher()->match($pathinfo);
    }

The match method identifies which route in our collection corresponds to the current request's path. It's instrumental in understanding which part of our code will respond to a particular request. You can see an example matcher implementation in the /vendor/symfony/routing/Matcher/UrlMatcher.php file. The UrlMatcher class is the main matcher for translating between the path of an incoming request and the route that will ultimately generate a response.

How Drupal enhances Symfony's routing component

You can find Drupal's version of the Router in the core/lib/Drupal/Core/Routing directory. There's a lot more going on here than in the base Symfony routing component, all because Drupal adds several enhancements to meet its needs.

One of the most useful additions Drupal provides is access control at the route level. Have a look at AccessAwareRouter class's implementation of the matchRequest method:

  public function matchRequest(Request $request): array {
    $parameters = $this->router->matchRequest($request);
    $request->attributes->add($parameters);
    $this->checkAccess($request);
    // We can not return $parameters because the access check can change the
    // request attributes.
    return $request->attributes->all();
  }

Here, Drupal matches a URL to a controller and checks that the route has the correct access permissions, giving developers a helping hand in managing complex permission requirements. Our Add Access Checking to a Route tutorial can give you an in-depth understanding of this.

Next, you might be curious about how Drupal gathers information about routes. Delve into this by looking at the RouteBuilder.php file. It's packed with information on how Drupal builds its unique routing system.

// ...
 public function rebuild() {
    // ...

    $collection = new RouteCollection();
    foreach ($this->getRouteDefinitions() as $routes) {
      // The top-level 'routes_callback' is a list of methods in controller
      // syntax, see \Drupal\Core\Controller\ControllerResolver. These methods
      // should return a set of \Symfony\Component\Routing\Route objects, either
      // in an associative array keyed by the route name, which will be iterated
      // over and added to the collection for this provider, or as a new
      // \Symfony\Component\Routing\RouteCollection object, which will be added
      // to the collection.
      if (isset($routes['route_callbacks'])) {
        foreach ($routes['route_callbacks'] as $route_callback) {
          $callback = $this->controllerResolver->getControllerFromDefinition($route_callback);
          if ($callback_routes = call_user_func($callback)) {
            // If a RouteCollection is returned, add the whole collection.
            if ($callback_routes instanceof RouteCollection) {
              $collection->addCollection($callback_routes);
            }
            // Otherwise, add each Route object individually.
            else {
              foreach ($callback_routes as $name => $callback_route) {
                $collection->add($name, $callback_route);
              }
            }
          }
        }
        unset($routes['route_callbacks']);
      }
      foreach ($routes as $name => $route_info) {
        // ...
        $route = new Route($route_info['path'], $route_info['defaults'], $route_info['requirements'], $route_info['options'], $route_info['host'], $route_info['schemes'], $route_info['methods'], $route_info['condition']);
        $collection->add($name, $route);
      }
    }

    // DYNAMIC is supposed to be used to add new routes based upon all the
    // static defined ones.
    $this->dispatcher->dispatch(new RouteBuildEvent($collection), RoutingEvents::DYNAMIC);

    // ALTER is the final step to alter all the existing routes. We cannot stop
    // people from adding new routes here, but we define two separate steps to
    // make it clear.
    $this->dispatcher->dispatch(new RouteBuildEvent($collection), RoutingEvents::ALTER);

    $this->checkProvider->setChecks($collection);

    $this->dumper->addRoutes($collection);
    // ...
    $this->dispatcher->dispatch(new Event(), RoutingEvents::FINISHED);

Here we can see the key steps that make Drupal's router more robust than the one provided by Symfony.

  1. Drupal gets all the route definitions described in YAML configuration throughout the codebase. These get added to a $collection variable.
  2. The rebuild method uses the EventDispatcher to send (dispatch) a RoutingEvents::DYNAMIC event. This enables any contributed module or custom code to register their own dynamic route and add them to the route collection.
  3. Drupal dispatches another event, RoutingEvents::ALTER, to allow any other code to alter route definitions compiled thus far. This is akin to a menu_alter hook in older versions of Drupal (<= Drupal 7).
  4. The dumper adds collected route information to a database table using the \Psr\Log\LoggerInterface.
  5. The rebuild method fires off one more event, RoutingEvents::FINISHED, in case any application code needs to react to the route collection rebuilding.

By making use of the Event system, Drupal is able to support dynamic routes and the altering of existing routes. This makes the system more flexible and helps provide the functionality that the ecosystem of contributed modules have come to expect over the years.

Learn more

Next up

  • We'll take a look at what we're terming "utility components". Other Symfony components that are included in Drupal, but that aren't extensively customized by Drupal (if at all).
Related Topics

Add new comment

Filtered HTML

  • Web page addresses and email addresses turn into links automatically.
  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <code class> <ul type> <ol start type> <li> <dl> <dt> <dd><h3 id> <p>
  • Lines and paragraphs break automatically.

About us

Drupalize.Me is the best resource for learning Drupal online. We have an extensive library covering multiple versions of Drupal and we are the most accurate and up-to-date Drupal resource. Learn more