From 0e71ca686dd2a723709cc2a5346d64df79ca4d5a Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Thu, 27 Oct 2016 21:19:40 -0700
Subject: [PATCH] Issue #2810303 by dawehner, Crell, tim.plunkett, klausi, Wim
 Leers: Reunite the router: One router to rule them all

---
 core/core.services.yml                        |  10 +-
 .../Drupal/Core/Routing/AccessAwareRouter.php |  41 ++-
 core/lib/Drupal/Core/Routing/Router.php       | 301 ++++++++++++++++++
 3 files changed, 334 insertions(+), 18 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Routing/Router.php

diff --git a/core/core.services.yml b/core/core.services.yml
index 5e6e2d9a80a9..850bb3542d1f 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -801,6 +801,7 @@ services:
   router.matcher.final_matcher:
     class: Drupal\Core\Routing\UrlMatcher
     arguments: ['@path.current']
+    deprecated: The "%service_id%" service is deprecated. You should use the 'router.no_access_checks' service instead.
   router.matcher:
     class: Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
     arguments: ['@router.route_provider']
@@ -808,6 +809,7 @@ services:
       - [setFinalMatcher, ['@router.matcher.final_matcher']]
     tags:
       - { name: service_collector, tag: non_lazy_route_filter, call: addRouteFilter }
+    deprecated: The "%service_id%" service is deprecated. You should use the 'router.no_access_checks' service instead.
   route_filter.lazy_collector:
     class: Drupal\Core\Routing\LazyRouteFilter
     tags:
@@ -846,11 +848,15 @@ services:
     arguments: ['@router.request_context', '@router.matcher', '@url_generator']
     tags:
       - { name: service_collector, tag: non_lazy_route_enhancer, call: addRouteEnhancer }
+    deprecated: The "%service_id%" service is deprecated. You should use the 'router.no_access_checks' service instead.
   router.no_access_checks:
-    class: Symfony\Cmf\Component\Routing\ChainRouter
+    class: \Drupal\Core\Routing\Router
+    arguments: ['@router.route_provider', '@path.current', '@url_generator']
+    tags:
+      - { name: service_collector, tag: non_lazy_route_enhancer, call: addRouteEnhancer }
+      - { name: service_collector, tag: non_lazy_route_filter, call: addRouteFilter }
     calls:
       - [setContext, ['@router.request_context']]
-      - [add, ['@router.dynamic']]
   router.path_roots_subscriber:
     class: Drupal\Core\EventSubscriber\PathRootsSubscriber
     arguments: ['@state']
diff --git a/core/lib/Drupal/Core/Routing/AccessAwareRouter.php b/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
index 9346b0ae1f79..5c3d93182ebf 100644
--- a/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
+++ b/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
@@ -5,10 +5,12 @@
 use Drupal\Core\Access\AccessManagerInterface;
 use Drupal\Core\Access\AccessResultReasonInterface;
 use Drupal\Core\Session\AccountInterface;
-use Symfony\Cmf\Component\Routing\ChainRouter;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
 use Symfony\Component\Routing\RequestContext as SymfonyRequestContext;
+use Symfony\Component\Routing\RequestContextAwareInterface;
+use Symfony\Component\Routing\RouterInterface;
 
 /**
  * A router class for Drupal with access check and upcasting.
@@ -16,11 +18,11 @@
 class AccessAwareRouter implements AccessAwareRouterInterface {
 
   /**
-   * The chain router doing the actual routing.
+   * The router doing the actual routing.
    *
-   * @var \Symfony\Cmf\Component\Routing\ChainRouter
+   * @var \Symfony\Component\Routing\Matcher\RequestMatcherInterface
    */
-  protected $chainRouter;
+  protected $router;
 
   /**
    * The access manager.
@@ -39,15 +41,15 @@ class AccessAwareRouter implements AccessAwareRouterInterface {
   /**
    * Constructs a router for Drupal with access check and upcasting.
    *
-   * @param \Symfony\Cmf\Component\Routing\ChainRouter $chain_router
-   *   The chain router doing the actual routing.
+   * @param \Symfony\Component\Routing\Matcher\RequestMatcherInterface $router
+   *   The router doing the actual routing.
    * @param \Drupal\Core\Access\AccessManagerInterface $access_manager
    *   The access manager.
    * @param \Drupal\Core\Session\AccountInterface $account
    *   The account to use in access checks.
    */
