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
parent
b0f90a1046
commit
806ff4acc8
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue