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