3 namespace Drupal\Core\Routing;
5 use Drupal\Core\Path\CurrentPathStack;
6 use Drupal\Core\Routing\Enhancer\RouteEnhancerInterface;
7 use Symfony\Cmf\Component\Routing\LazyRouteCollection;
8 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
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;
19 * Router implementation in Drupal.
21 * A router determines, for an incoming request, the active controller, which is
22 * a callable that creates a response.
24 * It consists of several steps, of which each are explained in more details
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().
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
40 * @see \Symfony\Cmf\Component\Routing\DynamicRouter
41 * @see \Drupal\Core\Routing\UrlMatcher
42 * @see \Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
44 class Router extends UrlMatcher implements RequestMatcherInterface, RouterInterface {
47 * The route provider responsible for the first-pass match.
49 * @var \Symfony\Cmf\Component\Routing\RouteProviderInterface
51 protected $routeProvider;
54 * The list of available enhancers.
56 * @var \Drupal\Core\Routing\EnhancerInterface[]
58 protected $enhancers = [];
61 * The list of available route filters.
63 * @var \Drupal\Core\Routing\FilterInterface[]
65 protected $filters = [];
70 * @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
72 protected $urlGenerator;
75 * Constructs a new Router.
77 * @param \Symfony\Cmf\Component\Routing\RouteProviderInterface $route_provider
79 * @param \Drupal\Core\Path\CurrentPathStack $current_path
80 * The current path stack.
81 * @param \Symfony\Component\Routing\Generator\UrlGeneratorInterface $url_generator
84 public function __construct(BaseRouteProviderInterface $route_provider, CurrentPathStack $current_path, BaseUrlGeneratorInterface $url_generator) {
85 parent::__construct($current_path);
86 $this->routeProvider = $route_provider;
87 $this->urlGenerator = $url_generator;
91 * Adds a route filter.
93 * @param \Drupal\Core\Routing\FilterInterface $route_filter
96 public function addRouteFilter(FilterInterface $route_filter) {
97 $this->filters[] = $route_filter;
101 * Adds a route enhancer.
103 * @param \Drupal\Core\Routing\EnhancerInterface $route_enhancer
104 * The route enhancer.
106 public function addRouteEnhancer(EnhancerInterface $route_enhancer) {
107 $this->enhancers[] = $route_enhancer;
113 public function match($pathinfo) {
114 $request = Request::create($pathinfo);
116 return $this->matchRequest($request);
122 public function matchRequest(Request $request) {
123 $collection = $this->getInitialRouteCollection($request);
124 if ($collection->count() === 0) {
125 throw new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
127 $collection = $this->applyRouteFilters($collection, $request);
128 $collection = $this->applyFitOrder($collection);
130 if ($ret = $this->matchCollection(rawurldecode($this->currentPath->getPath($request)), $collection)) {
131 return $this->applyRouteEnhancers($ret, $request);
134 throw 0 < count($this->allow)
135 ? new MethodNotAllowedException(array_unique($this->allow))
136 : new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
140 * Tries to match a URL with a set of routes.
142 * @param string $pathinfo
143 * The path info to be parsed
144 * @param \Symfony\Component\Routing\RouteCollection $routes
148 * An array of parameters. NULL when there is no match.
150 protected function matchCollection($pathinfo, RouteCollection $routes) {
151 // Try a case-sensitive match.
152 $match = $this->doMatchCollection($pathinfo, $routes, TRUE);
153 // Try a case-insensitive match.
154 if ($match === NULL && $routes->count() > 0) {
155 $match = $this->doMatchCollection($pathinfo, $routes, FALSE);
161 * Tries to match a URL with a set of routes.
163 * This code is very similar to Symfony's UrlMatcher::matchCollection() but it
164 * supports case-insensitive matching. The static prefix optimization is
165 * removed as this duplicates work done by the query in
166 * RouteProvider::getRoutesByPath().
168 * @param string $pathinfo
169 * The path info to be parsed
170 * @param \Symfony\Component\Routing\RouteCollection $routes
172 * @param bool $case_sensitive
173 * Determines if the match should be case-sensitive of not.
176 * An array of parameters. NULL when there is no match.
178 * @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection()
179 * @see \Drupal\Core\Routing\RouteProvider::getRoutesByPath()
181 protected function doMatchCollection($pathinfo, RouteCollection $routes, $case_sensitive) {
182 foreach ($routes as $name => $route) {
183 $compiledRoute = $route->compile();
185 // Set the regex to use UTF-8.
186 $regex = $compiledRoute->getRegex() . 'u';
187 if (!$case_sensitive) {
188 $regex = $regex . 'i';
190 if (!preg_match($regex, $pathinfo, $matches)) {
195 if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
196 $routes->remove($name);
200 // Check HTTP method requirement.
201 if ($requiredMethods = $route->getMethods()) {
202 // HEAD and GET are equivalent as per RFC.
203 if ('HEAD' === $method = $this->context->getMethod()) {
207 if (!in_array($method, $requiredMethods)) {
208 $this->allow = array_merge($this->allow, $requiredMethods);
209 $routes->remove($name);
214 $status = $this->handleRouteRequirements($pathinfo, $name, $route);
216 if (self::ROUTE_MATCH === $status[0]) {
220 if (self::REQUIREMENT_MISMATCH === $status[0]) {
221 $routes->remove($name);
225 return $this->getAttributes($route, $name, array_replace($matches, $hostMatches));
230 * Returns a collection of potential matching routes for a request.
232 * @param \Symfony\Component\HttpFoundation\Request $request
233 * The current request.
235 * @return \Symfony\Component\Routing\RouteCollection
236 * The initial fetched route collection.
238 protected function getInitialRouteCollection(Request $request) {
239 return $this->routeProvider->getRouteCollectionForRequest($request);
243 * Apply the route enhancers to the defaults, according to priorities.
245 * @param array $defaults
246 * The defaults coming from the final matched route.
247 * @param \Symfony\Component\HttpFoundation\Request $request
251 * The request attributes after applying the enhancers. This might consist
252 * raw values from the URL but also upcasted values, like entity objects,
253 * from route enhancers.
255 protected function applyRouteEnhancers($defaults, Request $request) {
256 foreach ($this->enhancers as $enhancer) {
257 if ($enhancer instanceof RouteEnhancerInterface && !$enhancer->applies($defaults[RouteObjectInterface::ROUTE_OBJECT])) {
260 $defaults = $enhancer->enhance($defaults, $request);
267 * Applies all route filters to a given route collection.
269 * This method reduces the sets of routes further down, for example by
270 * checking the HTTP method.
272 * @param \Symfony\Component\Routing\RouteCollection $collection
273 * The route collection.
274 * @param \Symfony\Component\HttpFoundation\Request $request
277 * @return \Symfony\Component\Routing\RouteCollection
278 * The filtered/sorted route collection.
280 protected function applyRouteFilters(RouteCollection $collection, Request $request) {
281 // Route filters are expected to throw an exception themselves if they
282 // end up filtering the list down to 0.
283 foreach ($this->filters as $filter) {
284 $collection = $filter->filter($collection, $request);
291 * Reapplies the fit order to a RouteCollection object.
293 * Route filters can reorder route collections. For example, routes with an
294 * explicit _format requirement will be preferred. This can result in a less
295 * fit route being used. For example, as a result of filtering /user/% comes
296 * before /user/login. In order to not break this fundamental property of
297 * routes, we need to reapply the fit order. We also need to ensure that order
298 * within each group of the same fit is preserved.
300 * @param \Symfony\Component\Routing\RouteCollection $collection
301 * The route collection.
303 * @return \Symfony\Component\Routing\RouteCollection
304 * The reordered route collection.
306 protected function applyFitOrder(RouteCollection $collection) {
308 // Sort all the routes by fit descending.
309 foreach ($collection->all() as $name => $route) {
310 $fit = $route->compile()->getFit();
311 $buckets += [$fit => []];
312 $buckets[$fit][] = [$name, $route];
316 $flattened = array_reduce($buckets, 'array_merge', []);
318 // Add them back onto a new route collection.
319 $collection = new RouteCollection();
320 foreach ($flattened as $pair) {
323 $collection->add($name, $route);
331 public function getRouteCollection() {
332 return new LazyRouteCollection($this->routeProvider);
338 public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH) {
339 @trigger_error('Use the \Drupal\Core\Url object instead', E_USER_DEPRECATED);
340 return $this->urlGenerator->generate($name, $parameters, $referenceType);