You are here

class Router in Drupal 10

Same name and namespace in other branches
  1. 8 core/lib/Drupal/Core/Routing/Router.php \Drupal\Core\Routing\Router
  2. 9 core/lib/Drupal/Core/Routing/Router.php \Drupal\Core\Routing\Router

Router implementation in Drupal.

A router determines, for an incoming request, the active controller, which is a callable that creates a response.

It consists of several steps, of which each are explained in more details below: 1. Get a collection of routes which potentially match the current request. This is done by the route provider. See ::getInitialRouteCollection(). 2. Filter the collection down further more. For example this filters out routes applying to other formats: See ::applyRouteFilters() 3. Find the best matching route out of the remaining ones, by applying a regex. See ::matchCollection(). 4. Enhance the list of route attributes, for example loading entity objects. See ::applyRouteEnhancers().

Hierarchy

  • class \Drupal\Core\Routing\Router extends \Drupal\Core\Routing\UrlMatcher implements \Symfony\Component\Routing\Matcher\RequestMatcherInterface, \Symfony\Component\Routing\RouterInterface

Expanded class hierarchy of Router

2 files declare their use of Router
RouterTest.php in core/tests/Drupal/Tests/Core/Routing/RouterTest.php
RouterUnsupportedTest.php in core/tests/Drupal/Tests/Core/Routing/RouterUnsupportedTest.php
1 string reference to 'Router'
core.services.yml in core/core.services.yml
core/core.services.yml
1 service uses Router
router.no_access_checks in core/core.services.yml
\Drupal\Core\Routing\Router

File

core/lib/Drupal/Core/Routing/Router.php, line 31

Namespace

Drupal\Core\Routing
View source
class Router extends UrlMatcher implements RequestMatcherInterface, RouterInterface {

  /**
   * The route provider responsible for the first-pass match.
   *
   * @var \Drupal\Core\Routing\RouteProviderInterface
   */
  protected $routeProvider;

  /**
   * The list of available enhancers.
   *
   * @var \Drupal\Core\Routing\EnhancerInterface[]
   */
  protected $enhancers = [];

  /**
   * The list of available route filters.
   *
   * @var \Drupal\Core\Routing\FilterInterface[]
   */
  protected $filters = [];

  /**
   * The URL generator.
   *
   * @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
   */
  protected $urlGenerator;

  /**
   * Constructs a new Router.
   *
   * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
   *   The route provider.
   * @param \Drupal\Core\Path\CurrentPathStack $current_path
   *   The current path stack.
   * @param \Symfony\Component\Routing\Generator\UrlGeneratorInterface $url_generator
   *   The URL generator.
   */
  public function __construct(RouteProviderInterface $route_provider, CurrentPathStack $current_path, BaseUrlGeneratorInterface $url_generator) {
    parent::__construct($current_path);
    $this->routeProvider = $route_provider;
    $this->urlGenerator = $url_generator;
  }

  /**
   * Adds a route filter.
   *
   * @param \Drupal\Core\Routing\FilterInterface $route_filter
   *   The route filter.
   */
  public function addRouteFilter(FilterInterface $route_filter) {
    $this->filters[] = $route_filter;
  }

  /**
   * Adds a route enhancer.
   *
   * @param \Drupal\Core\Routing\EnhancerInterface $route_enhancer
   *   The route enhancer.
   */
  public function addRouteEnhancer(EnhancerInterface $route_enhancer) {
    $this->enhancers[] = $route_enhancer;
  }

  /**
   * {@inheritdoc}
   */
  public function match($pathinfo) : array {
    $request = Request::create($pathinfo);
    return $this
      ->matchRequest($request);
  }

