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