diff core/lib/Drupal/Core/Routing/RouteProvider.php @ 0:4c8ae668cc8c

Initial import (non-working)
author Chris Cannam
date Wed, 29 Nov 2017 16:09:58 +0000
parents
children 7a779792577d
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/lib/Drupal/Core/Routing/RouteProvider.php	Wed Nov 29 16:09:58 2017 +0000
@@ -0,0 +1,434 @@
+<?php
+
+namespace Drupal\Core\Routing;
+
+use Drupal\Component\Utility\Unicode;
+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 = [];
+
+  /**
+   * 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
+   *   RouteCollection with all urls that could potentially match $request.
+   *   Empty collection if nothing can match. The collection will be sorted from
+   *   highest to lowest fit (match of path parts) and then in ascending order
+   *   by route name for routes with the same fit.
+   */
+  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([$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 {
+        try {
+          $result = $this->connection->query('SELECT name, route FROM {' . $this->connection->escapeTable($this->tableName) . '} WHERE name IN ( :names[] )', [':names[]' => $routes_to_load]);
+          $routes = $result->fetchAllKeyed();
+
+          $this->cache->set($cid, $routes, Cache::PERMANENT, ['routes']);
+        }
+        catch (\Exception $e) {
+          $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 = [];
+    $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 = [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 = [];
+    }
+    else {
+      // Get the actual patterns that exist out of state.
+      $masks = (array) $this->state->get('routing.menu_masks.' . $this->tableName, []);
+    }
+
+    // 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.
+   *
+   * @return \Symfony\Component\Routing\RouteCollection
+   *   Returns a route collection of matching routes. The collection may be
+   *   empty and will be sorted from highest to lowest fit (match of path parts)
+   *   and then in ascending order by route name for routes with the same fit.
+   */
+  protected function getRoutesByPath($path) {
+    // Split the path up on the slashes, ignoring multiple slashes in a row
+    // or leading or trailing slashes. Convert to lower case here so we can
+    // have a case-insensitive match from the incoming path to the lower case
+    // pattern outlines from \Drupal\Core\Routing\RouteCompiler::compile().
+    // @see \Drupal\Core\Routing\CompiledRoute::__construct()
+    $parts = preg_split('@/+@', Unicode::strtolower($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.
+    try {
+      $routes = $this->connection->query("SELECT name, route, fit FROM {" . $this->connection->escapeTable($this->tableName) . "} WHERE pattern_outline IN ( :patterns[] ) AND number_parts >= :count_parts", [
+        ':patterns[]' => $ancestors,
+        ':count_parts' => count($parts),
+      ])
+        ->fetchAll(\PDO::FETCH_ASSOC);
+    }
+    catch (\Exception $e) {
+      $routes = [];
+    }
+
+    // We sort by fit and name in PHP to avoid a SQL filesort and avoid any
+    // difference in the sorting behavior of SQL back-ends.
+    usort($routes, [$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  = [];
+    $this->serializedRoutes = [];
+    $this->cacheTagInvalidator->invalidateTags(['routes']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[RoutingEvents::FINISHED][] = ['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();
+  }
+
+}