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