Add a skeleton for a Path matcher.

The PathMatcher matches against the database table structure generated by the
MatcherDumper.  As of this commit the lookup is not yet implemented. It's still
in testing.
8.0.x
Larry Garfield 2012-06-23 19:59:46 -05:00 committed by effulgentsia
parent db11de09c8
commit e9a95aa1fb
7 changed files with 352 additions and 75 deletions

View File

@ -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.
*

View File

@ -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);

View File

@ -0,0 +1,118 @@
<?php
namespace Drupal\Core\Routing;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RouteCollection;
use Drupal\Core\Database\Connection;
/**
* Description of PathMatcher
*
* @author crell
*/
class PathMatcher implements InitialMatcherInterface {
/**
* 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;
public function __construct(Connection $connection, $table = 'router') {
$this->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;
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,69 @@
<?php
/**
* @file
* Definition of Drupal\system\Tests\Routing\PartialMatcherTest.
*/
namespace Drupal\system\Tests\Routing;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use Drupal\simpletest\UnitTestBase;
use Drupal\Core\Routing\PathMatcher;
use Drupal\Core\Database\Database;
/**
* Basic tests for the UrlMatcherDumper.
*/
class PathMatcherTest extends UnitTestBase {
/**
* A collection of shared fixture data for tests.
*
* @var RoutingFixtures
*/
protected $fixtures;
public static function getInfo() {
return array(
'name' => '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.'));
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace Drupal\system\Tests\Routing;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Utility methods to generate sample data, database configuration, etc.
*/
class RoutingFixtures {
/**
* Returns a standard set of routes for testing.
*
* @return \Symfony\Component\Routing\RouteCollection
*/
public function sampleRouteCollection() {
$collection = new RouteCollection();
$route = new Route('path/one');
$route->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;
}
}