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 }
|