annotate core/lib/Drupal/Core/Routing/Router.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\Core\Routing;
Chris@0 4
Chris@0 5 use Drupal\Core\Path\CurrentPathStack;
Chris@14 6 use Drupal\Core\Routing\Enhancer\RouteEnhancerInterface;
Chris@0 7 use Symfony\Cmf\Component\Routing\LazyRouteCollection;
Chris@14 8 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
Chris@0 9 use Symfony\Cmf\Component\Routing\RouteProviderInterface as BaseRouteProviderInterface;
Chris@0 10 use Symfony\Component\HttpFoundation\Request;
Chris@0 11 use Symfony\Component\Routing\Exception\MethodNotAllowedException;
Chris@0 12 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
Chris@0 13 use Symfony\Component\Routing\Generator\UrlGeneratorInterface as BaseUrlGeneratorInterface;
Chris@0 14 use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
Chris@0 15 use Symfony\Component\Routing\RouteCollection;
Chris@0 16 use Symfony\Component\Routing\RouterInterface;
Chris@0 17
Chris@0 18 /**
Chris@0 19 * Router implementation in Drupal.
Chris@0 20 *
Chris@0 21 * A router determines, for an incoming request, the active controller, which is
Chris@0 22 * a callable that creates a response.
Chris@0 23 *
Chris@0 24 * It consists of several steps, of which each are explained in more details
Chris@0 25 * below:
Chris@0 26 * 1. Get a collection of routes which potentially match the current request.
Chris@0 27 * This is done by the route provider. See ::getInitialRouteCollection().
Chris@0 28 * 2. Filter the collection down further more. For example this filters out
Chris@0 29 * routes applying to other formats: See ::applyRouteFilters()
Chris@0 30 * 3. Find the best matching route out of the remaining ones, by applying a
Chris@0 31 * regex. See ::matchCollection().
Chris@0 32 * 4. Enhance the list of route attributes, for example loading entity objects.
Chris@0 33 * See ::applyRouteEnhancers().
Chris@0 34 *
Chris@0 35 * This implementation uses ideas of the following routers:
Chris@0 36 * - \Symfony\Cmf\Component\Routing\DynamicRouter
Chris@0 37 * - \Drupal\Core\Routing\UrlMatcher
Chris@0 38 * - \Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
Chris@0 39 *
Chris@0 40 * @see \Symfony\Cmf\Component\Routing\DynamicRouter
Chris@0 41 * @see \Drupal\Core\Routing\UrlMatcher
Chris@0 42 * @see \Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
Chris@0 43 */
Chris@0 44 class Router extends UrlMatcher implements RequestMatcherInterface, RouterInterface {
Chris@0 45
Chris@0 46 /**
Chris@0 47 * The route provider responsible for the first-pass match.
Chris@0 48 *
Chris@0 49 * @var \Symfony\Cmf\Component\Routing\RouteProviderInterface
Chris@0 50 */
Chris@0 51 protected $routeProvider;
Chris@0 52
Chris@0 53 /**
Chris@0 54 * The list of available enhancers.
Chris@0 55 *
Chris@14 56 * @var \Drupal\Core\Routing\EnhancerInterface[]
Chris@0 57 */
Chris@0 58 protected $enhancers = [];
Chris@0 59
Chris@0 60 /**
Chris@0 61 * The list of available route filters.
Chris@0 62 *
Chris@14 63 * @var \Drupal\Core\Routing\FilterInterface[]
Chris@0 64 */
Chris@0 65 protected $filters = [];
Chris@0 66
Chris@0 67 /**
Chris@0 68 * The URL generator.
Chris@0 69 *
Chris@0 70 * @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
Chris@0 71 */
Chris@0 72 protected $urlGenerator;
Chris@0 73
Chris@0 74 /**
Chris@0 75 * Constructs a new Router.
Chris@0 76 *
Chris@0 77 * @param \Symfony\Cmf\Component\Routing\RouteProviderInterface $route_provider
Chris@0 78 * The route provider.
Chris@0 79 * @param \Drupal\Core\Path\CurrentPathStack $current_path
Chris@0 80 * The current path stack.
Chris@0 81 * @param \Symfony\Component\Routing\Generator\UrlGeneratorInterface $url_generator
Chris@0 82 * The URL generator.
Chris@0 83 */
Chris@0 84 public function __construct(BaseRouteProviderInterface $route_provider, CurrentPathStack $current_path, BaseUrlGeneratorInterface $url_generator) {
Chris@0 85 parent::__construct($current_path);
Chris@0 86 $this->routeProvider = $route_provider;
Chris@0 87 $this->urlGenerator = $url_generator;
Chris@0 88 }
Chris@0 89
Chris@0 90 /**
Chris@14 91 * Adds a route filter.
Chris@0 92 *
Chris@14 93 * @param \Drupal\Core\Routing\FilterInterface $route_filter
Chris@14 94 * The route filter.
Chris@0 95 */
Chris@14 96 public function addRouteFilter(FilterInterface $route_filter) {
Chris@14 97 $this->filters[] = $route_filter;
Chris@0 98 }
Chris@0 99
Chris@0 100 /**
Chris@14 101 * Adds a route enhancer.
Chris@0 102 *
Chris@14 103 * @param \Drupal\Core\Routing\EnhancerInterface $route_enhancer
Chris@14 104 * The route enhancer.
Chris@0 105 */
Chris@14 106 public function addRouteEnhancer(EnhancerInterface $route_enhancer) {
Chris@14 107 $this->enhancers[] = $route_enhancer;
Chris@0 108 }
Chris@0 109
Chris@0 110 /**
Chris@0 111 * {@inheritdoc}
Chris@0 112 */
Chris@0 113 public function match($pathinfo) {
Chris@0 114 $request = Request::create($pathinfo);
Chris@0 115
Chris@0 116 return $this->matchRequest($request);
Chris@0 117 }
Chris@0 118
Chris@0 119 /**
Chris@0 120 * {@inheritdoc}
Chris@0 121 */
Chris@0 122 public function matchRequest(Request $request) {
Chris@0 123 $collection = $this->getInitialRouteCollection($request);
Chris@14 124 if ($collection->count() === 0) {
Chris@14 125 throw new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
Chris@14 126 }
Chris@0 127 $collection = $this->applyRouteFilters($collection, $request);
Chris@17 128 $collection = $this->applyFitOrder($collection);
Chris@0 129
Chris@0 130 if ($ret = $this->matchCollection(rawurldecode($this->currentPath->getPath($request)), $collection)) {
Chris@0 131 return $this->applyRouteEnhancers($ret, $request);
Chris@0 132 }
Chris@0 133
Chris@0 134 throw 0 < count($this->allow)
Chris@0 135 ? new MethodNotAllowedException(array_unique($this->allow))
Chris@0 136 : new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
Chris@0 137 }
Chris@0 138
Chris@0 139 /**
Chris@0 140 * Tries to match a URL with a set of routes.
Chris@0 141 *
Chris@0 142 * @param string $pathinfo
Chris@0 143 * The path info to be parsed
Chris@0 144 * @param \Symfony\Component\Routing\RouteCollection $routes
Chris@0 145 * The set of routes.
Chris@0 146 *
Chris@0 147 * @return array|null
Chris@0 148 * An array of parameters. NULL when there is no match.
Chris@0 149 */
Chris@0 150 protected function matchCollection($pathinfo, RouteCollection $routes) {
Chris@0 151 // Try a case-sensitive match.
Chris@0 152 $match = $this->doMatchCollection($pathinfo, $routes, TRUE);
Chris@0 153 // Try a case-insensitive match.
Chris@0 154 if ($match === NULL && $routes->count() > 0) {
Chris@0 155 $match = $this->doMatchCollection($pathinfo, $routes, FALSE);
Chris@0 156 }
Chris@0 157 return $match;
Chris@0 158 }
Chris@0 159
Chris@0 160 /**
Chris@0 161 * Tries to match a URL with a set of routes.
Chris@0 162 *
Chris@0 163 * This code is very similar to Symfony's UrlMatcher::matchCollection() but it
Chris@0 164 * supports case-insensitive matching. The static prefix optimization is
Chris@0 165 * removed as this duplicates work done by the query in
Chris@0 166 * RouteProvider::getRoutesByPath().
Chris@0 167 *
Chris@0 168 * @param string $pathinfo
Chris@0 169 * The path info to be parsed
Chris@0 170 * @param \Symfony\Component\Routing\RouteCollection $routes
Chris@0 171 * The set of routes.
Chris@0 172 * @param bool $case_sensitive
Chris@0 173 * Determines if the match should be case-sensitive of not.
Chris@0 174 *
Chris@0 175 * @return array|null
Chris@0 176 * An array of parameters. NULL when there is no match.
Chris@0 177 *
Chris@0 178 * @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection()
Chris@0 179 * @see \Drupal\Core\Routing\RouteProvider::getRoutesByPath()
Chris@0 180 */
Chris@0 181 protected function doMatchCollection($pathinfo, RouteCollection $routes, $case_sensitive) {
Chris@0 182 foreach ($routes as $name => $route) {
Chris@0 183 $compiledRoute = $route->compile();
Chris@0 184
Chris@0 185 // Set the regex to use UTF-8.
Chris@0 186 $regex = $compiledRoute->getRegex() . 'u';
Chris@0 187 if (!$case_sensitive) {
Chris@0 188 $regex = $regex . 'i';
Chris@0 189 }
Chris@0 190 if (!preg_match($regex, $pathinfo, $matches)) {
Chris@0 191 continue;
Chris@0 192 }
Chris@0 193
Chris@0 194 $hostMatches = [];
Chris@0 195 if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
Chris@0 196 $routes->remove($name);
Chris@0 197 continue;
Chris@0 198 }
Chris@0 199
Chris@0 200 // Check HTTP method requirement.
Chris@0 201 if ($requiredMethods = $route->getMethods()) {
Chris@0 202 // HEAD and GET are equivalent as per RFC.
Chris@0 203 if ('HEAD' === $method = $this->context->getMethod()) {
Chris@0 204 $method = 'GET';
Chris@0 205 }
Chris@0 206
Chris@0 207 if (!in_array($method, $requiredMethods)) {
Chris@0 208 $this->allow = array_merge($this->allow, $requiredMethods);
Chris@0 209 $routes->remove($name);
Chris@0 210 continue;
Chris@0 211 }
Chris@0 212 }
Chris@0 213
Chris@0 214 $status = $this->handleRouteRequirements($pathinfo, $name, $route);
Chris@0 215
Chris@0 216 if (self::ROUTE_MATCH === $status[0]) {
Chris@0 217 return $status[1];
Chris@0 218 }
Chris@0 219
Chris@0 220 if (self::REQUIREMENT_MISMATCH === $status[0]) {
Chris@0 221 $routes->remove($name);
Chris@0 222 continue;
Chris@0 223 }
Chris@0 224
Chris@0 225 return $this->getAttributes($route, $name, array_replace($matches, $hostMatches));
Chris@0 226 }
Chris@0 227 }
Chris@0 228
Chris@0 229 /**
Chris@0 230 * Returns a collection of potential matching routes for a request.
Chris@0 231 *
Chris@0 232 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@0 233 * The current request.
Chris@0 234 *
Chris@0 235 * @return \Symfony\Component\Routing\RouteCollection
Chris@0 236 * The initial fetched route collection.
Chris@0 237 */
Chris@0 238 protected function getInitialRouteCollection(Request $request) {
Chris@0 239 return $this->routeProvider->getRouteCollectionForRequest($request);
Chris@0 240 }
Chris@0 241
Chris@0 242 /**
Chris@0 243 * Apply the route enhancers to the defaults, according to priorities.
Chris@0 244 *
Chris@0 245 * @param array $defaults
Chris@0 246 * The defaults coming from the final matched route.
Chris@0 247 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@0 248 * The request.
Chris@0 249 *
Chris@0 250 * @return array
Chris@0 251 * The request attributes after applying the enhancers. This might consist
Chris@0 252 * raw values from the URL but also upcasted values, like entity objects,
Chris@0 253 * from route enhancers.
Chris@0 254 */
Chris@0 255 protected function applyRouteEnhancers($defaults, Request $request) {
Chris@14 256 foreach ($this->enhancers as $enhancer) {
Chris@14 257 if ($enhancer instanceof RouteEnhancerInterface && !$enhancer->applies($defaults[RouteObjectInterface::ROUTE_OBJECT])) {
Chris@14 258 continue;
Chris@14 259 }
Chris@0 260 $defaults = $enhancer->enhance($defaults, $request);
Chris@0 261 }
Chris@0 262
Chris@0 263 return $defaults;
Chris@0 264 }
Chris@0 265
Chris@0 266 /**
Chris@0 267 * Applies all route filters to a given route collection.
Chris@0 268 *
Chris@0 269 * This method reduces the sets of routes further down, for example by
Chris@0 270 * checking the HTTP method.
Chris@0 271 *
Chris@0 272 * @param \Symfony\Component\Routing\RouteCollection $collection
Chris@0 273 * The route collection.
Chris@0 274 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@0 275 * The request.
Chris@0 276 *
Chris@0 277 * @return \Symfony\Component\Routing\RouteCollection
Chris@0 278 * The filtered/sorted route collection.
Chris@0 279 */
Chris@0 280 protected function applyRouteFilters(RouteCollection $collection, Request $request) {
Chris@0 281 // Route filters are expected to throw an exception themselves if they
Chris@0 282 // end up filtering the list down to 0.
Chris@14 283 foreach ($this->filters as $filter) {
Chris@0 284 $collection = $filter->filter($collection, $request);
Chris@0 285 }
Chris@0 286
Chris@0 287 return $collection;
Chris@0 288 }
Chris@0 289
Chris@0 290 /**
Chris@17 291 * Reapplies the fit order to a RouteCollection object.
Chris@17 292 *
Chris@17 293 * Route filters can reorder route collections. For example, routes with an
Chris@17 294 * explicit _format requirement will be preferred. This can result in a less
Chris@17 295 * fit route being used. For example, as a result of filtering /user/% comes
Chris@17 296 * before /user/login. In order to not break this fundamental property of
Chris@17 297 * routes, we need to reapply the fit order. We also need to ensure that order
Chris@17 298 * within each group of the same fit is preserved.
Chris@17 299 *
Chris@17 300 * @param \Symfony\Component\Routing\RouteCollection $collection
Chris@17 301 * The route collection.
Chris@17 302 *
Chris@17 303 * @return \Symfony\Component\Routing\RouteCollection
Chris@17 304 * The reordered route collection.
Chris@17 305 */
Chris@17 306 protected function applyFitOrder(RouteCollection $collection) {
Chris@17 307 $buckets = [];
Chris@17 308 // Sort all the routes by fit descending.
Chris@17 309 foreach ($collection->all() as $name => $route) {
Chris@17 310 $fit = $route->compile()->getFit();
Chris@17 311 $buckets += [$fit => []];
Chris@17 312 $buckets[$fit][] = [$name, $route];
Chris@17 313 }
Chris@17 314 krsort($buckets);
Chris@17 315
Chris@17 316 $flattened = array_reduce($buckets, 'array_merge', []);
Chris@17 317
Chris@17 318 // Add them back onto a new route collection.
Chris@17 319 $collection = new RouteCollection();
Chris@17 320 foreach ($flattened as $pair) {
Chris@17 321 $name = $pair[0];
Chris@17 322 $route = $pair[1];
Chris@17 323 $collection->add($name, $route);
Chris@17 324 }
Chris@17 325 return $collection;
Chris@17 326 }
Chris@17 327
Chris@17 328 /**
Chris@0 329 * {@inheritdoc}
Chris@0 330 */
Chris@0 331 public function getRouteCollection() {
Chris@0 332 return new LazyRouteCollection($this->routeProvider);
Chris@0 333 }
Chris@0 334
Chris@0 335 /**
Chris@0 336 * {@inheritdoc}
Chris@0 337 */
Chris@0 338 public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH) {
Chris@0 339 @trigger_error('Use the \Drupal\Core\Url object instead', E_USER_DEPRECATED);
Chris@0 340 return $this->urlGenerator->generate($name, $parameters, $referenceType);
Chris@0 341 }
Chris@0 342
Chris@0 343 }