  /**
   * {@inheritdoc}
   */
  public function matchRequest(Request $request) : array {
    $collection = $this
      ->getInitialRouteCollection($request);
    if ($collection
      ->count() === 0) {
      throw new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath
        ->getPath()));
    }
    $collection = $this
      ->applyRouteFilters($collection, $request);
    $collection = $this
      ->applyFitOrder($collection);
    $ret = $this
      ->matchCollection(rawurldecode($this->currentPath
      ->getPath($request)), $collection);
    return $this
      ->applyRouteEnhancers($ret, $request);
  }

  /**
   * {@inheritdoc}
   */
  protected function matchCollection($pathinfo, RouteCollection $routes) : array {

    // Try a case-sensitive match.
    $match = $this
      ->doMatchCollection($pathinfo, $routes, TRUE);

    // Try a case-insensitive match.
    if ($match === NULL && $routes
      ->count() > 0) {
      $match = $this
        ->doMatchCollection($pathinfo, $routes, FALSE);
    }
    if ($match === NULL) {
      throw 0 < count($this->allow) ? new MethodNotAllowedException(array_unique($this->allow)) : new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath
        ->getPath()));
    }
    return $match;
  }

  /**
   * Tries to match a URL with a set of routes.
   *
   * This code is very similar to Symfony's UrlMatcher::matchCollection() but it
   * supports case-insensitive matching. The static prefix optimization is
   * removed as this duplicates work done by the query in
   * RouteProvider::getRoutesByPath().
   *
   * @param string $pathinfo
   *   The path info to be parsed
   * @param \Symfony\Component\Routing\RouteCollection $routes
   *   The set of routes.
   * @param bool $case_sensitive
   *   Determines if the match should be case-sensitive of not.
   *
   * @return array|null
   *   An array of parameters. NULL when there is no match.
   *
   * @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection()
   * @see \Drupal\Core\Routing\RouteProvider::getRoutesByPath()
   */
  protected function doMatchCollection($pathinfo, RouteCollection $routes, $case_sensitive) {
    foreach ($routes as $name => $route) {
      $compiledRoute = $route
        ->compile();

      // Set the regex to use UTF-8.
      $regex = $compiledRoute
        ->getRegex() . 'u';
      if (!$case_sensitive) {
        $regex = $regex . 'i';
      }
      if (!preg_match($regex, $pathinfo, $matches)) {
        continue;
      }
      $hostMatches = [];
      if ($compiledRoute
        ->getHostRegex() && !preg_match($compiledRoute
        ->getHostRegex(), $this->context
        ->getHost(), $hostMatches)) {
        $routes
          ->remove($name);
        continue;
      }

      // Check HTTP method requirement.
      if ($requiredMethods = $route
        ->getMethods()) {

        // HEAD and GET are equivalent as per RFC.
        if ('HEAD' === ($method = $this->context
          ->getMethod())) {
          $method = 'GET';
        }
        if (!in_array($method, $requiredMethods)) {
          $this->allow = array_merge($this->allow, $requiredMethods);
          $routes
            ->remove($name);
          continue;
        }
      }
      $attributes = $this
        ->getAttributes($route, $name, array_replace($matches, $hostMatches));
      $status = $this
        ->handleRouteRequirements($pathinfo, $name, $route, $attributes);
      if (self::ROUTE_MATCH === $status[0]) {
        return $status[1];
      }
      if (self::REQUIREMENT_MISMATCH === $status[0]) {
        $routes
          ->remove($name);
        continue;
      }
      return $attributes;
    }
  }

  /**
   * Returns a collection of potential matching routes for a request.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request.
   *
   * @return \Symfony\Component\Routing\RouteCollection
   *   The initial fetched route collection.
   */
  protected function getInitialRouteCollection(Request $request) {
    return $this->routeProvider
      ->getRouteCollectionForRequest($request);
  }

  /**
   * Apply the route enhancers to the defaults, according to priorities.
   *
   * @param array $defaults
   *   The defaults coming from the final matched route.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request.
   *
   * @return array
   *   The request attributes after applying the enhancers. This might consist
   *   raw values from the URL but also upcasted values, like entity objects,
   *   from route enhancers.
   */
  protected function applyRouteEnhancers($defaults, Request $request) {
    foreach ($this->enhancers as $enhancer) {
      $defaults = $enhancer
        ->enhance($defaults, $request);
    }
    return $defaults;
  }

  /**
   * Applies all route filters to a given route collection.
   *
   * This method reduces the sets of routes further down, for example by
   * checking the HTTP method.
   *
   * @param \Symfony\Component\Routing\RouteCollection $collection
   *   The route collection.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request.
   *
   * @return \Symfony\Component\Routing\RouteCollection
   *   The filtered/sorted route collection.
   */
  protected function applyRouteFilters(RouteCollection $collection, Request $request) {

    // Route filters are expected to throw an exception themselves if they
    // end up filtering the list down to 0.
    foreach ($this->filters as $filter) {
      $collection = $filter
        ->filter($collection, $request);
    }
    return $collection;
  }

  /**
   * Reapplies the fit order to a RouteCollection object.
   *
   * Route filters can reorder route collections. For example, routes with an
   * explicit _format requirement will be preferred. This can result in a less
   * fit route being used. For example, as a result of filtering /user/% comes
   * before /user/login. In order to not break this fundamental property of
   * routes, we need to reapply the fit order. We also need to ensure that order
   * within each group of the same fit is preserved.
   *
   * @param \Symfony\Component\Routing\RouteCollection $collection
   *   The route collection.
   *
   * @return \Symfony\Component\Routing\RouteCollection
   *   The reordered route collection.
   */
  protected function applyFitOrder(RouteCollection $collection) {
    $buckets = [];

    // Sort all the routes by fit descending.
    foreach ($collection
      ->all() as $name => $route) {
      $fit = $route
        ->compile()
        ->getFit();
      $buckets += [
        $fit => [],
      ];
      $buckets[$fit][] = [
        $name,
        $route,
      ];
    }
    krsort($buckets);
    $flattened = array_reduce($buckets, 'array_merge', []);

    // Add them back onto a new route collection.
    $collection = new RouteCollection();
    foreach ($flattened as $pair) {
      $name = $pair[0];
      $route = $pair[1];
      $collection
        ->add($name, $route);
    }
    return $collection;
  }

  /**
   * {@inheritdoc}
   */
  public function getRouteCollection() : RouteCollection {
    return new LazyRouteCollection($this->routeProvider);
  }

  /**
   * This method is intentionally not implemented. Use
   * Drupal\Core\Url instead.
   *
   * @see https://www.drupal.org/node/2820197
   *
   * @throws \BadMethodCallException
   */
  public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH) : string {
    throw new \BadMethodCallException(__METHOD__ . '() is not supported. Use the \\Drupal\\Core\\Url object instead. See https://www.drupal.org/node/2820197');
  }

}

Members