Router.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. <?php
  2. namespace Drupal\Core\Routing;
  3. use Drupal\Core\Path\CurrentPathStack;
  4. use Drupal\Core\Routing\Enhancer\RouteEnhancerInterface;
  5. use Symfony\Cmf\Component\Routing\LazyRouteCollection;
  6. use Symfony\Cmf\Component\Routing\RouteObjectInterface;
  7. use Symfony\Cmf\Component\Routing\RouteProviderInterface as BaseRouteProviderInterface;
  8. use Symfony\Component\HttpFoundation\Request;
  9. use Symfony\Component\Routing\Exception\MethodNotAllowedException;
  10. use Symfony\Component\Routing\Exception\ResourceNotFoundException;
  11. use Symfony\Component\Routing\Generator\UrlGeneratorInterface as BaseUrlGeneratorInterface;
  12. use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
  13. use Symfony\Component\Routing\RouteCollection;
  14. use Symfony\Component\Routing\RouterInterface;
  15. /**
  16. * Router implementation in Drupal.
  17. *
  18. * A router determines, for an incoming request, the active controller, which is
  19. * a callable that creates a response.
  20. *
  21. * It consists of several steps, of which each are explained in more details
  22. * below:
  23. * 1. Get a collection of routes which potentially match the current request.
  24. * This is done by the route provider. See ::getInitialRouteCollection().
  25. * 2. Filter the collection down further more. For example this filters out
  26. * routes applying to other formats: See ::applyRouteFilters()
  27. * 3. Find the best matching route out of the remaining ones, by applying a
  28. * regex. See ::matchCollection().
  29. * 4. Enhance the list of route attributes, for example loading entity objects.
  30. * See ::applyRouteEnhancers().
  31. *
  32. * This implementation uses ideas of the following routers:
  33. * - \Symfony\Cmf\Component\Routing\DynamicRouter
  34. * - \Drupal\Core\Routing\UrlMatcher
  35. * - \Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
  36. *
  37. * @see \Symfony\Cmf\Component\Routing\DynamicRouter
  38. * @see \Drupal\Core\Routing\UrlMatcher
  39. * @see \Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
  40. */
  41. class Router extends UrlMatcher implements RequestMatcherInterface, RouterInterface {
  42. /**
  43. * The route provider responsible for the first-pass match.
  44. *
  45. * @var \Symfony\Cmf\Component\Routing\RouteProviderInterface
  46. */
  47. protected $routeProvider;
  48. /**
  49. * The list of available enhancers.
  50. *
  51. * @var \Drupal\Core\Routing\EnhancerInterface[]
  52. */
  53. protected $enhancers = [];
  54. /**
  55. * The list of available route filters.
  56. *
  57. * @var \Drupal\Core\Routing\FilterInterface[]
  58. */
  59. protected $filters = [];
  60. /**
  61. * The URL generator.
  62. *
  63. * @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
  64. */
  65. protected $urlGenerator;
  66. /**
  67. * Constructs a new Router.
  68. *
  69. * @param \Symfony\Cmf\Component\Routing\RouteProviderInterface $route_provider
  70. * The route provider.
  71. * @param \Drupal\Core\Path\CurrentPathStack $current_path
  72. * The current path stack.
  73. * @param \Symfony\Component\Routing\Generator\UrlGeneratorInterface $url_generator
  74. * The URL generator.
  75. */
  76. public function __construct(BaseRouteProviderInterface $route_provider, CurrentPathStack $current_path, BaseUrlGeneratorInterface $url_generator) {
  77. parent::__construct($current_path);
  78. $this->routeProvider = $route_provider;
  79. $this->urlGenerator = $url_generator;
  80. }
  81. /**
  82. * Adds a route filter.
  83. *
  84. * @param \Drupal\Core\Routing\FilterInterface $route_filter
  85. * The route filter.
  86. */
  87. public function addRouteFilter(FilterInterface $route_filter) {
  88. $this->filters[] = $route_filter;
  89. }
  90. /**
  91. * Adds a route enhancer.
  92. *
  93. * @param \Drupal\Core\Routing\EnhancerInterface $route_enhancer
  94. * The route enhancer.
  95. */
  96. public function addRouteEnhancer(EnhancerInterface $route_enhancer) {
  97. $this->enhancers[] = $route_enhancer;
  98. }
  99. /**
  100. * {@inheritdoc}
  101. */
  102. public function match($pathinfo) {
  103. $request = Request::create($pathinfo);
  104. return $this->matchRequest($request);
  105. }
  106. /**
  107. * {@inheritdoc}
  108. */
  109. public function matchRequest(Request $request) {
  110. $collection = $this->getInitialRouteCollection($request);
  111. if ($collection->count() === 0) {
  112. throw new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
  113. }
  114. $collection = $this->applyRouteFilters($collection, $request);
  115. $collection = $this->applyFitOrder($collection);
  116. if ($ret = $this->matchCollection(rawurldecode($this->currentPath->getPath($request)), $collection)) {
  117. return $this->applyRouteEnhancers($ret, $request);
  118. }
  119. throw 0 < count($this->allow)
  120. ? new MethodNotAllowedException(array_unique($this->allow))
  121. : new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
  122. }
  123. /**
  124. * Tries to match a URL with a set of routes.
  125. *
  126. * @param string $pathinfo
  127. * The path info to be parsed
  128. * @param \Symfony\Component\Routing\RouteCollection $routes
  129. * The set of routes.
  130. *
  131. * @return array|null
  132. * An array of parameters. NULL when there is no match.
  133. */
  134. protected function matchCollection($pathinfo, RouteCollection $routes) {
  135. // Try a case-sensitive match.
  136. $match = $this->doMatchCollection($pathinfo, $routes, TRUE);
  137. // Try a case-insensitive match.
  138. if ($match === NULL && $routes->count() > 0) {
  139. $match = $this->doMatchCollection($pathinfo, $routes, FALSE);
  140. }
  141. return $match;
  142. }
  143. /**
  144. * Tries to match a URL with a set of routes.
  145. *
  146. * This code is very similar to Symfony's UrlMatcher::matchCollection() but it
  147. * supports case-insensitive matching. The static prefix optimization is
  148. * removed as this duplicates work done by the query in
  149. * RouteProvider::getRoutesByPath().
  150. *
  151. * @param string $pathinfo
  152. * The path info to be parsed
  153. * @param \Symfony\Component\Routing\RouteCollection $routes
  154. * The set of routes.
  155. * @param bool $case_sensitive
  156. * Determines if the match should be case-sensitive of not.
  157. *
  158. * @return array|null
  159. * An array of parameters. NULL when there is no match.
  160. *
  161. * @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection()
  162. * @see \Drupal\Core\Routing\RouteProvider::getRoutesByPath()
  163. */
  164. protected function doMatchCollection($pathinfo, RouteCollection $routes, $case_sensitive) {
  165. foreach ($routes as $name => $route) {
  166. $compiledRoute = $route->compile();
  167. // Set the regex to use UTF-8.
  168. $regex = $compiledRoute->getRegex() . 'u';
  169. if (!$case_sensitive) {
  170. $regex = $regex . 'i';
  171. }
  172. if (!preg_match($regex, $pathinfo, $matches)) {
  173. continue;
  174. }
  175. $hostMatches = [];
  176. if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
  177. $routes->remove($name);
  178. continue;
  179. }
  180. // Check HTTP method requirement.
  181. if ($requiredMethods = $route->getMethods()) {
  182. // HEAD and GET are equivalent as per RFC.
  183. if ('HEAD' === $method = $this->context->getMethod()) {
  184. $method = 'GET';
  185. }
  186. if (!in_array($method, $requiredMethods)) {
  187. $this->allow = array_merge($this->allow, $requiredMethods);
  188. $routes->remove($name);
  189. continue;
  190. }
  191. }
  192. $status = $this->handleRouteRequirements($pathinfo, $name, $route);
  193. if (self::ROUTE_MATCH === $status[0]) {
  194. return $status[1];
  195. }
  196. if (self::REQUIREMENT_MISMATCH === $status[0]) {
  197. $routes->remove($name);
  198. continue;
  199. }
  200. return $this->getAttributes($route, $name, array_replace($matches, $hostMatches));
  201. }
  202. }
  203. /**
  204. * Returns a collection of potential matching routes for a request.
  205. *
  206. * @param \Symfony\Component\HttpFoundation\Request $request
  207. * The current request.
  208. *
  209. * @return \Symfony\Component\Routing\RouteCollection
  210. * The initial fetched route collection.
  211. */
  212. protected function getInitialRouteCollection(Request $request) {
  213. return $this->routeProvider->getRouteCollectionForRequest($request);
  214. }
  215. /**
  216. * Apply the route enhancers to the defaults, according to priorities.
  217. *
  218. * @param array $defaults
  219. * The defaults coming from the final matched route.
  220. * @param \Symfony\Component\HttpFoundation\Request $request
  221. * The request.
  222. *
  223. * @return array
  224. * The request attributes after applying the enhancers. This might consist
  225. * raw values from the URL but also upcasted values, like entity objects,
  226. * from route enhancers.
  227. */
  228. protected function applyRouteEnhancers($defaults, Request $request) {
  229. foreach ($this->enhancers as $enhancer) {
  230. if ($enhancer instanceof RouteEnhancerInterface && !$enhancer->applies($defaults[RouteObjectInterface::ROUTE_OBJECT])) {
  231. continue;
  232. }
  233. $defaults = $enhancer->enhance($defaults, $request);
  234. }
  235. return $defaults;
  236. }
  237. /**
  238. * Applies all route filters to a given route collection.
  239. *
  240. * This method reduces the sets of routes further down, for example by
  241. * checking the HTTP method.
  242. *
  243. * @param \Symfony\Component\Routing\RouteCollection $collection
  244. * The route collection.
  245. * @param \Symfony\Component\HttpFoundation\Request $request
  246. * The request.
  247. *
  248. * @return \Symfony\Component\Routing\RouteCollection
  249. * The filtered/sorted route collection.
  250. */
  251. protected function applyRouteFilters(RouteCollection $collection, Request $request) {
  252. // Route filters are expected to throw an exception themselves if they
  253. // end up filtering the list down to 0.
  254. foreach ($this->filters as $filter) {
  255. $collection = $filter->filter($collection, $request);
  256. }
  257. return $collection;
  258. }
  259. /**
  260. * Reapplies the fit order to a RouteCollection object.
  261. *
  262. * Route filters can reorder route collections. For example, routes with an
  263. * explicit _format requirement will be preferred. This can result in a less
  264. * fit route being used. For example, as a result of filtering /user/% comes
  265. * before /user/login. In order to not break this fundamental property of
  266. * routes, we need to reapply the fit order. We also need to ensure that order
  267. * within each group of the same fit is preserved.
  268. *
  269. * @param \Symfony\Component\Routing\RouteCollection $collection
  270. * The route collection.
  271. *
  272. * @return \Symfony\Component\Routing\RouteCollection
  273. * The reordered route collection.
  274. */
  275. protected function applyFitOrder(RouteCollection $collection) {
  276. $buckets = [];
  277. // Sort all the routes by fit descending.
  278. foreach ($collection->all() as $name => $route) {
  279. $fit = $route->compile()->getFit();
  280. $buckets += [$fit => []];
  281. $buckets[$fit][] = [$name, $route];
  282. }
  283. krsort($buckets);
  284. $flattened = array_reduce($buckets, 'array_merge', []);
  285. // Add them back onto a new route collection.
  286. $collection = new RouteCollection();
  287. foreach ($flattened as $pair) {
  288. $name = $pair[0];
  289. $route = $pair[1];
  290. $collection->add($name, $route);
  291. }
  292. return $collection;
  293. }
  294. /**
  295. * {@inheritdoc}
  296. */
  297. public function getRouteCollection() {
  298. return new LazyRouteCollection($this->routeProvider);
  299. }
  300. /**
  301. * {@inheritdoc}
  302. */
  303. public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH) {
  304. @trigger_error('Use the \Drupal\Core\Url object instead', E_USER_DEPRECATED);
  305. return $this->urlGenerator->generate($name, $parameters, $referenceType);
  306. }
  307. }