Chris@0: connection = $connection; Chris@0: $this->state = $state; Chris@0: $this->currentPath = $current_path; Chris@0: $this->cache = $cache_backend; Chris@0: $this->cacheTagInvalidator = $cache_tag_invalidator; Chris@0: $this->pathProcessor = $path_processor; Chris@0: $this->tableName = $table; Chris@14: $this->languageManager = $language_manager ?: \Drupal::languageManager(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Finds routes that may potentially match the request. Chris@0: * Chris@0: * This may return a mixed list of class instances, but all routes returned Chris@0: * must extend the core symfony route. The classes may also implement Chris@0: * RouteObjectInterface to link to a content document. Chris@0: * Chris@0: * This method may not throw an exception based on implementation specific Chris@0: * restrictions on the url. That case is considered a not found - returning Chris@0: * an empty array. Exceptions are only used to abort the whole request in Chris@0: * case something is seriously broken, like the storage backend being down. Chris@0: * Chris@0: * Note that implementations may not implement an optimal matching Chris@0: * algorithm, simply a reasonable first pass. That allows for potentially Chris@0: * very large route sets to be filtered down to likely candidates, which Chris@0: * may then be filtered in memory more completely. Chris@0: * Chris@12: * @param \Symfony\Component\HttpFoundation\Request $request Chris@0: * A request against which to match. Chris@0: * Chris@0: * @return \Symfony\Component\Routing\RouteCollection Chris@0: * RouteCollection with all urls that could potentially match $request. Chris@0: * Empty collection if nothing can match. The collection will be sorted from Chris@0: * highest to lowest fit (match of path parts) and then in ascending order Chris@0: * by route name for routes with the same fit. Chris@0: */ Chris@0: public function getRouteCollectionForRequest(Request $request) { Chris@0: // Cache both the system path as well as route parameters and matching Chris@0: // routes. Chris@14: $cid = $this->getRouteCollectionCacheId($request); Chris@0: if ($cached = $this->cache->get($cid)) { Chris@0: $this->currentPath->setPath($cached->data['path'], $request); Chris@0: $request->query->replace($cached->data['query']); Chris@0: return $cached->data['routes']; Chris@0: } Chris@0: else { Chris@0: // Just trim on the right side. Chris@0: $path = $request->getPathInfo(); Chris@0: $path = $path === '/' ? $path : rtrim($request->getPathInfo(), '/'); Chris@0: $path = $this->pathProcessor->processInbound($path, $request); Chris@0: $this->currentPath->setPath($path, $request); Chris@0: // Incoming path processors may also set query parameters. Chris@0: $query_parameters = $request->query->all(); Chris@0: $routes = $this->getRoutesByPath(rtrim($path, '/')); Chris@0: $cache_value = [ Chris@0: 'path' => $path, Chris@0: 'query' => $query_parameters, Chris@0: 'routes' => $routes, Chris@0: ]; Chris@0: $this->cache->set($cid, $cache_value, CacheBackendInterface::CACHE_PERMANENT, ['route_match']); Chris@0: return $routes; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Find the route using the provided route name (and parameters). Chris@0: * Chris@0: * @param string $name Chris@0: * The route name to fetch Chris@0: * Chris@0: * @return \Symfony\Component\Routing\Route Chris@0: * The found route. Chris@0: * Chris@0: * @throws \Symfony\Component\Routing\Exception\RouteNotFoundException Chris@0: * Thrown if there is no route with that name in this repository. Chris@0: */ Chris@0: public function getRouteByName($name) { Chris@0: $routes = $this->getRoutesByNames([$name]); Chris@0: if (empty($routes)) { Chris@0: throw new RouteNotFoundException(sprintf('Route "%s" does not exist.', $name)); Chris@0: } Chris@0: Chris@0: return reset($routes); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function preLoadRoutes($names) { Chris@0: if (empty($names)) { Chris@0: throw new \InvalidArgumentException('You must specify the route names to load'); Chris@0: } Chris@0: Chris@0: $routes_to_load = array_diff($names, array_keys($this->routes), array_keys($this->serializedRoutes)); Chris@0: if ($routes_to_load) { Chris@0: Chris@0: $cid = static::ROUTE_LOAD_CID_PREFIX . hash('sha512', serialize($routes_to_load)); Chris@0: if ($cache = $this->cache->get($cid)) { Chris@0: $routes = $cache->data; Chris@0: } Chris@0: else { Chris@0: try { Chris@0: $result = $this->connection->query('SELECT name, route FROM {' . $this->connection->escapeTable($this->tableName) . '} WHERE name IN ( :names[] )', [':names[]' => $routes_to_load]); Chris@0: $routes = $result->fetchAllKeyed(); Chris@0: Chris@0: $this->cache->set($cid, $routes, Cache::PERMANENT, ['routes']); Chris@0: } Chris@0: catch (\Exception $e) { Chris@0: $routes = []; Chris@0: } Chris@0: } Chris@0: Chris@0: $this->serializedRoutes += $routes; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getRoutesByNames($names) { Chris@0: $this->preLoadRoutes($names); Chris@0: Chris@0: foreach ($names as $name) { Chris@0: // The specified route name might not exist or might be serialized. Chris@0: if (!isset($this->routes[$name]) && isset($this->serializedRoutes[$name])) { Chris@0: $this->routes[$name] = unserialize($this->serializedRoutes[$name]); Chris@0: unset($this->serializedRoutes[$name]); Chris@0: } Chris@0: } Chris@0: Chris@0: return array_intersect_key($this->routes, array_flip($names)); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns an array of path pattern outlines that could match the path parts. Chris@0: * Chris@0: * @param array $parts Chris@0: * The parts of the path for which we want candidates. Chris@0: * Chris@0: * @return array Chris@0: * An array of outlines that could match the specified path parts. Chris@0: */ Chris@0: protected function getCandidateOutlines(array $parts) { Chris@0: $number_parts = count($parts); Chris@0: $ancestors = []; Chris@0: $length = $number_parts - 1; Chris@0: $end = (1 << $number_parts) - 1; Chris@0: Chris@0: // The highest possible mask is a 1 bit for every part of the path. We will Chris@0: // check every value down from there to generate a possible outline. Chris@0: if ($number_parts == 1) { Chris@0: $masks = [1]; Chris@0: } Chris@0: elseif ($number_parts <= 3 && $number_parts > 0) { Chris@0: // Optimization - don't query the state system for short paths. This also Chris@0: // insulates against the state entry for masks going missing for common Chris@0: // user-facing paths since we generate all values without checking state. Chris@0: $masks = range($end, 1); Chris@0: } Chris@0: elseif ($number_parts <= 0) { Chris@0: // No path can match, short-circuit the process. Chris@0: $masks = []; Chris@0: } Chris@0: else { Chris@0: // Get the actual patterns that exist out of state. Chris@0: $masks = (array) $this->state->get('routing.menu_masks.' . $this->tableName, []); Chris@0: } Chris@0: Chris@0: // Only examine patterns that actually exist as router items (the masks). Chris@0: foreach ($masks as $i) { Chris@0: if ($i > $end) { Chris@0: // Only look at masks that are not longer than the path of interest. Chris@0: continue; Chris@0: } Chris@0: elseif ($i < (1 << $length)) { Chris@0: // We have exhausted the masks of a given length, so decrease the length. Chris@0: --$length; Chris@0: } Chris@0: $current = ''; Chris@0: for ($j = $length; $j >= 0; $j--) { Chris@0: // Check the bit on the $j offset. Chris@0: if ($i & (1 << $j)) { Chris@0: // Bit one means the original value. Chris@0: $current .= $parts[$length - $j]; Chris@0: } Chris@0: else { Chris@0: // Bit zero means means wildcard. Chris@0: $current .= '%'; Chris@0: } Chris@0: // Unless we are at offset 0, add a slash. Chris@0: if ($j) { Chris@0: $current .= '/'; Chris@0: } Chris@0: } Chris@0: $ancestors[] = '/' . $current; Chris@0: } Chris@0: return $ancestors; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getRoutesByPattern($pattern) { Chris@0: $path = RouteCompiler::getPatternOutline($pattern); Chris@0: Chris@0: return $this->getRoutesByPath($path); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Get all routes which match a certain pattern. Chris@0: * Chris@0: * @param string $path Chris@0: * The route pattern to search for. Chris@0: * Chris@0: * @return \Symfony\Component\Routing\RouteCollection Chris@0: * Returns a route collection of matching routes. The collection may be Chris@0: * empty and will be sorted from highest to lowest fit (match of path parts) Chris@0: * and then in ascending order by route name for routes with the same fit. Chris@0: */ Chris@0: protected function getRoutesByPath($path) { Chris@0: // Split the path up on the slashes, ignoring multiple slashes in a row Chris@0: // or leading or trailing slashes. Convert to lower case here so we can Chris@0: // have a case-insensitive match from the incoming path to the lower case Chris@0: // pattern outlines from \Drupal\Core\Routing\RouteCompiler::compile(). Chris@0: // @see \Drupal\Core\Routing\CompiledRoute::__construct() Chris@17: $parts = preg_split('@/+@', mb_strtolower($path), NULL, PREG_SPLIT_NO_EMPTY); Chris@0: Chris@0: $collection = new RouteCollection(); Chris@0: Chris@0: $ancestors = $this->getCandidateOutlines($parts); Chris@0: if (empty($ancestors)) { Chris@0: return $collection; Chris@0: } Chris@0: Chris@0: // The >= check on number_parts allows us to match routes with optional Chris@0: // trailing wildcard parts as long as the pattern matches, since we Chris@0: // dump the route pattern without those optional parts. Chris@0: try { Chris@0: $routes = $this->connection->query("SELECT name, route, fit FROM {" . $this->connection->escapeTable($this->tableName) . "} WHERE pattern_outline IN ( :patterns[] ) AND number_parts >= :count_parts", [ Chris@0: ':patterns[]' => $ancestors, Chris@0: ':count_parts' => count($parts), Chris@0: ]) Chris@0: ->fetchAll(\PDO::FETCH_ASSOC); Chris@0: } Chris@0: catch (\Exception $e) { Chris@0: $routes = []; Chris@0: } Chris@0: Chris@0: // We sort by fit and name in PHP to avoid a SQL filesort and avoid any Chris@0: // difference in the sorting behavior of SQL back-ends. Chris@0: usort($routes, [$this, 'routeProviderRouteCompare']); Chris@0: Chris@0: foreach ($routes as $row) { Chris@0: $collection->add($row['name'], unserialize($row['route'])); Chris@0: } Chris@0: Chris@0: return $collection; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Comparison function for usort on routes. Chris@0: */ Chris@0: protected function routeProviderRouteCompare(array $a, array $b) { Chris@0: if ($a['fit'] == $b['fit']) { Chris@0: return strcmp($a['name'], $b['name']); Chris@0: } Chris@0: // Reverse sort from highest to lowest fit. PHP should cast to int, but Chris@0: // the explicit cast makes this sort more robust against unexpected input. Chris@0: return (int) $a['fit'] < (int) $b['fit'] ? 1 : -1; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getAllRoutes() { Chris@0: return new PagedRouteCollection($this); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function reset() { Chris@17: $this->routes = []; Chris@0: $this->serializedRoutes = []; Chris@0: $this->cacheTagInvalidator->invalidateTags(['routes']); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public static function getSubscribedEvents() { Chris@0: $events[RoutingEvents::FINISHED][] = ['reset']; Chris@0: return $events; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getRoutesPaged($offset, $length = NULL) { Chris@0: $select = $this->connection->select($this->tableName, 'router') Chris@0: ->fields('router', ['name', 'route']); Chris@0: Chris@0: if (isset($length)) { Chris@0: $select->range($offset, $length); Chris@0: } Chris@0: Chris@0: $routes = $select->execute()->fetchAllKeyed(); Chris@0: Chris@0: $result = []; Chris@0: foreach ($routes as $name => $route) { Chris@0: $result[$name] = unserialize($route); Chris@0: } Chris@0: Chris@0: return $result; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getRoutesCount() { Chris@0: return $this->connection->query("SELECT COUNT(*) FROM {" . $this->connection->escapeTable($this->tableName) . "}")->fetchField(); Chris@0: } Chris@0: Chris@14: /** Chris@14: * Returns the cache ID for the route collection cache. Chris@14: * Chris@14: * @param \Symfony\Component\HttpFoundation\Request $request Chris@14: * The request object. Chris@14: * Chris@14: * @return string Chris@14: * The cache ID. Chris@14: */ Chris@14: protected function getRouteCollectionCacheId(Request $request) { Chris@14: // Include the current language code in the cache identifier as Chris@14: // the language information can be elsewhere than in the path, for example Chris@14: // based on the domain. Chris@14: $language_part = $this->getCurrentLanguageCacheIdPart(); Chris@14: return 'route:' . $language_part . ':' . $request->getPathInfo() . ':' . $request->getQueryString(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the language identifier for the route collection cache. Chris@14: * Chris@14: * @return string Chris@14: * The language identifier. Chris@14: */ Chris@14: protected function getCurrentLanguageCacheIdPart() { Chris@14: // This must be in sync with the language logic in Chris@14: // \Drupal\Core\PathProcessor\PathProcessorAlias::processInbound() and Chris@14: // \Drupal\Core\Path\AliasManager::getPathByAlias(). Chris@14: // @todo Update this if necessary in https://www.drupal.org/node/1125428. Chris@14: return $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId(); Chris@14: } Chris@14: Chris@0: }