annotate core/lib/Drupal/Core/Routing/Router.php @ 14:1fec387a4317

Update Drupal core to 8.5.2 via Composer
author Chris Cannam
date Mon, 23 Apr 2018 09:46:53 +0100
parents 4c8ae668cc8c
children 129ea1e6d783
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@0 128
Chris@0 129 if ($ret = $this->matchCollection(rawurldecode($this->currentPath->getPath($request)), $collection)) {
Chris@0 130 return $this->applyRouteEnhancers($ret, $request);
Chris@0 131 }
Chris@0 132
Chris@0 133 throw 0 < count($this->allow)
Chris@0 134 ? new MethodNotAllowedException(array_unique($this->allow))
Chris@0 135 : new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
Chris@0 136 }
Chris@0 137
Chris@0 138 /**
Chris@0 139 * Tries to match a URL with a set of routes.
Chris@0 140 *
Chris@0 141 * @param string $pathinfo
Chris@0 142 * The path info to be parsed
Chris@0 143 * @param \Symfony\Component\Routing\RouteCollection $routes
Chris@0 144 * The set of routes.
Chris@0 145 *
Chris@0 146 * @return array|null
Chris@0 147 * An array of parameters. NULL when there is no match.
Chris@0 148 */
Chris@0 149 protected function matchCollection($pathinfo, RouteCollection $routes) {
Chris@0 150 // Try a case-sensitive match.
Chris@0 151 $match = $this->doMatchCollection($pathinfo, $routes, TRUE);
Chris@0 152 // Try a case-insensitive match.
Chris@0 153 if ($match === NULL && $routes->count() > 0) {
Chris@0 154 $match = $this->doMatchCollection($pathinfo, $routes, FALSE);
Chris@0 155 }
Chris@0 156 return $match;
Chris@0 157 }
Chris@0 158
Chris@0 159 /**
Chris@0 160 * Tries to match a URL with a set of routes.
Chris@0 161 *
Chris@0 162 * This code is very similar to Symfony's UrlMatcher::matchCollection() but it
Chris@0 163 * supports case-insensitive matching. The static prefix optimization is
Chris@0 164 * removed as this duplicates work done by the query in
Chris@0 165 * RouteProvider::getRoutesByPath().
Chris@0 166 *
Chris@0 167 * @param string $pathinfo
Chris@0 168 * The path info to be parsed
Chris@0 169 * @param \Symfony\Component\Routing\RouteCollection $routes
Chris@0 170 * The set of routes.
Chris@0 171 * @param bool $case_sensitive
Chris@0 172 * Determines if the match should be case-sensitive of not.
Chris@0 173 *
Chris@0 174 * @return array|null
Chris@0 175 * An array of parameters. NULL when there is no match.
Chris@0 176 *
Chris@0 177 * @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection()
Chris@0 178 * @see \Drupal\Core\Routing\RouteProvider::getRoutesByPath()
Chris@0 179 */
Chris@0 180 protected function doMatchCollection($pathinfo, RouteCollection $routes, $case_sensitive) {
Chris@0 181 foreach ($routes as $name => $route) {
Chris@0 182 $compiledRoute = $route->compile();
Chris@0 183
Chris@0 184 // Set the regex to use UTF-8.
Chris@0 185 $regex = $compiledRoute->getRegex() . 'u';
Chris@0 186 if (!$case_sensitive) {
Chris@0 187 $regex = $regex . 'i';
Chris@0 188 }
Chris@0 189 if (!preg_match($regex, $pathinfo, $matches)) {
Chris@0 190 continue;
Chris@0 191 }
Chris@0 192
Chris@0 193 $hostMatches = [];
Chris@0 194 if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
Chris@0 195 $routes->remove($name);
Chris@0 196 continue;
Chris@0 197 }
Chris@0 198
Chris@0 199 // Check HTTP method requirement.
Chris@0 200 if ($requiredMethods = $route->getMethods()) {
Chris@0 201 // HEAD and GET are equivalent as per RFC.
Chris@0 202 if ('HEAD' === $method = $this->context->getMethod()) {
Chris@0 203 $method = 'GET';
Chris@0 204 }
Chris@0 205
Chris@0 206 if (!in_array($method, $requiredMethods)) {
Chris@0 207 $this->allow = array_merge($this->allow, $requiredMethods);
Chris@0 208 $routes->remove($name);
Chris@0 209 continue;
Chris@0 210 }
Chris@0 211 }
Chris@0 212
Chris@0 213 $status = $this->handleRouteRequirements($pathinfo, $name, $route);
Chris@0 214
Chris@0 215 if (self::ROUTE_MATCH === $status[0]) {
Chris@0 216 return $status[1];
Chris@0 217 }
Chris@0 218
Chris@0 219 if (self::REQUIREMENT_MISMATCH === $status[0]) {
Chris@0 220 $routes->remove($name);
Chris@0 221 continue;
Chris@0 222 }
Chris@0 223
Chris@0 224 return $this->getAttributes($route, $name, array_replace($matches, $hostMatches));
Chris@0 225 }
Chris@0 226 }
Chris@0 227
Chris@0 228 /**
Chris@0 229 * Returns a collection of potential matching routes for a request.
Chris@0 230 *
Chris@0 231 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@0 232 * The current request.
Chris@0 233 *
Chris@0 234 * @return \Symfony\Component\Routing\RouteCollection
Chris@0 235 * The initial fetched route collection.
Chris@0 236 */
Chris@0 237 protected function getInitialRouteCollection(Request $request) {
Chris@0 238 return $this->routeProvider->getRouteCollectionForRequest($request);
Chris@0 239 }
Chris@0 240
Chris@0 241 /**
Chris@0 242 * Apply the route enhancers to the defaults, according to priorities.
Chris@0 243 *
Chris@0 244 * @param array $defaults
Chris@0 245 * The defaults coming from the final matched route.
Chris@0 246 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@0 247 * The request.
Chris@0 248 *
Chris@0 249 * @return array
Chris@0 250 * The request attributes after applying the enhancers. This might consist
Chris@0 251 * raw values from the URL but also upcasted values, like entity objects,
Chris@0 252 * from route enhancers.
Chris@0 253 */
Chris@0 254 protected function applyRouteEnhancers($defaults, Request $request) {
Chris@14 255 foreach ($this->enhancers as $enhancer) {
Chris@14 256 if ($enhancer instanceof RouteEnhancerInterface && !$enhancer->applies($defaults[RouteObjectInterface::ROUTE_OBJECT])) {
Chris@14 257 continue;
Chris@14 258 }
Chris@0 259 $defaults = $enhancer->enhance($defaults, $request);
Chris@0 260 }
Chris@0 261
Chris@0 262 return $defaults;
Chris@0 263 }
Chris@0 264
Chris@0 265 /**
Chris@0 266 * Applies all route filters to a given route collection.
Chris@0 267 *
Chris@0 268 * This method reduces the sets of routes further down, for example by
Chris@0 269 * checking the HTTP method.
Chris@0 270 *
Chris@0 271 * @param \Symfony\Component\Routing\RouteCollection $collection
Chris@0 272 * The route collection.
Chris@0 273 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@0 274 * The request.
Chris@0 275 *
Chris@0 276 * @return \Symfony\Component\Routing\RouteCollection
Chris@0 277 * The filtered/sorted route collection.
Chris@0 278 */
Chris@0 279 protected function applyRouteFilters(RouteCollection $collection, Request $request) {
Chris@0 280 // Route filters are expected to throw an exception themselves if they
Chris@0 281 // end up filtering the list down to 0.
Chris@14 282 foreach ($this->filters as $filter) {
Chris@0 283 $collection = $filter->filter($collection, $request);
Chris@0 284 }
Chris@0 285
Chris@0 286 return $collection;
Chris@0 287 }
Chris@0 288
Chris@0 289 /**
Chris@0 290 * {@inheritdoc}
Chris@0 291 */
Chris@0 292 public function getRouteCollection() {
Chris@0 293 return new LazyRouteCollection($this->routeProvider);
Chris@0 294 }
Chris@0 295
Chris@0 296 /**
Chris@0 297 * {@inheritdoc}
Chris@0 298 */
Chris@0 299 public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH) {
Chris@0 300 @trigger_error('Use the \Drupal\Core\Url object instead', E_USER_DEPRECATED);
Chris@0 301 return $this->urlGenerator->generate($name, $parameters, $referenceType);
Chris@0 302 }
Chris@0 303
Chris@0 304 }