Router.php 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  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. if ($ret = $this->matchCollection(rawurldecode($this->currentPath->getPath($request)), $collection)) {
  116. return $this->applyRouteEnhancers($ret, $request);
  117. }
  118. throw 0 < count($this->allow)
  119. ? new MethodNotAllowedException(array_unique($this->allow))
  120. : new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
  121. }
  122. /**
  123. * Tries to match a URL with a set of routes.
  124. *
  125. * @param string $pathinfo
  126. * The path info to be parsed
  127. * @param \Symfony\Component\Routing\RouteCollection $routes
  128. * The set of routes.
  129. *
  130. * @return array|null
  131. * An array of parameters. NULL when there is no match.
  132. */
  133. protected function matchCollection($pathinfo, RouteCollection $routes) {
  134. // Try a case-sensitive match.
  135. $match = $this->doMatchCollection($pathinfo, $routes, TRUE);
  136. // Try a case-insensitive match.
  137. if ($match === NULL && $routes->count() > 0) {
  138. $match = $this->doMatchCollection($pathinfo, $routes, FALSE);
  139. }
  140. return $match;
  141. }
  142. /**
  143. * Tries to match a URL with a set of routes.
  144. *
  145. * This code is very similar to Symfony's UrlMatcher::matchCollection() but it
  146. * supports case-insensitive matching. The static prefix optimization is
  147. * removed as this duplicates work done by the query in
  148. * RouteProvider::getRoutesByPath().
  149. *
  150. * @param string $pathinfo
  151. * The path info to be parsed
  152. * @param \Symfony\Component\Routing\RouteCollection $routes
  153. * The set of routes.
  154. * @param bool $case_sensitive
  155. * Determines if the match should be case-sensitive of not.
  156. *
  157. * @return array|null
  158. * An array of parameters. NULL when there is no match.
  159. *
  160. * @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection()
  161. * @see \Drupal\Core\Routing\RouteProvider::getRoutesByPath()
  162. */
  163. protected function doMatchCollection($pathinfo, RouteCollection $routes, $case_sensitive) {
  164. foreach ($routes as $name => $route) {
  165. $compiledRoute = $route->compile();
  166. // Set the regex to use UTF-8.
  167. $regex = $compiledRoute->getRegex() . 'u';
  168. if (!$case_sensitive) {
  169. $regex = $regex . 'i';
  170. }
  171. if (!preg_match($regex, $pathinfo, $matches)) {
  172. continue;
  173. }
  174. $hostMatches = [];
  175. if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
  176. $routes->remove($name);
  177. continue;
  178. }
  179. // Check HTTP method requirement.
  180. if ($requiredMethods = $route->getMethods()) {
  181. // HEAD and GET are equivalent as per RFC.
  182. if ('HEAD' === $method = $this->context->getMethod()) {
  183. $method = 'GET';
  184. }
  185. if (!in_array($method, $requiredMethods)) {
  186. $this->allow = array_merge($this->allow, $requiredMethods);
  187. $routes->remove($name);
  188. continue;
  189. }
  190. }
  191. $status = $this->handleRouteRequirements($pathinfo, $name, $route);
  192. if (self::ROUTE_MATCH === $status[0]) {
  193. return $status[1];
  194. }
  195. if (self::REQUIREMENT_MISMATCH === $status[0]) {
  196. $routes->remove($name);
  197. continue;
  198. }
  199. return $this->getAttributes($route, $name, array_replace($matches, $hostMatches));
  200. }
  201. }
  202. /**
  203. * Returns a collection of potential matching routes for a request.
  204. *
  205. * @param \Symfony\Component\HttpFoundation\Request $request
  206. * The current request.
  207. *
  208. * @return \Symfony\Component\Routing\RouteCollection
  209. * The initial fetched route collection.
  210. */
  211. protected function getInitialRouteCollection(Request $request) {
  212. return $this->routeProvider->getRouteCollectionForRequest($request);
  213. }
  214. /**
  215. * Apply the route enhancers to the defaults, according to priorities.
  216. *
  217. * @param array $defaults
  218. * The defaults coming from the final matched route.
  219. * @param \Symfony\Component\HttpFoundation\Request $request
  220. * The request.
  221. *
  222. * @return array
  223. * The request attributes after applying the enhancers. This might consist
  224. * raw values from the URL but also upcasted values, like entity objects,
  225. * from route enhancers.
  226. */
  227. protected function applyRouteEnhancers($defaults, Request $request) {
  228. foreach ($this->enhancers as $enhancer) {
  229. if ($enhancer instanceof RouteEnhancerInterface && !$enhancer->applies($defaults[RouteObjectInterface::ROUTE_OBJECT])) {
  230. continue;
  231. }
  232. $defaults = $enhancer->enhance($defaults, $request);
  233. }
  234. return $defaults;
  235. }
  236. /**
  237. * Applies all route filters to a given route collection.
  238. *
  239. * This method reduces the sets of routes further down, for example by
  240. * checking the HTTP method.
  241. *
  242. * @param \Symfony\Component\Routing\RouteCollection $collection
  243. * The route collection.
  244. * @param \Symfony\Component\HttpFoundation\Request $request
  245. * The request.
  246. *
  247. * @return \Symfony\Component\Routing\RouteCollection
  248. * The filtered/sorted route collection.
  249. */
  250. protected function applyRouteFilters(RouteCollection $collection, Request $request) {
  251. // Route filters are expected to throw an exception themselves if they
  252. // end up filtering the list down to 0.
  253. foreach ($this->filters as $filter) {
  254. $collection = $filter->filter($collection, $request);
  255. }
  256. return $collection;
  257. }
  258. /**
  259. * {@inheritdoc}
  260. */
  261. public function getRouteCollection() {
  262. return new LazyRouteCollection($this->routeProvider);
  263. }
  264. /**
  265. * {@inheritdoc}
  266. */
  267. public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH) {
  268. @trigger_error('Use the \Drupal\Core\Url object instead', E_USER_DEPRECATED);
  269. return $this->urlGenerator->generate($name, $parameters, $referenceType);
  270. }
  271. }