123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- <?php
- namespace Drupal\Core\Routing;
- use Drupal\Core\Path\CurrentPathStack;
- use Drupal\Core\Routing\Enhancer\RouteEnhancerInterface;
- use Symfony\Cmf\Component\Routing\LazyRouteCollection;
- use Symfony\Cmf\Component\Routing\RouteObjectInterface;
- use Symfony\Cmf\Component\Routing\RouteProviderInterface as BaseRouteProviderInterface;
- use Symfony\Component\HttpFoundation\Request;
- use Symfony\Component\Routing\Exception\MethodNotAllowedException;
- use Symfony\Component\Routing\Exception\ResourceNotFoundException;
- use Symfony\Component\Routing\Generator\UrlGeneratorInterface as BaseUrlGeneratorInterface;
- use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
- use Symfony\Component\Routing\RouteCollection;
- use Symfony\Component\Routing\RouterInterface;
- /**
- * Router implementation in Drupal.
- *
- * A router determines, for an incoming request, the active controller, which is
- * a callable that creates a response.
- *
- * It consists of several steps, of which each are explained in more details
- * below:
- * 1. Get a collection of routes which potentially match the current request.
- * This is done by the route provider. See ::getInitialRouteCollection().
- * 2. Filter the collection down further more. For example this filters out
- * routes applying to other formats: See ::applyRouteFilters()
- * 3. Find the best matching route out of the remaining ones, by applying a
- * regex. See ::matchCollection().
- * 4. Enhance the list of route attributes, for example loading entity objects.
- * See ::applyRouteEnhancers().
- *
- * This implementation uses ideas of the following routers:
- * - \Symfony\Cmf\Component\Routing\DynamicRouter
- * - \Drupal\Core\Routing\UrlMatcher
- * - \Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
- *
- * @see \Symfony\Cmf\Component\Routing\DynamicRouter
- * @see \Drupal\Core\Routing\UrlMatcher
- * @see \Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
- */
- class Router extends UrlMatcher implements RequestMatcherInterface, RouterInterface {
- /**
- * The route provider responsible for the first-pass match.
- *
- * @var \Symfony\Cmf\Component\Routing\RouteProviderInterface
- */
- protected $routeProvider;
- /**
- * The list of available enhancers.
- *
- * @var \Drupal\Core\Routing\EnhancerInterface[]
- */
- protected $enhancers = [];
- /**
- * The list of available route filters.
- *
- * @var \Drupal\Core\Routing\FilterInterface[]
- */
- protected $filters = [];
- /**
- * The URL generator.
- *
- * @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
- */
- protected $urlGenerator;
- /**
- * Constructs a new Router.
- *
- * @param \Symfony\Cmf\Component\Routing\RouteProviderInterface $route_provider
- * The route provider.
- * @param \Drupal\Core\Path\CurrentPathStack $current_path
- * The current path stack.
- * @param \Symfony\Component\Routing\Generator\UrlGeneratorInterface $url_generator
- * The URL generator.
- */
- public function __construct(BaseRouteProviderInterface $route_provider, CurrentPathStack $current_path, BaseUrlGeneratorInterface $url_generator) {
- parent::__construct($current_path);
- $this->routeProvider = $route_provider;
- $this->urlGenerator = $url_generator;
- }
- /**
- * Adds a route filter.
- *
- * @param \Drupal\Core\Routing\FilterInterface $route_filter
- * The route filter.
- */
- public function addRouteFilter(FilterInterface $route_filter) {
- $this->filters[] = $route_filter;
- }
- /**
- * Adds a route enhancer.
- *
- * @param \Drupal\Core\Routing\EnhancerInterface $route_enhancer
- * The route enhancer.
- */
- public function addRouteEnhancer(EnhancerInterface $route_enhancer) {
- $this->enhancers[] = $route_enhancer;
- }
- /**
- * {@inheritdoc}
- */
- public function match($pathinfo) {
- $request = Request::create($pathinfo);
- return $this->matchRequest($request);
- }
- /**
- * {@inheritdoc}
- */
- public function matchRequest(Request $request) {
- $collection = $this->getInitialRouteCollection($request);
- if ($collection->count() === 0) {
- throw new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
- }
- $collection = $this->applyRouteFilters($collection, $request);
- if ($ret = $this->matchCollection(rawurldecode($this->currentPath->getPath($request)), $collection)) {
- return $this->applyRouteEnhancers($ret, $request);
- }
- throw 0 < count($this->allow)
- ? new MethodNotAllowedException(array_unique($this->allow))
- : new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath()));
- }
- /**
- * Tries to match a URL with a set of routes.
- *
- * @param string $pathinfo
- * The path info to be parsed
- * @param \Symfony\Component\Routing\RouteCollection $routes
- * The set of routes.
- *
- * @return array|null
- * An array of parameters. NULL when there is no match.
- */
- protected function matchCollection($pathinfo, RouteCollection $routes) {
- // Try a case-sensitive match.
- $match = $this->doMatchCollection($pathinfo, $routes, TRUE);
- // Try a case-insensitive match.
- if ($match === NULL && $routes->count() > 0) {
- $match = $this->doMatchCollection($pathinfo, $routes, FALSE);
- }
- return $match;
- }
- /**
- * Tries to match a URL with a set of routes.
- *
- * This code is very similar to Symfony's UrlMatcher::matchCollection() but it
- * supports case-insensitive matching. The static prefix optimization is
- * removed as this duplicates work done by the query in
- * RouteProvider::getRoutesByPath().
- *
- * @param string $pathinfo
- * The path info to be parsed
- * @param \Symfony\Component\Routing\RouteCollection $routes
- * The set of routes.
- * @param bool $case_sensitive
- * Determines if the match should be case-sensitive of not.
- *
- * @return array|null
- * An array of parameters. NULL when there is no match.
- *
- * @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection()
- * @see \Drupal\Core\Routing\RouteProvider::getRoutesByPath()
- */
- protected function doMatchCollection($pathinfo, RouteCollection $routes, $case_sensitive) {
- foreach ($routes as $name => $route) {
- $compiledRoute = $route->compile();
- // Set the regex to use UTF-8.
- $regex = $compiledRoute->getRegex() . 'u';
- if (!$case_sensitive) {
- $regex = $regex . 'i';
- }
- if (!preg_match($regex, $pathinfo, $matches)) {
- continue;
- }
- $hostMatches = [];
- if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
- $routes->remove($name);
- continue;
- }
- // Check HTTP method requirement.
- if ($requiredMethods = $route->getMethods()) {
- // HEAD and GET are equivalent as per RFC.
- if ('HEAD' === $method = $this->context->getMethod()) {
- $method = 'GET';
- }
- if (!in_array($method, $requiredMethods)) {
- $this->allow = array_merge($this->allow, $requiredMethods);
- $routes->remove($name);
- continue;
- }
- }
- $status = $this->handleRouteRequirements($pathinfo, $name, $route);
- if (self::ROUTE_MATCH === $status[0]) {
- return $status[1];
- }
- if (self::REQUIREMENT_MISMATCH === $status[0]) {
- $routes->remove($name);
- continue;
- }
- return $this->getAttributes($route, $name, array_replace($matches, $hostMatches));
- }
- }
- /**
- * Returns a collection of potential matching routes for a request.
- *
- * @param \Symfony\Component\HttpFoundation\Request $request
- * The current request.
- *
- * @return \Symfony\Component\Routing\RouteCollection
- * The initial fetched route collection.
- */
- protected function getInitialRouteCollection(Request $request) {
- return $this->routeProvider->getRouteCollectionForRequest($request);
- }
- /**
- * Apply the route enhancers to the defaults, according to priorities.
- *
- * @param array $defaults
- * The defaults coming from the final matched route.
- * @param \Symfony\Component\HttpFoundation\Request $request
- * The request.
- *
- * @return array
- * The request attributes after applying the enhancers. This might consist
- * raw values from the URL but also upcasted values, like entity objects,
- * from route enhancers.
- */
- protected function applyRouteEnhancers($defaults, Request $request) {
- foreach ($this->enhancers as $enhancer) {
- if ($enhancer instanceof RouteEnhancerInterface && !$enhancer->applies($defaults[RouteObjectInterface::ROUTE_OBJECT])) {
- continue;
- }
- $defaults = $enhancer->enhance($defaults, $request);
- }
- return $defaults;
- }
- /**
- * Applies all route filters to a given route collection.
- *
- * This method reduces the sets of routes further down, for example by
- * checking the HTTP method.
- *
- * @param \Symfony\Component\Routing\RouteCollection $collection
- * The route collection.
- * @param \Symfony\Component\HttpFoundation\Request $request
- * The request.
- *
- * @return \Symfony\Component\Routing\RouteCollection
- * The filtered/sorted route collection.
- */
- protected function applyRouteFilters(RouteCollection $collection, Request $request) {
- // Route filters are expected to throw an exception themselves if they
- // end up filtering the list down to 0.
- foreach ($this->filters as $filter) {
- $collection = $filter->filter($collection, $request);
- }
- return $collection;
- }
- /**
- * {@inheritdoc}
- */
- public function getRouteCollection() {
- return new LazyRouteCollection($this->routeProvider);
- }
- /**
- * {@inheritdoc}
- */
- public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH) {
- @trigger_error('Use the \Drupal\Core\Url object instead', E_USER_DEPRECATED);
- return $this->urlGenerator->generate($name, $parameters, $referenceType);
- }
- }
|