diff --git a/Candidates/Candidates.php b/Candidates/Candidates.php new file mode 100644 index 00000000..8a3fdcd1 --- /dev/null +++ b/Candidates/Candidates.php @@ -0,0 +1,155 @@ + + */ +class Candidates implements CandidatesInterface +{ + /** + * @var array + */ + protected $locales; + + /** + * A limit to apply to the number of candidates generated. + * + * This is to prevent abusive requests with a lot of "/". The limit is per + * batch, that is if a locale matches you could get as many as 2 * $limit + * candidates if the URL has that many slashes. + * + * @var int + */ + protected $limit; + + /** + * @param array $locales The locales to support. + * @param int $limit A limit to apply to the candidates generated. + */ + public function __construct(array $locales = array(), $limit = 20) + { + $this->setLocales($locales); + $this->limit = $limit; + } + + /** + * Set the locales to support by this strategy. + * + * @param array $locales The locales to support. + */ + public function setLocales(array $locales) + { + $this->locales = $locales; + } + + /** + * {@inheritDoc} + * + * Always returns true. + */ + public function isCandidate($name) + { + return true; + } + + /** + * {@inheritDoc} + * + * Does nothing. + */ + public function restrictQuery($queryBuilder) + { + } + + /** + * {@inheritDoc} + */ + public function getCandidates(Request $request) + { + $url = $request->getPathInfo(); + $candidates = $this->getCandidatesFor($url); + + $locale = $this->determineLocale($url); + if ($locale) { + $candidates = array_unique(array_merge($candidates, $this->getCandidatesFor(substr($url, strlen($locale) + 1)))); + } + + return $candidates; + } + + /** + * Determine the locale of this URL. + * + * @param string $url The url to determine the locale from. + * + * @return string|boolean The locale if $url starts with one of the allowed locales. + */ + protected function determineLocale($url) + { + if (!count($this->locales)) { + return false; + } + + $matches = array(); + if (preg_match('#(' . implode('|', $this->locales) . ')(/|$)#', $url, $matches)) { + return $matches[1]; + } + + return false; + } + + /** + * Handle a possible format extension and split the $url on "/". + * + * $prefix is prepended to every candidate generated. + * + * @param string $url The URL to split. + * @param string $prefix A prefix to prepend to every pattern. + * + * @return array Paths that could represent routes that match $url and are + * child of $prefix. + */ + protected function getCandidatesFor($url, $prefix = '') + { + $candidates = array(); + if ('/' !== $url) { + // handle format extension, like .html or .json + if (preg_match('/(.+)\.[a-z]+$/i', $url, $matches)) { + $candidates[] = $prefix . $url; + $url = $matches[1]; + } + + $part = $url; + $count = 0; + while (false !== ($pos = strrpos($part, '/'))) { + if (++$count > $this->limit) { + return $candidates; + } + $candidates[] = $prefix . $part; + $part = substr($url, 0, $pos); + } + } + + $candidates[] = $prefix ?: '/'; + + return $candidates; + } +} diff --git a/Candidates/CandidatesInterface.php b/Candidates/CandidatesInterface.php new file mode 100644 index 00000000..9ecae81b --- /dev/null +++ b/Candidates/CandidatesInterface.php @@ -0,0 +1,47 @@ + + */ +interface CandidatesInterface +{ + /** + * @param Request $request + * + * @return array a list of PHPCR-ODM ids + */ + function getCandidates(Request $request); + + /** + * Determine if $name is a valid candidate, e.g. in getRouteByName. + * + * @param string $name + * + * @return boolean + */ + function isCandidate($name); + + /** + * Provide a best effort query restriction to limit a query to only find + * routes that are supported. + * + * @param object $queryBuilder A query builder suited for the storage backend. + */ + function restrictQuery($queryBuilder); +} diff --git a/RouteProviderInterface.php b/RouteProviderInterface.php index f893b051..f843e870 100644 --- a/RouteProviderInterface.php +++ b/RouteProviderInterface.php @@ -13,6 +13,9 @@ namespace Symfony\Cmf\Component\Routing; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; /** * Interface for the route provider the DynamicRouter is using. @@ -41,7 +44,7 @@ interface RouteProviderInterface * * @param Request $request A request against which to match. * - * @return \Symfony\Component\Routing\RouteCollection with all Routes that + * @return RouteCollection with all Routes that * could potentially match $request. Empty collection if nothing can * match. */ @@ -50,12 +53,12 @@ public function getRouteCollectionForRequest(Request $request); /** * Find the route using the provided route name. * - * @param string $name the route name to fetch + * @param string $name The route name to fetch. * - * @return \Symfony\Component\Routing\Route + * @return Route * - * @throws \Symfony\Component\Routing\Exception\RouteNotFoundException if - * there is no route with that name in this repository + * @throws RouteNotFoundException If there is no route with that name in + * this repository */ public function getRouteByName($name); @@ -77,11 +80,11 @@ public function getRouteByName($name); * that the DynamicRouter will only call this method once per * DynamicRouter::getRouteCollection() call. * - * @param array|null $names the list of names to retrieve, in case of null - * the provider will determine what routes to return + * @param array|null $names The list of names to retrieve, In case of null, + * the provider will determine what routes to return. * - * @return \Symfony\Component\Routing\Route[] iteratable with the keys - * as names of the $names argument. + * @return Route[] Iterable list with the keys being the names from the + * $names array. */ public function getRoutesByNames($names); } diff --git a/Tests/Candidates/CandidatesTest.php b/Tests/Candidates/CandidatesTest.php new file mode 100644 index 00000000..7e4929b8 --- /dev/null +++ b/Tests/Candidates/CandidatesTest.php @@ -0,0 +1,109 @@ +assertTrue($candidates->isCandidate('/routes')); + $this->assertTrue($candidates->isCandidate('/routes/my/path')); + } + + /** + * Nothing should be called on the query builder + */ + public function testRestrictQuery() + { + $candidates = new Candidates(); + $candidates->restrictQuery(null); + } + + public function testGetCandidates() + { + $request = Request::create('/my/path.html'); + + $candidates = new Candidates(); + $paths = $candidates->getCandidates($request); + + $this->assertEquals( + array( + '/my/path.html', + '/my/path', + '/my', + '/', + ), + $paths + ); + } + + public function testGetCandidatesLocales() + { + $candidates = new Candidates(array('de', 'fr')); + + $request = Request::create('/fr/path.html'); + $paths = $candidates->getCandidates($request); + + $this->assertEquals( + array( + '/fr/path.html', + '/fr/path', + '/fr', + '/', + '/path.html', + '/path' + ), + $paths + ); + + $request = Request::create('/it/path.html'); + $paths = $candidates->getCandidates($request); + + $this->assertEquals( + array( + '/it/path.html', + '/it/path', + '/it', + '/', + ), + $paths + ); + } + + public function testGetCandidatesLimit() + { + $candidates = new Candidates(array(), 1); + + $request = Request::create('/my/path/is/deep.html'); + + $paths = $candidates->getCandidates($request); + + $this->assertEquals( + array( + '/my/path/is/deep.html', + '/my/path/is/deep', + ), + $paths + ); + + } +} diff --git a/Tests/Routing/ContentAwareGeneratorTest.php b/Tests/Routing/ContentAwareGeneratorTest.php index c33cd733..a6ae9f61 100644 --- a/Tests/Routing/ContentAwareGeneratorTest.php +++ b/Tests/Routing/ContentAwareGeneratorTest.php @@ -437,7 +437,7 @@ public function testGetRouteDebugMessage() */ class TestableContentAwareGenerator extends ContentAwareGenerator { - protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $absolute, $hostTokens = null) + protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, array $requiredSchemes = array()) { return 'result_url'; } diff --git a/Tests/Routing/DynamicRouterTest.php b/Tests/Routing/DynamicRouterTest.php index d6c875fc..ff277ce5 100644 --- a/Tests/Routing/DynamicRouterTest.php +++ b/Tests/Routing/DynamicRouterTest.php @@ -58,11 +58,19 @@ public function testContext() $this->assertSame($this->context, $this->router->getContext()); } - public function testRouteCollection() + public function testRouteCollectionEmpty() { $collection = $this->router->getRouteCollection(); $this->assertInstanceOf('Symfony\Component\Routing\RouteCollection', $collection); - // TODO: once this is implemented, check content of collection + } + + public function testRouteCollectionLazy() + { + $provider = $this->getMock('Symfony\Cmf\Component\Routing\RouteProviderInterface'); + $router = new DynamicRouter($this->context, $this->matcher, $this->generator, '', null, $provider); + + $collection = $router->getRouteCollection(); + $this->assertInstanceOf('Symfony\Cmf\Component\Routing\LazyRouteCollection', $collection); } /// generator tests /// diff --git a/Tests/Routing/ProviderBasedGeneratorTest.php b/Tests/Routing/ProviderBasedGeneratorTest.php index af7b461e..a7f8708f 100644 --- a/Tests/Routing/ProviderBasedGeneratorTest.php +++ b/Tests/Routing/ProviderBasedGeneratorTest.php @@ -130,7 +130,7 @@ public function testGenerateByRoute() */ class TestableProviderBasedGenerator extends ProviderBasedGenerator { - protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $absolute, $hostTokens = null) + protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, array $requiredSchemes = array()) { return 'result_url'; }