Add a mechanism for a NestedRouter.

A Nested router is a series of partial routers, each of which whittle down a RouteCollection
until it is left with a single matching route.  That single route is the final route that
matches the request.
8.0.x
Larry Garfield 2012-06-23 16:19:54 -05:00 committed by effulgentsia
parent b0f90a1046
commit 806ff4acc8
9 changed files with 370 additions and 19 deletions

View File

@ -0,0 +1,25 @@
<?php
namespace Drupal\Core\Routing;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RouteCollection;
/**
* A PartialMatcher works like a UrlMatcher, but will return multiple candidate routes.
*/
interface FinalMatcherInterface {
public function setCollection(RouteCollection $collection);
/**
* 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 matchRequest(Request $request);
}

View File

@ -12,7 +12,7 @@ class HttpMethodMatcher implements PartialMatcherInterface {
protected $routes;
public function __construct(RouteCollection $routes) {
public function setCollection(RouteCollection $routes) {
$this->routes = $routes;
}
@ -25,7 +25,7 @@ class HttpMethodMatcher implements PartialMatcherInterface {
* @return RouteCollection
* A RouteCollection of matched routes.
*/
public function matchByRequest(Request $request) {
public function matchRequestPartial(Request $request) {
$method = $request->getMethod();

View File

@ -0,0 +1,22 @@
<?php
namespace Drupal\Core\Routing;
use Symfony\Component\HttpFoundation\Request;
/**
* A PartialMatcher works like a UrlMatcher, but will return multiple candidate routes.
*/
interface InitialMatcherInterface {
/**
* 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);
}

View File

@ -0,0 +1,149 @@
<?php
/**
* @file
* Definition of Drupal\Core\Routing\NestedMatcher.
*/
namespace Drupal\Core\Routing;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\HttpFoundation\Request;
/**
* The nested matcher layers multiple partial matchers together.
*/
class NestedMatcher implements NestedMatcherInterface {
/**
* The final matcher
*
* @var RequestMatcherInterface
*/
protected $finalMatcher;
/**
* An array of PartialMatchers.
*
* @var array
*/
protected $partialMatchers = array();
/**
* The initial matcher to match against.
*
* @var InitialMatcherInterface
*/
protected $initialMatcher;
/**
* The request context.
*
* @var RequestContext
*/
protected $context;
/**
* Adds a partial matcher to the matching plan.
*
* Partial matchers will be run in the order in which they are added.
*
* @param PartialMatcherInterface $matcher
* A partial
*
* @return NestedMatcherInterface
* The current matcher.
*/
public function addPartialMatcher(PartialMatcherInterface $matcher) {
$this->partialMatchers[] = $matcher;
return $this;
}
/**
* Sets the final matcher for the matching plan.
*
* @param UrlMatcherInterface $final
* The matcher that will be called last to ensure only a single route is
* found.
*
* @return NestedMatcherInterface
* The current matcher.
*/
public function setFinalMatcher(FinalMatcherInterface $final) {
$this->finalMatcher = $final;
return $this;
}
/**
* Sets the first matcher for the matching plan.
*
* Partial matchers will be run in the order in which they are added.
*
* @param InitialMatcherInterface $matcher
* An initial matcher. It is responsible for its own configuration and
* initial route collection
*
* @return NestedMatcherInterface
* The current matcher.
*/
public function setInitialMatcher(InitialMatcherInterface $initial) {
$this->initialMatcher = $initial;
return $this;
}
/**
* Tries to match a request with a set of routes.
*
* If the matcher can not find information, it must throw one of the exceptions documented
* below.
*
* @param Request $request The request to match
*
* @return array An array of parameters
*
* @throws ResourceNotFoundException If no matching resource could be found
* @throws MethodNotAllowedException If a matching resource was found but the request method is not allowed
*/
public function matchRequest(Request $request) {
$collection = $this->initialMatcher->matchRequestPartial($request);
foreach ($this->partialMatchers as $matcher) {
if ($collection) {
$matcher->setCollection($collection);
}
$collection = $matcher->matchRequestPartial($request);
}
$route = $this->finalMatcher->setCollection($collection)->matchRequest($request);
return $route;
}
/**
* Sets the request context.
*
* This method is unused. It is here only to satisfy the interface.
*
* @param RequestContext $context The context
*/
public function setContext(RequestContext $context) {
$this->context = $context;
}
/**
* Gets the request context.
*
* This method is unused. It is here only to satisfy the interface.
*
* @return RequestContext The context
*/
public function getContext() {
return $this->context;
}
}

View File

@ -2,12 +2,26 @@
namespace Drupal\Core\Routing;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
/**
* A NestedMatcher allows for multiple-stage resolution of a route.
*/
interface NestedMatcherInterface extends UrlMatcherInterface {
interface NestedMatcherInterface extends RequestMatcherInterface {
/**
* Sets the first matcher for the matching plan.
*
* Partial matchers will be run in the order in which they are added.
*
* @param InitialMatcherInterface $matcher
* An initial matcher. It is responsible for its own configuration and
* initial route collection
*
* @return NestedMatcherInterface
* The current matcher.
*/
public function setInitialMatcher(InitialMatcherInterface $initial);
/**
* Adds a partial matcher to the matching plan.
@ -15,7 +29,7 @@ interface NestedMatcherInterface extends UrlMatcherInterface {
* Partial matchers will be run in the order in which they are added.
*
* @param PartialMatcherInterface $matcher
* A partial
* A partial matcher.
*
* @return NestedMatcherInterface
* The current matcher.
@ -32,5 +46,5 @@ interface NestedMatcherInterface extends UrlMatcherInterface {
* @return NestedMatcherInterface
* The current matcher.
*/
public function setFinalMatcher(UrlMatcherInterface $final);
public function setFinalMatcher(FinalMatcherInterface $final);
}

View File

@ -3,12 +3,15 @@
namespace Drupal\Core\Routing;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RouteCollection;
/**
* A PartialMatcher works like a UrlMatcher, but will return multiple candidate routes.
*/
interface PartialMatcherInterface {
public function setCollection(RouteCollection $collection);
/**
* Matches a request against multiple routes.
*
@ -18,5 +21,5 @@ interface PartialMatcherInterface {
* @return RouteCollection
* A RouteCollection of matched routes.
*/
public function matchByRequest(Request $request);
public function matchRequestPartial(Request $request);
}

View File

@ -0,0 +1,57 @@
<?php
namespace Drupal\system\Tests\Routing;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use Drupal\Core\Routing\FinalMatcherInterface;
/**
* Mock final matcher for testing.
*
* This class simply matches the first remaining route.
*/
class MockFinalMatcher implements FinalMatcherInterface {
protected $routes;
public function setCollection(RouteCollection $collection) {
$this->routes = $collection;
return $this;
}
public function matchRequest(Request $request) {
// For testing purposes, just return whatever the first route is.
foreach ($this->routes as $name => $route) {
return array_merge($this->mergeDefaults(array(), $route->getDefaults()), array('_route' => $name));
}
}
/**
* Get merged default parameters.
*
* @param array $params
* The parameters
* @param array $defaults
* The defaults
*
* @return array
* Merged default parameters
*/
protected function mergeDefaults($params, $defaults) {
$parameters = $defaults;
foreach ($params as $key => $value) {
if (!is_int($key)) {
$parameters[$key] = $value;
}
}
return $parameters;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Drupal\system\Tests\Routing;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use Drupal\Core\Routing\InitialMatcherInterface;
/**
* Description of MockPathMatcher
*
* @author crell
*/
class MockPathMatcher implements InitialMatcherInterface {
protected $routes;
public function __construct(RouteCollection $routes) {
$this->routes = $routes;
}
/**
* 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) {
// For now for testing we'll just do a straight string match.
$path = $request->getPathInfo();
$return = new RouteCollection();
foreach ($this->routes as $name => $route) {
if ($route->getPattern() == $path) {
$return->add($name, $route);
}
}
return $return;
}
}

View File

@ -1,4 +1,5 @@
<?php
/**
* @file
* Definition of Drupal\system\Tests\Routing\PartialMatcherTest.
@ -12,6 +13,7 @@ use Symfony\Component\Routing\RouteCollection;
use Drupal\simpletest\UnitTestBase;
use Drupal\Core\Routing\HttpMethodMatcher;
use Drupal\Core\Routing\NestedMatcher;
/**
* Basic tests for the UrlMatcherDumper.
@ -25,14 +27,52 @@ class PartialMatcherTest extends UnitTestBase {
);
}
function setUp() {
public function setUp() {
parent::setUp();
}
/**
* Confirms that the HttpMethod matcher matches properly.
*/
function testFilterRoutes() {
public function testFilterRoutes() {
$matcher = new HttpMethodMatcher();
$matcher->setCollection($this->sampleRouteCollection());
$routes = $matcher->matchRequestPartial(Request::create('path/one', 'GET'));
$this->assertEqual(count($routes->all()), 4, t('The correct number of routes was found.'));
$this->assertNotNull($routes->get('route_a'), t('The first matching route was found.'));
$this->assertNull($routes->get('route_b'), t('The non-matching route was not found.'));
$this->assertNotNull($routes->get('route_c'), t('The second matching route was found.'));
$this->assertNotNull($routes->get('route_d'), t('The all-matching route was found.'));
$this->assertNotNull($routes->get('route_e'), t('The multi-matching route was found.'));
}
/**
* Confirms we can nest multiple partial matchers.
*/
public function testNestedMatcher() {
$matcher = new NestedMatcher();
$matcher->setInitialMatcher(new MockPathMatcher($this->sampleRouteCollection()));
$matcher->addPartialMatcher(new HttpMethodMatcher());
$matcher->setFinalMatcher(new MockFinalMatcher());
$request = Request::create('/path/one', 'GET');
$attributes = $matcher->matchRequest($request);
$this->assertEqual($attributes['_route'], 'route_a', t('The correct matching route was found.'));
}
/**
* Returns a standard set of routes for testing.
*
* @return \Symfony\Component\Routing\RouteCollection
*/
protected function sampleRouteCollection() {
$collection = new RouteCollection();
$route = new Route('path/one');
@ -54,16 +94,7 @@ class PartialMatcherTest extends UnitTestBase {
$route->setRequirement('_method', 'GET|HEAD');
$collection->add('route_e', $route);
$matcher = new HttpMethodMatcher($collection, 'GET');
$routes = $matcher->matchByRequest(Request::create('path/one', 'GET'));
$this->assertEqual(count($routes->all()), 4, t('The correct number of routes was found.'));
$this->assertNotNull($routes->get('route_a'), t('The first matching route was found.'));
$this->assertNull($routes->get('route_b'), t('The non-matching route was not found.'));
$this->assertNotNull($routes->get('route_c'), t('The second matching route was found.'));
$this->assertNotNull($routes->get('route_d'), t('The all-matching route was found.'));
$this->assertNotNull($routes->get('route_e'), t('The multi-matching route was found.'));
return $collection;
}
}