-  public function __construct(ChainRouter $chain_router, AccessManagerInterface $access_manager, AccountInterface $account) {
-    $this->chainRouter = $chain_router;
+  public function __construct(RequestMatcherInterface $router, AccessManagerInterface $access_manager, AccountInterface $account) {
+    $this->router = $router;
     $this->accessManager = $access_manager;
     $this->account = $account;
   }
@@ -56,23 +58,26 @@ public function __construct(ChainRouter $chain_router, AccessManagerInterface $a
    * {@inheritdoc}
    */
   public function __call($name, $arguments) {
-    // Ensure to call every other function to the chained router.
-    // @todo Sadly does the ChainRouter not implement an interface in CMF.
-    return call_user_func_array([$this->chainRouter, $name], $arguments);
+    // Ensure to call every other function to the router.
+    return call_user_func_array([$this->router, $name], $arguments);
   }
 
   /**
    * {@inheritdoc}
    */
   public function setContext(SymfonyRequestContext $context) {
-    $this->chainRouter->setContext($context);
+    if ($this->router instanceof RequestContextAwareInterface) {
+      $this->router->setContext($context);
+    }
   }
 
   /**
    * {@inheritdoc}
    */
   public function getContext() {
-    return $this->chainRouter->getContext();
+    if ($this->router instanceof RequestContextAwareInterface) {
+      return $this->router->getContext();
+    }
   }
 
   /**
@@ -82,7 +87,7 @@ public function getContext() {
    *   Thrown when access checking failed.
    */
   public function matchRequest(Request $request) {
-    $parameters = $this->chainRouter->matchRequest($request);
+    $parameters = $this->router->matchRequest($request);
     $request->attributes->add($parameters);
     $this->checkAccess($request);
     // We can not return $parameters because the access check can change the
@@ -114,14 +119,18 @@ protected function checkAccess(Request $request) {
    * {@inheritdoc}
    */
   public function getRouteCollection() {
-    return $this->chainRouter->getRouteCollection();
+    if ($this->router instanceof RouterInterface) {
+      return $this->router->getRouteCollection();
+    }
   }
 
   /**
    * {@inheritdoc}
    */
   public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH) {
-    return $this->chainRouter->generate($name, $parameters, $referenceType);
+    if ($this->router instanceof UrlGeneratorInterface) {
+      return $this->router->generate($name, $parameters, $referenceType);
+    }
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Routing/Router.php b/core/lib/Drupal/Core/Routing/Router.php
new file mode 100644
index 000000000000..80a31f79578a
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/Router.php
@@ -0,0 +1,301 @@
+<?php
+
+namespace Drupal\Core\Routing;
+
+use Drupal\Core\Path\CurrentPathStack;
+use Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface as BaseRouteEnhancerInterface;
+use Symfony\Cmf\Component\Routing\LazyRouteCollection;
+use Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface as BaseRouteFilterInterface;
+use Symfony\Cmf\Component\Routing\RouteProviderInterface as BaseRouteProviderInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface as BaseUrlGeneratorInterface;
+use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\RouterInterface;
+
+/**
+ * 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().
+ *
+ * This implementation uses ideas of the following routers:
+ * - \Symfony\Cmf\Component\Routing\DynamicRouter
+ * - \Drupal\Core\Routing\UrlMatcher
+ * - \Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
+ *
+ * @see \Symfony\Cmf\Component\Routing\DynamicRouter
+ * @see \Drupal\Core\Routing\UrlMatcher
+ * @see \Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
+ */
+class Router extends UrlMatcher implements RequestMatcherInterface, RouterInterface {
+
+  /**
+   * The route provider responsible for the first-pass match.
+   *
+   * @var \Symfony\Cmf\Component\Routing\RouteProviderInterface
+   */
+  protected $routeProvider;
+
+  /**
+   * The list of available enhancers.
+   *
+   * @var \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface[]
+   */
+  protected $enhancers = [];
+
+  /**
+   * Cached sorted list of enhancers.
+   *
+   * @var \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface[]
+   */
+  protected $sortedEnhancers;
+
+  /**
+   * The list of available route filters.
+   *
+   * @var \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface[]
+   */
+  protected $filters = [];
+
+  /**
+   * Cached sorted list route filters.
+   *
+   * @var \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface[]
+   */
+  protected $sortedFilters;
+
+  /**
+   * The URL generator.
+   *
+   * @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
+   */
+  protected $urlGenerator;
+
+  /**
+   * Constructs a new Router.
+   *
+   * @param \Symfony\Cmf\Component\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(BaseRouteProviderInterface $route_provider, CurrentPathStack $current_path, BaseUrlGeneratorInterface $url_generator) {
+    parent::__construct($current_path);
+    $this->routeProvider = $route_provider;
+    $this->urlGenerator = $url_generator;
+  }
+
+  /**
+   * Adds a route enhancer to the list of used route enhancers.
+   *
+   * @param \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface $route_enhancer
+   *   A route enhancer.
+   * @param int $priority
+   *   (optional) The priority of the enhancer. Higher number enhancers will be
+   *   used first.
+   *
+   * @return $this
+   */
+  public function addRouteEnhancer(BaseRouteEnhancerInterface $route_enhancer, $priority = 0) {
+    $this->enhancers[$priority][] = $route_enhancer;
+    return $this;
+  }
+
+  /**
+   * Adds a route filter to the list of used route filters.
+   *
+   * @param \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface $route_filter
+   *   A route filter.
+   * @param int $priority
+   *   (optional) The priority of the filter. Higher number filters will be used
+   *   first.
+   *
+   * @return $this
+   */
+  public function addRouteFilter(BaseRouteFilterInterface $route_filter, $priority = 0) {
+    $this->filters[$priority][] = $route_filter;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function match($pathinfo) {
+    $request = Request::create($pathinfo);
+
+    return $this->matchRequest($request);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function matchRequest(Request $request) {
+    $collection = $this->getInitialRouteCollection($request);
+    $collection = $this->applyRouteFilters($collection, $request);
+
+    if ($ret = $this->matchCollection(rawurldecode($this->currentPath->getPath($request)), $collection)) {
+      return $this->applyRouteEnhancers($ret, $request);
+    }
+
+    throw 0 < count($this->allow)
+      ? new MethodNotAllowedException(array_unique($this->allow))
+      : new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
+  }
+
+  /**
+   * 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->getRouteEnhancers() as $enhancer) {
+      $defaults = $enhancer->enhance($defaults, $request);
+    }
+
+    return $defaults;
+  }
+
+  /**
+   * Sorts the enhancers and flattens them.
+   *
+   * @return \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface[]
+   *   The enhancers ordered by priority.
+   */
+  public function getRouteEnhancers() {
+    if (!isset($this->sortedEnhancers)) {
+      $this->sortedEnhancers = $this->sortRouteEnhancers();
+    }
+
+    return $this->sortedEnhancers;
+  }
+
+  /**
+   * Sort enhancers by priority.
+   *
+   * The highest priority number is the highest priority (reverse sorting).
+   *
+   * @return \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface[]
+   *   The sorted enhancers.
+   */
+  protected function sortRouteEnhancers() {
+    $sortedEnhancers = [];
+    krsort($this->enhancers);
+
+    foreach ($this->enhancers as $enhancers) {
+      $sortedEnhancers = array_merge($sortedEnhancers, $enhancers);
+    }
+
+    return $sortedEnhancers;
+  }
+
+  /**
+   * 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->getRouteFilters() as $filter) {
+      $collection = $filter->filter($collection, $request);
+    }
+
+    return $collection;
+  }
+
+  /**
+   * Sorts the filters and flattens them.
+   *
+   * @return \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface[]
+   *   The filters ordered by priority
+   */
+  public function getRouteFilters() {
+    if (!isset($this->sortedFilters)) {
+      $this->sortedFilters = $this->sortFilters();
+    }
+
+    return $this->sortedFilters;
+  }
+
+  /**
+   * Sort filters by priority.
+   *
+   * The highest priority number is the highest priority (reverse sorting).
+   *
+   * @return \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface[]
+   *   The sorted filters.
+   */
+  protected function sortFilters() {
+    $sortedFilters = [];
+    krsort($this->filters);
+
+    foreach ($this->filters as $filters) {
+      $sortedFilters = array_merge($sortedFilters, $filters);
+    }
+
+    return $sortedFilters;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRouteCollection() {
+    return new LazyRouteCollection($this->routeProvider);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH) {
+    @trigger_error('Use the \Drupal\Core\Url object instead', E_USER_DEPRECATED);
+    return $this->urlGenerator->generate($name, $parameters, $referenceType);
+  }
+
+}
-- 
GitLab