Newer
Older
<?php
namespace Drupal\Core\Routing;
use Drupal\Core\Path\CurrentPathStack;
use Drupal\Core\Routing\Enhancer\RouteEnhancerInterface;
use Symfony\Cmf\Component\Routing\LazyRouteCollection;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
use Symfony\Cmf\Component\Routing\RouteProviderInterface as BaseRouteProviderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface as BaseUrlGeneratorInterface;
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\RouterInterface;
/**
* Router implementation in Drupal.
*
* A router determines, for an incoming request, the active controller, which is
* a callable that creates a response.
*
* It consists of several steps, of which each are explained in more details
* below:
* 1. Get a collection of routes which potentially match the current request.
* This is done by the route provider. See ::getInitialRouteCollection().
* 2. Filter the collection down further more. For example this filters out
* routes applying to other formats: See ::applyRouteFilters()
* 3. Find the best matching route out of the remaining ones, by applying a
* regex. See ::matchCollection().
* 4. Enhance the list of route attributes, for example loading entity objects.
* See ::applyRouteEnhancers().
*
* This implementation uses ideas of the following routers:
* - \Symfony\Cmf\Component\Routing\DynamicRouter
* - \Drupal\Core\Routing\UrlMatcher
* - \Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
*
* @see \Symfony\Cmf\Component\Routing\DynamicRouter
* @see \Drupal\Core\Routing\UrlMatcher
* @see \Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
*/
class Router extends UrlMatcher implements RequestMatcherInterface, RouterInterface {
/**
* The route provider responsible for the first-pass match.
*
* @var \Symfony\Cmf\Component\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* The list of available enhancers.
*
* @var \Drupal\Core\Routing\EnhancerInterface[]
*/
protected $enhancers = [];
/**
* The list of available route filters.
*
* @var \Drupal\Core\Routing\FilterInterface[]
*/
protected $filters = [];
/**
* The URL generator.
*
* @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
*/
protected $urlGenerator;
/**
* Constructs a new Router.
*
* @param \Symfony\Cmf\Component\Routing\RouteProviderInterface $route_provider
* The route provider.
* @param \Drupal\Core\Path\CurrentPathStack $current_path
* The current path stack.
* @param \Symfony\Component\Routing\Generator\UrlGeneratorInterface $url_generator
* The URL generator.
*/
public function __construct(BaseRouteProviderInterface $route_provider, CurrentPathStack $current_path, BaseUrlGeneratorInterface $url_generator) {
parent::__construct($current_path);
$this->routeProvider = $route_provider;
$this->urlGenerator = $url_generator;
}
/**
* Adds a route filter.
*
* @param \Drupal\Core\Routing\FilterInterface $route_filter
* The route filter.
*/
public function addRouteFilter(FilterInterface $route_filter) {
$this->filters[] = $route_filter;
}
/**
* Adds a deprecated route filter.
*
* @param \Drupal\Core\Routing\FilterInterface $route_filter
* The route filter.
*/
public function addDeprecatedRouteFilter(FilterInterface $route_filter) {
@trigger_error('non_lazy_route_filter is deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. Instead, should use route_filter, see https://www.drupal.org/node/2894934', E_USER_DEPRECATED);
$this->filters[] = $route_filter;
}
/**
* Adds a route enhancer.
*
* @param \Drupal\Core\Routing\EnhancerInterface $route_enhancer
* The route enhancer.
*/
public function addRouteEnhancer(EnhancerInterface $route_enhancer) {
$this->enhancers[] = $route_enhancer;
}
/**
* Adds a deprecated route enhancer.
*
* @param \Drupal\Core\Routing\EnhancerInterface $route_enhancer
* The route enhancer.
*/
public function addDeprecatedRouteEnhancer(EnhancerInterface $route_enhancer) {
@trigger_error('non_lazy_route_enhancer is deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. Instead, should use route_enhancer, see https://www.drupal.org/node/2894934', E_USER_DEPRECATED);
$this->enhancers[] = $route_enhancer;
}
/**
* {@inheritdoc}
*/
public function match($pathinfo) {
$request = Request::create($pathinfo);
return $this->matchRequest($request);
}
/**
* {@inheritdoc}
*/
public function matchRequest(Request $request) {
$collection = $this->getInitialRouteCollection($request);
if ($collection->count() === 0) {
throw new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
}
$collection = $this->applyRouteFilters($collection, $request);
if ($ret = $this->matchCollection(rawurldecode($this->currentPath->getPath($request)), $collection)) {
return $this->applyRouteEnhancers($ret, $request);
}
throw 0 < count($this->allow)
? new MethodNotAllowedException(array_unique($this->allow))
: new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
}
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
/**
* Tries to match a URL with a set of routes.
*
* @param string $pathinfo
* The path info to be parsed
* @param \Symfony\Component\Routing\RouteCollection $routes
* The set of routes.
*
* @return array|null
* An array of parameters. NULL when there is no match.
*/
protected function matchCollection($pathinfo, RouteCollection $routes) {
// Try a case-sensitive match.
$match = $this->doMatchCollection($pathinfo, $routes, TRUE);
// Try a case-insensitive match.
if ($match === NULL && $routes->count() > 0) {
$match = $this->doMatchCollection($pathinfo, $routes, FALSE);
}
return $match;
}
/**
* Tries to match a URL with a set of routes.
*
* This code is very similar to Symfony's UrlMatcher::matchCollection() but it
* supports case-insensitive matching. The static prefix optimization is
* removed as this duplicates work done by the query in
* RouteProvider::getRoutesByPath().
*
* @param string $pathinfo
* The path info to be parsed
* @param \Symfony\Component\Routing\RouteCollection $routes
* The set of routes.
* @param bool $case_sensitive
* Determines if the match should be case-sensitive of not.
*
* @return array|null
* An array of parameters. NULL when there is no match.
*
* @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection()
* @see \Drupal\Core\Routing\RouteProvider::getRoutesByPath()
*/
protected function doMatchCollection($pathinfo, RouteCollection $routes, $case_sensitive) {
foreach ($routes as $name => $route) {
$compiledRoute = $route->compile();
// Set the regex to use UTF-8.
$regex = $compiledRoute->getRegex() . 'u';
if (!$case_sensitive) {
$regex = $regex . 'i';
}
if (!preg_match($regex, $pathinfo, $matches)) {
continue;
}
$hostMatches = [];
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
$routes->remove($name);
continue;
}
// Check HTTP method requirement.
if ($requiredMethods = $route->getMethods()) {
// HEAD and GET are equivalent as per RFC.
if ('HEAD' === $method = $this->context->getMethod()) {
$method = 'GET';
}
if (!in_array($method, $requiredMethods)) {
$this->allow = array_merge($this->allow, $requiredMethods);
$routes->remove($name);
continue;
}
}
$status = $this->handleRouteRequirements($pathinfo, $name, $route);
if (self::ROUTE_MATCH === $status[0]) {
return $status[1];
}
if (self::REQUIREMENT_MISMATCH === $status[0]) {
$routes->remove($name);
continue;
}
return $this->getAttributes($route, $name, array_replace($matches, $hostMatches));
}
}
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
/**
* Returns a collection of potential matching routes for a request.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return \Symfony\Component\Routing\RouteCollection
* The initial fetched route collection.
*/
protected function getInitialRouteCollection(Request $request) {
return $this->routeProvider->getRouteCollectionForRequest($request);
}
/**
* Apply the route enhancers to the defaults, according to priorities.
*
* @param array $defaults
* The defaults coming from the final matched route.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return array
* The request attributes after applying the enhancers. This might consist
* raw values from the URL but also upcasted values, like entity objects,
* from route enhancers.
*/
protected function applyRouteEnhancers($defaults, Request $request) {
foreach ($this->enhancers as $enhancer) {
if ($enhancer instanceof RouteEnhancerInterface && !$enhancer->applies($defaults[RouteObjectInterface::ROUTE_OBJECT])) {
continue;
}
$defaults = $enhancer->enhance($defaults, $request);
}
return $defaults;
}
/**
* Applies all route filters to a given route collection.
*
* This method reduces the sets of routes further down, for example by
* checking the HTTP method.
*
* @param \Symfony\Component\Routing\RouteCollection $collection
* The route collection.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return \Symfony\Component\Routing\RouteCollection
* The filtered/sorted route collection.
*/
protected function applyRouteFilters(RouteCollection $collection, Request $request) {
// Route filters are expected to throw an exception themselves if they
// end up filtering the list down to 0.
foreach ($this->filters as $filter) {
$collection = $filter->filter($collection, $request);
}
return $collection;
}
/**
* {@inheritdoc}
*/
public function getRouteCollection() {
return new LazyRouteCollection($this->routeProvider);
}
/**
* {@inheritdoc}
*/
public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH) {