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);
129 if ($ret = $this->matchCollection(rawurldecode($this->currentPath->getPath($request)), $collection)) {
130 return $this->applyRouteEnhancers($ret, $request);
133 throw 0 < count($this->allow)
134 ? new MethodNotAllowedException(array_unique($this->allow))
135 : new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
139 * Tries to match a URL with a set of routes.
141 * @param string $pathinfo
142 * The path info to be parsed
143 * @param \Symfony\Component\Routing\RouteCollection $routes
147 * An array of parameters. NULL when there is no match.
149 protected function matchCollection($pathinfo, RouteCollection $routes) {
150 // Try a case-sensitive match.
151 $match = $this->doMatchCollection($pathinfo, $routes, TRUE);
152 // Try a case-insensitive match.
153 if ($match === NULL && $routes->count() > 0) {
154 $match = $this->doMatchCollection($pathinfo, $routes, FALSE);
160 * Tries to match a URL with a set of routes.
162 * This code is very similar to Symfony's UrlMatcher::matchCollection() but it
163 * supports case-insensitive matching. The static prefix optimization is
164 * removed as this duplicates work done by the query in
165 * RouteProvider::getRoutesByPath().
167 * @param string $pathinfo
168 * The path info to be parsed
169 * @param \Symfony\Component\Routing\RouteCollection $routes
171 * @param bool $case_sensitive
172 * Determines if the match should be case-sensitive of not.
175 * An array of parameters. NULL when there is no match.
177 * @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection()
178 * @see \Drupal\Core\Routing\RouteProvider::getRoutesByPath()
180 protected function doMatchCollection($pathinfo, RouteCollection $routes, $case_sensitive) {
181 foreach ($routes as $name => $route) {
182 $compiledRoute = $route->compile();
184 // Set the regex to use UTF-8.
185 $regex = $compiledRoute->getRegex() . 'u';
186 if (!$case_sensitive) {
187 $regex = $regex . 'i';
189 if (!preg_match($regex, $pathinfo, $matches)) {
194 if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
195 $routes->remove($name);
199 // Check HTTP method requirement.
200 if ($requiredMethods = $route->getMethods()) {
201 // HEAD and GET are equivalent as per RFC.
202 if ('HEAD' === $method = $this->context->getMethod()) {
206 if (!in_array($method, $requiredMethods)) {
207 $this->allow = array_merge($this->allow, $requiredMethods);
208 $routes->remove($name);
213 $status = $this->handleRouteRequirements($pathinfo, $name, $route);
215 if (self::ROUTE_MATCH === $status[0]) {
219 if (self::REQUIREMENT_MISMATCH === $status[0]) {
220 $routes->remove($name);
224 return $this->getAttributes($route, $name, array_replace($matches, $hostMatches));
229 * Returns a collection of potential matching routes for a request.
231 * @param \Symfony\Component\HttpFoundation\Request $request
232 * The current request.
234 * @return \Symfony\Component\Routing\RouteCollection
235 * The initial fetched route collection.
237 protected function getInitialRouteCollection(Request $request) {
238 return $this->routeProvider->getRouteCollectionForRequest($request);
242 * Apply the route enhancers to the defaults, according to priorities.
244 * @param array $defaults
245 * The defaults coming from the final matched route.
246 * @param \Symfony\Component\HttpFoundation\Request $request
250 * The request attributes after applying the enhancers. This might consist
251 * raw values from the URL but also upcasted values, like entity objects,
252 * from route enhancers.
254 protected function applyRouteEnhancers($defaults, Request $request) {
255 foreach ($this->enhancers as $enhancer) {
256 if ($enhancer instanceof RouteEnhancerInterface && !$enhancer->applies($defaults[RouteObjectInterface::ROUTE_OBJECT])) {
259 $defaults = $enhancer->enhance($defaults, $request);
266 * Applies all route filters to a given route collection.
268 * This method reduces the sets of routes further down, for example by
269 * checking the HTTP method.
271 * @param \Symfony\Component\Routing\RouteCollection $collection
272 * The route collection.
273 * @param \Symfony\Component\HttpFoundation\Request $request
276 * @return \Symfony\Component\Routing\RouteCollection
277 * The filtered/sorted route collection.
279 protected function applyRouteFilters(RouteCollection $collection, Request $request) {
280 // Route filters are expected to throw an exception themselves if they
281 // end up filtering the list down to 0.
282 foreach ($this->filters as $filter) {
283 $collection = $filter->filter($collection, $request);
292 public function getRouteCollection() {
293 return new LazyRouteCollection($this->routeProvider);
299 public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH) {
300 @trigger_error('Use the \Drupal\Core\Url object instead', E_USER_DEPRECATED);
301 return $this->urlGenerator->generate($name, $parameters, $referenceType);