Newer
Older
/**
* @file
* Contains Drupal\Core\Routing\RouteProvider.
*/
namespace Drupal\Core\Routing;
use Drupal\Component\Utility\String;
catch
committed
use Drupal\Core\Path\CurrentPathStack;
Alex Pott
committed
use Drupal\Core\State\StateInterface;
Angie Byron
committed
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 Symfony\Component\Routing\Exception\ResourceNotFoundException;
use \Drupal\Core\Database\Connection;
/**
* A Route Provider front-end for all Drupal-stored routes.
Angie Byron
committed
class RouteProvider implements RouteProviderInterface, PagedRouteProviderInterface, EventSubscriberInterface {
/**
* The database connection from which to read route information.
*
Alex Pott
committed
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The name of the SQL table from which to read the routes.
*
* @var string
*/
protected $tableName;
/**
* The route builder.
*
* @var \Drupal\Core\Routing\RouteBuilderInterface
*/
protected $routeBuilder;
/**
* The state.
*
Alex Pott
committed
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* A cache of already-loaded routes, keyed by route name.
*
* @var array
*/
protected $routes = array();
catch
committed
/**
* The current path.
*
* @var \Drupal\Core\Path\CurrentPathStack
*/
protected $currentPath;
/**
* Constructs a new PathMatcher.
*
* @param \Drupal\Core\Database\Connection $connection
* A database connection object.
* @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder
* The route builder.
Alex Pott
committed
* @param \Drupal\Core\State\StateInterface $state
* The state.
catch
committed
* @param \Drupal\Core\Path\CurrentPathStack $current_path
* THe current path.
* @param string $table
* The table in the database to use for matching.
*/
catch
committed
public function __construct(Connection $connection, RouteBuilderInterface $route_builder, StateInterface $state, CurrentPathStack $current_path, $table = 'router') {
$this->connection = $connection;
$this->routeBuilder = $route_builder;
$this->state = $state;
$this->tableName = $table;
catch
committed
$this->currentPath = $current_path;
}
/**
* 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.
*
* @todo Should this method's found routes also be included in the cache?
*/
public function getRouteCollectionForRequest(Request $request) {
catch
committed
$path = $this->currentPath->getPath($request);
catch
committed
$collection = $this->getRoutesByPath(rtrim($path, '/'));
// Try rebuilding the router if it is necessary.
if (!$collection->count() && $this->routeBuilder->rebuildIfNeeded()) {
$collection = $this->getRoutesByPath($path);
}
return $collection;
}
/**
* 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);
}
/**
* Find many routes by their names using the provided list of names.
*
* Note that this method may not throw an exception if some of the routes
* are not found. It will just return the list of those routes it found.
*
* This method exists in order to allow performance optimizations. The
* simple implementation could be to just repeatedly call
* $this->getRouteByName().
* @param array $names
* The list of names to retrieve.
* @return \Symfony\Component\Routing\Route[]
* Iterable thing with the keys the names of the $names argument.
public function getRoutesByNames($names) {
if (empty($names)) {
throw new \InvalidArgumentException('You must specify the route names to load');
}
$this->routeBuilder->rebuildIfNeeded();
$routes_to_load = array_diff($names, array_keys($this->routes));
if ($routes_to_load) {
$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();
foreach ($routes as $name => $route) {
$this->routes[$name] = unserialize($route);
}
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.
*/
public 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) {
// 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());
}
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
// 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;
}
Alex Pott
committed
/**
* {@inheritdoc}
*/
public function getRoutesByPattern($pattern) {
$path = RouteCompiler::getPatternOutline($pattern);
$this->routeBuilder->rebuildIfNeeded();
Alex Pott
committed
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) {
// Filter out each empty value, though allow '0' and 0, which would be
// filtered out by empty().
$parts = array_values(array_filter(explode('/', $path), function($value) {
Alex Pott
committed
return $value !== NULL && $value !== '';
}));
Alex Pott
committed
$collection = new RouteCollection();
Alex Pott
committed
$ancestors = $this->getCandidateOutlines($parts);
if (empty($ancestors)) {
return $collection;
}
Alex Pott
committed
$routes = $this->connection->query("SELECT name, route FROM {" . $this->connection->escapeTable($this->tableName) . "} WHERE pattern_outline IN ( :patterns[] ) ORDER BY fit DESC, name ASC", array(
':patterns[]' => $ancestors,
Alex Pott
committed
))
->fetchAllKeyed();
foreach ($routes as $name => $route) {
$route = unserialize($route);
if (preg_match($route->compile()->getRegex(), $path, $matches)) {
$collection->add($name, $route);
}
}
return $collection;
}
/**
* {@inheritdoc}
*/
public function getAllRoutes() {
Angie Byron
committed
return new PagedRouteCollection($this);
}
/**
* {@inheritdoc}
*/
public function reset() {
$this->routes = array();
}
/**
* {@inheritdoc}
*/
static function getSubscribedEvents() {
$events[RoutingEvents::FINISHED][] = array('reset');
return $events;
}
Angie Byron
committed
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
/**
* {@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();
}