3 namespace Drupal\Core\Routing;
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;
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 \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface[]
58 protected $enhancers = [];
61 * Cached sorted list of enhancers.
63 * @var \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface[]
65 protected $sortedEnhancers;
68 * The list of available route filters.
70 * @var \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface[]
72 protected $filters = [];
75 * Cached sorted list route filters.
77 * @var \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface[]
79 protected $sortedFilters;
84 * @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
86 protected $urlGenerator;
89 * Constructs a new Router.
91 * @param \Symfony\Cmf\Component\Routing\RouteProviderInterface $route_provider
93 * @param \Drupal\Core\Path\CurrentPathStack $current_path
94 * The current path stack.
95 * @param \Symfony\Component\Routing\Generator\UrlGeneratorInterface $url_generator
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;
105 * Adds a route enhancer to the list of used route enhancers.
107 * @param \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface $route_enhancer
109 * @param int $priority
110 * (optional) The priority of the enhancer. Higher number enhancers will be
115 public function addRouteEnhancer(BaseRouteEnhancerInterface $route_enhancer, $priority = 0) {
116 $this->enhancers[$priority][] = $route_enhancer;
121 * Adds a route filter to the list of used route filters.
123 * @param \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface $route_filter
125 * @param int $priority
126 * (optional) The priority of the filter. Higher number filters will be used
131 public function addRouteFilter(BaseRouteFilterInterface $route_filter, $priority = 0) {
132 $this->filters[$priority][] = $route_filter;
140 public function match($pathinfo) {
141 $request = Request::create($pathinfo);
143 return $this->matchRequest($request);
149 public function matchRequest(Request $request) {
150 $collection = $this->getInitialRouteCollection($request);
151 $collection = $this->applyRouteFilters($collection, $request);
153 if ($ret = $this->matchCollection(rawurldecode($this->currentPath->getPath($request)), $collection)) {
154 return $this->applyRouteEnhancers($ret, $request);
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()));
163 * Tries to match a URL with a set of routes.
165 * @param string $pathinfo
166 * The path info to be parsed
167 * @param \Symfony\Component\Routing\RouteCollection $routes
171 * An array of parameters. NULL when there is no match.
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);
184 * Tries to match a URL with a set of routes.
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().
191 * @param string $pathinfo
192 * The path info to be parsed
193 * @param \Symfony\Component\Routing\RouteCollection $routes
195 * @param bool $case_sensitive
196 * Determines if the match should be case-sensitive of not.
199 * An array of parameters. NULL when there is no match.
201 * @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection()
202 * @see \Drupal\Core\Routing\RouteProvider::getRoutesByPath()
204 protected function doMatchCollection($pathinfo, RouteCollection $routes, $case_sensitive) {
205 foreach ($routes as $name => $route) {
206 $compiledRoute = $route->compile();
208 // Set the regex to use UTF-8.
209 $regex = $compiledRoute->getRegex() . 'u';
210 if (!$case_sensitive) {
211 $regex = $regex . 'i';
213 if (!preg_match($regex, $pathinfo, $matches)) {
218 if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
219 $routes->remove($name);
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()) {
230 if (!in_array($method, $requiredMethods)) {
231 $this->allow = array_merge($this->allow, $requiredMethods);
232 $routes->remove($name);
237 $status = $this->handleRouteRequirements($pathinfo, $name, $route);
239 if (self::ROUTE_MATCH === $status[0]) {
243 if (self::REQUIREMENT_MISMATCH === $status[0]) {
244 $routes->remove($name);
248 return $this->getAttributes($route, $name, array_replace($matches, $hostMatches));
253 * Returns a collection of potential matching routes for a request.
255 * @param \Symfony\Component\HttpFoundation\Request $request
256 * The current request.
258 * @return \Symfony\Component\Routing\RouteCollection
259 * The initial fetched route collection.
261 protected function getInitialRouteCollection(Request $request) {
262 return $this->routeProvider->getRouteCollectionForRequest($request);
266 * Apply the route enhancers to the defaults, according to priorities.
268 * @param array $defaults
269 * The defaults coming from the final matched route.
270 * @param \Symfony\Component\HttpFoundation\Request $request
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.
278 protected function applyRouteEnhancers($defaults, Request $request) {
279 foreach ($this->getRouteEnhancers() as $enhancer) {
280 $defaults = $enhancer->enhance($defaults, $request);
287 * Sorts the enhancers and flattens them.
289 * @return \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface[]
290 * The enhancers ordered by priority.
292 public function getRouteEnhancers() {
293 if (!isset($this->sortedEnhancers)) {
294 $this->sortedEnhancers = $this->sortRouteEnhancers();
297 return $this->sortedEnhancers;
301 * Sort enhancers by priority.
303 * The highest priority number is the highest priority (reverse sorting).
305 * @return \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface[]
306 * The sorted enhancers.
308 protected function sortRouteEnhancers() {
309 $sortedEnhancers = [];
310 krsort($this->enhancers);
312 foreach ($this->enhancers as $enhancers) {
313 $sortedEnhancers = array_merge($sortedEnhancers, $enhancers);
316 return $sortedEnhancers;
320 * Applies all route filters to a given route collection.
322 * This method reduces the sets of routes further down, for example by
323 * checking the HTTP method.
325 * @param \Symfony\Component\Routing\RouteCollection $collection
326 * The route collection.
327 * @param \Symfony\Component\HttpFoundation\Request $request
330 * @return \Symfony\Component\Routing\RouteCollection
331 * The filtered/sorted route collection.
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);
344 * Sorts the filters and flattens them.
346 * @return \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface[]
347 * The filters ordered by priority
349 public function getRouteFilters() {
350 if (!isset($this->sortedFilters)) {
351 $this->sortedFilters = $this->sortFilters();
354 return $this->sortedFilters;
358 * Sort filters by priority.
360 * The highest priority number is the highest priority (reverse sorting).
362 * @return \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface[]
363 * The sorted filters.
365 protected function sortFilters() {
367 krsort($this->filters);
369 foreach ($this->filters as $filters) {
370 $sortedFilters = array_merge($sortedFilters, $filters);
373 return $sortedFilters;
379 public function getRouteCollection() {
380 return new LazyRouteCollection($this->routeProvider);
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);