diff --git a/core/lib/Drupal/Core/Routing/CompiledRoute.php b/core/lib/Drupal/Core/Routing/CompiledRoute.php index bd58c8a134a..d8e93f89c71 100644 --- a/core/lib/Drupal/Core/Routing/CompiledRoute.php +++ b/core/lib/Drupal/Core/Routing/CompiledRoute.php @@ -23,6 +23,13 @@ class CompiledRoute { */ protected $patternOutline; + /** + * The number of parts in the path of this route. + * + * @var int + */ + protected $numParts; + /** * The Route object of which this object is the compiled version. * @@ -45,11 +52,14 @@ class CompiledRoute { * The fitness of the route. * @param string $fit * The pattern outline for this route. + * @param int $num_parts + * The number of parts in the path. */ - public function __construct(Route $route, $fit, $pattern_outline) { + public function __construct(Route $route, $fit, $pattern_outline, $num_parts) { $this->route = $route; $this->fit = $fit; $this->patternOutline = $pattern_outline; + $this->numParts = $num_parts; } /** @@ -64,6 +74,19 @@ class CompiledRoute { return $this->fit; } + /** + * Returns the number of parts in this route's path. + * + * The string "foo/bar/baz" has 3 parts, regardless of how many of them are + * placeholders. + * + * @return int + * The number of parts in the path. + */ + public function getNumParts() { + return $this->numParts; + } + /** * Returns the pattern outline of this route. * diff --git a/core/lib/Drupal/Core/Routing/MatcherDumper.php b/core/lib/Drupal/Core/Routing/MatcherDumper.php index 6c425af2800..1ca60e4df24 100644 --- a/core/lib/Drupal/Core/Routing/MatcherDumper.php +++ b/core/lib/Drupal/Core/Routing/MatcherDumper.php @@ -85,6 +85,7 @@ class MatcherDumper implements MatcherDumperInterface { 'fit', 'pattern', 'pattern_outline', + 'number_parts', 'route', )); @@ -96,6 +97,7 @@ class MatcherDumper implements MatcherDumperInterface { 'fit' => $compiled->getFit(), 'pattern' => $compiled->getPattern(), 'pattern_outline' => $compiled->getPatternOutline(), + 'number_parts' => $compiled->getNumParts(), 'route' => serialize($route), ); $insert->values($values); diff --git a/core/lib/Drupal/Core/Routing/PathMatcher.php b/core/lib/Drupal/Core/Routing/PathMatcher.php new file mode 100644 index 00000000000..7f39dcf7390 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/PathMatcher.php @@ -0,0 +1,118 @@ +connection = $connection; + + $this->tableName = $table; + } + + /** + * Matches a request against multiple routes. + * + * @param Request $request + * A Request object against which to match. + * + * @return RouteCollection + * A RouteCollection of matched routes. + */ + public function matchRequestPartial(Request $request) { + + $path = $request->getPathInfo(); + + $parts = array_slide(explode('/', $path), 0, MatcherDumper::MAX_PARTS); + + $number_parts = count($parts); + + $ancestors = $this->getCandidateOutlines($parts); + + // @todo We want to allow matching more than one result because there could + // be more than one result with the same path. But how do we do that and + // limit by fit? + $routes = $this->connection + ->select($this->tableName, 'r') + ->fields('r', array('name', 'route')) + ->condition('pattern_outline', $ancestors, 'IN') + ->condition('number_parts', $number_parts) + ->execute() + ->fetchAllKeyed(); + + $collection = new RouteCollection(); + foreach ($routes as $name => $route) { + $collection->add($name, $route); + } + + return $collection; + } + + /** + * 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); + $length = $number_parts - 1; + $end = (1 << $number_parts) - 1; + $candidates = array(); + + $start = pow($number_parts-1, 2); + + // 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. + $masks = range($end, $start); + + foreach ($masks as $i) { + $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 .= '/'; + } + } + $candidates[] = $current; + } + + return $candidates; + } +} + diff --git a/core/lib/Drupal/Core/Routing/RouteCompiler.php b/core/lib/Drupal/Core/Routing/RouteCompiler.php index 8c3b7b9ef93..c05e72590ec 100644 --- a/core/lib/Drupal/Core/Routing/RouteCompiler.php +++ b/core/lib/Drupal/Core/Routing/RouteCompiler.php @@ -31,7 +31,9 @@ class RouteCompiler implements RouteCompilerInterface { $pattern_outline = $this->getPatternOutline($route->getPattern()); - return new CompiledRoute($route, $fit, $pattern_outline); + $num_parts = count(explode('/', $pattern_outline)); + + return new CompiledRoute($route, $fit, $pattern_outline, $num_parts); } diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/MatcherDumperTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/MatcherDumperTest.php index cb99cf0efc5..5da3befaa04 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Routing/MatcherDumperTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/MatcherDumperTest.php @@ -18,6 +18,14 @@ use Drupal\Core\Routing\MatcherDumper; * Basic tests for the UrlMatcherDumper. */ class MatcherDumperTest extends UnitTestBase { + + /** + * A collection of shared fixture data for tests. + * + * @var RoutingFixtures + */ + protected $fixtures; + public static function getInfo() { return array( 'name' => 'Dumper tests', @@ -26,6 +34,12 @@ class MatcherDumperTest extends UnitTestBase { ); } + function __construct($test_id = NULL) { + parent::__construct($test_id); + + $this->fixtures = new RoutingFixtures(); + } + function setUp() { parent::setUp(); } @@ -132,80 +146,13 @@ class MatcherDumperTest extends UnitTestBase { * Creates a test database table just for unit testing purposes. */ protected function prepareTables($connection) { - - $tables['test_routes'] = array( - 'description' => 'Maps paths to various callbacks (access, page and title)', - 'fields' => array( - 'name' => array( - 'description' => 'Primary Key: Machine name of this route', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ), - 'pattern' => array( - 'description' => 'The path pattern for this URI', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ), - 'pattern_outline' => array( - 'description' => 'The pattern', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ), - 'route_set' => array( - 'description' => 'The route set grouping to which a route belongs.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ), - 'access_callback' => array( - 'description' => 'The callback which determines the access to this router path. Defaults to user_access.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ), - 'access_arguments' => array( - 'description' => 'A serialized array of arguments for the access callback.', - 'type' => 'blob', - 'not null' => FALSE, - ), - 'fit' => array( - 'description' => 'A numeric representation of how specific the path is.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - 'number_parts' => array( - 'description' => 'Number of parts in this router path.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'size' => 'small', - ), - 'route' => array( - 'description' => 'A serialized Route object', - 'type' => 'text', - ), - ), - 'indexes' => array( - 'fit' => array('fit'), - 'pattern_outline' => array('pattern_outline'), - 'route_set' => array('route_set'), - ), - 'primary key' => array('name'), - ); - - + $tables = $this->fixtures->routingTableDefinition(); $schema = $connection->schema(); - $schema->dropTable('test_routes'); - $schema->createTable('test_routes', $tables['test_routes']); + + foreach ($tables as $name => $table) { + $schema->dropTable($name); + $schema->createTable($name, $table); + } } } diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/PathMatcherTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/PathMatcherTest.php new file mode 100644 index 00000000000..2931f62a20a --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/PathMatcherTest.php @@ -0,0 +1,69 @@ + 'Path matcher tests', + 'description' => 'Confirm that the path matching library is working correctly.', + 'group' => 'Routing', + ); + } + + function __construct($test_id = NULL) { + parent::__construct($test_id); + + $this->fixtures = new RoutingFixtures(); + } + + /** + * Confirms that the correct candidate outlines are generated. + */ + public function testCandidateOutlines() { + + $connection = Database::getConnection(); + $matcher = new PathMatcher($connection); + + $parts = array('node', '5', 'edit'); + + $candidates = $matcher->getCandidateOutlines($parts); + + //debug($candidates); + + $candidates = array_flip($candidates); + + $this->assertTrue(count($candidates) == 4, t('Correct number of candidates found')); + $this->assertTrue(array_key_exists('node/5/edit', $candidates), t('First candidate found.')); + $this->assertTrue(array_key_exists('node/5/%', $candidates), t('Second candidate found.')); + $this->assertTrue(array_key_exists('node/%/edit', $candidates), t('Third candidate found.')); + $this->assertTrue(array_key_exists('node/%/%', $candidates), t('Fourth candidate found.')); + } + + +} + diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/RoutingFixtures.php b/core/modules/system/lib/Drupal/system/Tests/Routing/RoutingFixtures.php new file mode 100644 index 00000000000..f7889afa424 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/RoutingFixtures.php @@ -0,0 +1,116 @@ +setRequirement('_method', 'GET'); + $collection->add('route_a', $route); + + $route = new Route('path/one'); + $route->setRequirement('_method', 'PUT'); + $collection->add('route_b', $route); + + $route = new Route('path/two'); + $route->setRequirement('_method', 'GET'); + $collection->add('route_c', $route); + + $route = new Route('path/three'); + $collection->add('route_d', $route); + + $route = new Route('path/two'); + $route->setRequirement('_method', 'GET|HEAD'); + $collection->add('route_e', $route); + + return $collection; + } + + public function routingTableDefinition() { + + $tables['test_routes'] = array( + 'description' => 'Maps paths to various callbacks (access, page and title)', + 'fields' => array( + 'name' => array( + 'description' => 'Primary Key: Machine name of this route', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'pattern' => array( + 'description' => 'The path pattern for this URI', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'pattern_outline' => array( + 'description' => 'The pattern', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'route_set' => array( + 'description' => 'The route set grouping to which a route belongs.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'access_callback' => array( + 'description' => 'The callback which determines the access to this router path. Defaults to user_access.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'access_arguments' => array( + 'description' => 'A serialized array of arguments for the access callback.', + 'type' => 'blob', + 'not null' => FALSE, + ), + 'fit' => array( + 'description' => 'A numeric representation of how specific the path is.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'number_parts' => array( + 'description' => 'Number of parts in this router path.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'size' => 'small', + ), + 'route' => array( + 'description' => 'A serialized Route object', + 'type' => 'text', + ), + ), + 'indexes' => array( + 'fit' => array('fit'), + 'pattern_outline' => array('pattern_outline'), + 'route_set' => array('route_set'), + ), + 'primary key' => array('name'), + ); + + return $tables; + } +}