Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 14 |
CRAP | |
0.00% |
0 / 105 |
| RouteProvider | |
0.00% |
0 / 1 |
|
0.00% |
0 / 14 |
1560 | |
0.00% |
0 / 105 |
| __construct | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 8 |
|||
| getRouteCollectionForRequest | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 16 |
|||
| getRouteByName | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 4 |
|||
| preLoadRoutes | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 12 |
|||
| getRoutesByNames | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 6 |
|||
| getCandidateOutlines | |
0.00% |
0 / 1 |
132 | |
0.00% |
0 / 25 |
|||
| getRoutesByPattern | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
| getRoutesByPath | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 12 |
|||
| routeProviderRouteCompare | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 3 |
|||
| getAllRoutes | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
| reset | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 4 |
|||
| getSubscribedEvents | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
| getRoutesPaged | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 9 |
|||
| getRoutesCount | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
| <?php | |
| /** | |
| * @file | |
| * Contains \Drupal\Core\Routing\RouteProvider. | |
| */ | |
| namespace Drupal\Core\Routing; | |
| use Drupal\Core\Cache\Cache; | |
| use Drupal\Core\Cache\CacheBackendInterface; | |
| use Drupal\Core\Cache\CacheTagsInvalidatorInterface; | |
| use Drupal\Core\Path\CurrentPathStack; | |
| use Drupal\Core\PathProcessor\InboundPathProcessorInterface; | |
| use Drupal\Core\State\StateInterface; | |
| use Symfony\Cmf\Component\Routing\PagedRouteCollection; | |
| use Symfony\Cmf\Component\Routing\PagedRouteProviderInterface; | |
| use Symfony\Component\EventDispatcher\EventSubscriberInterface; | |
| use Symfony\Component\HttpFoundation\Request; | |
| use Symfony\Component\Routing\Exception\RouteNotFoundException; | |
| use Symfony\Component\Routing\RouteCollection; | |
| use \Drupal\Core\Database\Connection; | |
| /** | |
| * A Route Provider front-end for all Drupal-stored routes. | |
| */ | |
| class RouteProvider implements PreloadableRouteProviderInterface, PagedRouteProviderInterface, EventSubscriberInterface { | |
| /** | |
| * The database connection from which to read route information. | |
| * | |
| * @var \Drupal\Core\Database\Connection | |
| */ | |
| protected $connection; | |
| /** | |
| * The name of the SQL table from which to read the routes. | |
| * | |
| * @var string | |
| */ | |
| protected $tableName; | |
| /** | |
| * The state. | |
| * | |
| * @var \Drupal\Core\State\StateInterface | |
| */ | |
| protected $state; | |
| /** | |
| * A cache of already-loaded routes, keyed by route name. | |
| * | |
| * @var \Symfony\Component\Routing\Route[] | |
| */ | |
| protected $routes = array(); | |
| /** | |
| * A cache of already-loaded serialized routes, keyed by route name. | |
| * | |
| * @var string[] | |
| */ | |
| protected $serializedRoutes = []; | |
| /** | |
| * The current path. | |
| * | |
| * @var \Drupal\Core\Path\CurrentPathStack | |
| */ | |
| protected $currentPath; | |
| /** | |
| * The cache backend. | |
| * | |
| * @var \Drupal\Core\Cache\CacheBackendInterface | |
| */ | |
| protected $cache; | |
| /** | |
| * The cache tag invalidator. | |
| * | |
| * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface | |
| */ | |
| protected $cacheTagInvalidator; | |
| /** | |
| * A path processor manager for resolving the system path. | |
| * | |
| * @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface | |
| */ | |
| protected $pathProcessor; | |
| /** | |
| * Cache ID prefix used to load routes. | |
| */ | |
| const ROUTE_LOAD_CID_PREFIX = 'route_provider.route_load:'; | |
| /** | |
| * Constructs a new PathMatcher. | |
| * | |
| * @param \Drupal\Core\Database\Connection $connection | |
| * A database connection object. | |
| * @param \Drupal\Core\State\StateInterface $state | |
| * The state. | |
| * @param \Drupal\Core\Path\CurrentPathStack $current_path | |
| * The current path. | |
| * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend | |
| * The cache backend. | |
| * @param \Drupal\Core\PathProcessor\InboundPathProcessorInterface $path_processor | |
| * The path processor. | |
| * @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tag_invalidator | |
| * The cache tag invalidator. | |
| * @param string $table | |
| * (Optional) The table in the database to use for matching. Defaults to 'router' | |
| */ | |
| public function __construct(Connection $connection, StateInterface $state, CurrentPathStack $current_path, CacheBackendInterface $cache_backend, InboundPathProcessorInterface $path_processor, CacheTagsInvalidatorInterface $cache_tag_invalidator, $table = 'router') { | |
| $this->connection = $connection; | |
| $this->state = $state; | |
| $this->currentPath = $current_path; | |
| $this->cache = $cache_backend; | |
| $this->cacheTagInvalidator = $cache_tag_invalidator; | |
| $this->pathProcessor = $path_processor; | |
| $this->tableName = $table; | |
| } | |
| /** | |
| * Finds routes that may potentially match the request. | |
| * | |
| * This may return a mixed list of class instances, but all routes returned | |
| * must extend the core symfony route. The classes may also implement | |
| * RouteObjectInterface to link to a content document. | |
| * | |
| * This method may not throw an exception based on implementation specific | |
| * restrictions on the url. That case is considered a not found - returning | |
| * an empty array. Exceptions are only used to abort the whole request in | |
| * case something is seriously broken, like the storage backend being down. | |
| * | |
| * Note that implementations may not implement an optimal matching | |
| * algorithm, simply a reasonable first pass. That allows for potentially | |
| * very large route sets to be filtered down to likely candidates, which | |
| * may then be filtered in memory more completely. | |
| * | |
| * @param Request $request A request against which to match. | |
| * | |
| * @return \Symfony\Component\Routing\RouteCollection with all urls that | |
| * could potentially match $request. Empty collection if nothing can | |
| * match. | |
| */ | |
| public function getRouteCollectionForRequest(Request $request) { | |
| // Cache both the system path as well as route parameters and matching | |
| // routes. | |
| $cid = 'route:' . $request->getPathInfo() . ':' . $request->getQueryString(); | |
| if ($cached = $this->cache->get($cid)) { | |
| $this->currentPath->setPath($cached->data['path'], $request); | |
| $request->query->replace($cached->data['query']); | |
| return $cached->data['routes']; | |
| } | |
| else { | |
| // Just trim on the right side. | |
| $path = $request->getPathInfo(); | |
| $path = $path === '/' ? $path : rtrim($request->getPathInfo(), '/'); | |
| $path = $this->pathProcessor->processInbound($path, $request); | |
| $this->currentPath->setPath($path, $request); | |
| // Incoming path processors may also set query parameters. | |
| $query_parameters = $request->query->all(); | |
| $routes = $this->getRoutesByPath(rtrim($path, '/')); | |
| $cache_value = [ | |
| 'path' => $path, | |
| 'query' => $query_parameters, | |
| 'routes' => $routes, | |
| ]; | |
| $this->cache->set($cid, $cache_value, CacheBackendInterface::CACHE_PERMANENT, ['route_match']); | |
| return $routes; | |
| } | |
| } | |
| /** | |
| * Find the route using the provided route name (and parameters). | |
| * | |
| * @param string $name | |
| * The route name to fetch | |
| * | |
| * @return \Symfony\Component\Routing\Route | |
| * The found route. | |
| * | |
| * @throws \Symfony\Component\Routing\Exception\RouteNotFoundException | |
| * Thrown if there is no route with that name in this repository. | |
| */ | |
| public function getRouteByName($name) { | |
| $routes = $this->getRoutesByNames(array($name)); | |
| if (empty($routes)) { | |
| throw new RouteNotFoundException(sprintf('Route "%s" does not exist.', $name)); | |
| } | |
| return reset($routes); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function preLoadRoutes($names) { | |
| if (empty($names)) { | |
| throw new \InvalidArgumentException('You must specify the route names to load'); | |
| } | |
| $routes_to_load = array_diff($names, array_keys($this->routes), array_keys($this->serializedRoutes)); | |
| if ($routes_to_load) { | |
| $cid = static::ROUTE_LOAD_CID_PREFIX . hash('sha512', serialize($routes_to_load)); | |
| if ($cache = $this->cache->get($cid)) { | |
| $routes = $cache->data; | |
| } | |
| else { | |
| $result = $this->connection->query('SELECT name, route FROM {' . $this->connection->escapeTable($this->tableName) . '} WHERE name IN ( :names[] )', array(':names[]' => $routes_to_load)); | |
| $routes = $result->fetchAllKeyed(); | |
| $this->cache->set($cid, $routes, Cache::PERMANENT, ['routes']); | |
| } | |
| $this->serializedRoutes += $routes; | |
| } | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getRoutesByNames($names) { | |
| $this->preLoadRoutes($names); | |
| foreach ($names as $name) { | |
| // The specified route name might not exist or might be serialized. | |
| if (!isset($this->routes[$name]) && isset($this->serializedRoutes[$name])) { | |
| $this->routes[$name] = unserialize($this->serializedRoutes[$name]); | |
| unset($this->serializedRoutes[$name]); | |
| } | |
| } | |
| return array_intersect_key($this->routes, array_flip($names)); | |
| } | |
| /** | |
| * Returns an array of path pattern outlines that could match the path parts. | |
| * | |
| * @param array $parts | |
| * The parts of the path for which we want candidates. | |
| * | |
| * @return array | |
| * An array of outlines that could match the specified path parts. | |
| */ | |
| protected function getCandidateOutlines(array $parts) { | |
| $number_parts = count($parts); | |
| $ancestors = array(); | |
| $length = $number_parts - 1; | |
| $end = (1 << $number_parts) - 1; | |
| // The highest possible mask is a 1 bit for every part of the path. We will | |
| // check every value down from there to generate a possible outline. | |
| if ($number_parts == 1) { | |
| $masks = array(1); | |
| } | |
| elseif ($number_parts <= 3 && $number_parts > 0) { | |
| // Optimization - don't query the state system for short paths. This also | |
| // insulates against the state entry for masks going missing for common | |
| // user-facing paths since we generate all values without checking state. | |
| $masks = range($end, 1); | |
| } | |
| elseif ($number_parts <= 0) { | |
| // No path can match, short-circuit the process. | |
| $masks = array(); | |
| } | |
| else { | |
| // Get the actual patterns that exist out of state. | |
| $masks = (array) $this->state->get('routing.menu_masks.' . $this->tableName, array()); | |
| } | |
| // Only examine patterns that actually exist as router items (the masks). | |
| foreach ($masks as $i) { | |
| if ($i > $end) { | |
| // Only look at masks that are not longer than the path of interest. | |
| continue; | |
| } | |
| elseif ($i < (1 << $length)) { | |
| // We have exhausted the masks of a given length, so decrease the length. | |
| --$length; | |
| } | |
| $current = ''; | |
| for ($j = $length; $j >= 0; $j--) { | |
| // Check the bit on the $j offset. | |
| if ($i & (1 << $j)) { | |
| // Bit one means the original value. | |
| $current .= $parts[$length - $j]; | |
| } | |
| else { | |
| // Bit zero means means wildcard. | |
| $current .= '%'; | |
| } | |
| // Unless we are at offset 0, add a slash. | |
| if ($j) { | |
| $current .= '/'; | |
| } | |
| } | |
| $ancestors[] = '/' . $current; | |
| } | |
| return $ancestors; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getRoutesByPattern($pattern) { | |
| $path = RouteCompiler::getPatternOutline($pattern); | |
| return $this->getRoutesByPath($path); | |
| } | |
| /** | |
| * Get all routes which match a certain pattern. | |
| * | |
| * @param string $path | |
| * The route pattern to search for (contains % as placeholders). | |
| * | |
| * @return \Symfony\Component\Routing\RouteCollection | |
| * Returns a route collection of matching routes. | |
| */ | |
| protected function getRoutesByPath($path) { | |
| // Split the path up on the slashes, ignoring multiple slashes in a row | |
| // or leading or trailing slashes. | |
| $parts = preg_split('@/+@', $path, NULL, PREG_SPLIT_NO_EMPTY); | |
| $collection = new RouteCollection(); | |
| $ancestors = $this->getCandidateOutlines($parts); | |
| if (empty($ancestors)) { | |
| return $collection; | |
| } | |
| // The >= check on number_parts allows us to match routes with optional | |
| // trailing wildcard parts as long as the pattern matches, since we | |
| // dump the route pattern without those optional parts. | |
| $routes = $this->connection->query("SELECT name, route, fit FROM {" . $this->connection->escapeTable($this->tableName) . "} WHERE pattern_outline IN ( :patterns[] ) AND number_parts >= :count_parts", array( | |
| ':patterns[]' => $ancestors, ':count_parts' => count($parts), | |
| )) | |
| ->fetchAll(\PDO::FETCH_ASSOC); | |
| // We sort by fit and name in PHP to avoid a SQL filesort. | |
| usort($routes, array($this, 'routeProviderRouteCompare')); | |
| foreach ($routes as $row) { | |
| $collection->add($row['name'], unserialize($row['route'])); | |
| } | |
| return $collection; | |
| } | |
| /** | |
| * Comparison function for usort on routes. | |
| */ | |
| protected function routeProviderRouteCompare(array $a, array $b) { | |
| if ($a['fit'] == $b['fit']) { | |
| return strcmp($a['name'], $b['name']); | |
| } | |
| // Reverse sort from highest to lowest fit. PHP should cast to int, but | |
| // the explicit cast makes this sort more robust against unexpected input. | |
| return (int) $a['fit'] < (int) $b['fit'] ? 1 : -1; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getAllRoutes() { | |
| return new PagedRouteCollection($this); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function reset() { | |
| $this->routes = array(); | |
| $this->serializedRoutes = array(); | |
| $this->cacheTagInvalidator->invalidateTags(['routes']); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| static function getSubscribedEvents() { | |
| $events[RoutingEvents::FINISHED][] = array('reset'); | |
| return $events; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getRoutesPaged($offset, $length = NULL) { | |
| $select = $this->connection->select($this->tableName, 'router') | |
| ->fields('router', ['name', 'route']); | |
| if (isset($length)) { | |
| $select->range($offset, $length); | |
| } | |
| $routes = $select->execute()->fetchAllKeyed(); | |
| $result = []; | |
| foreach ($routes as $name => $route) { | |
| $result[$name] = unserialize($route); | |
| } | |
| return $result; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getRoutesCount() { | |
| return $this->connection->query("SELECT COUNT(*) FROM {" . $this->connection->escapeTable($this->tableName) . "}")->fetchField(); | |
| } | |
| } |