Skip to content

Commit

Permalink
Merge pull request #98 from symfony-cmf/candidates
Browse files Browse the repository at this point in the history
adding candidates subsystem
  • Loading branch information
dbu committed Apr 1, 2014
2 parents 1a41473 + 8f9db0c commit 6fb75b2
Show file tree
Hide file tree
Showing 7 changed files with 335 additions and 13 deletions.
155 changes: 155 additions & 0 deletions Candidates/Candidates.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<?php

/*
* This file is part of the Symfony CMF package.
*
* (c) 2011-2014 Symfony CMF
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Cmf\Component\Routing\Candidates;

use Doctrine\ODM\PHPCR\DocumentManager;
use Symfony\Component\HttpFoundation\Request;

/**
* A straightforward strategy that splits the URL on "/".
*
* If locales is set, additionally generates candidates removing the locale if
* it is one of the configured locales, for non-locale specific URLs.
*
* @author David Buchmann <[email protected]>
*/
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;
}
}
47 changes: 47 additions & 0 deletions Candidates/CandidatesInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

/*
* This file is part of the Symfony CMF package.
*
* (c) 2011-2014 Symfony CMF
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Cmf\Component\Routing\Candidates;

use Symfony\Component\HttpFoundation\Request;

/**
* Candidates is a subsystem useful for the route provider. It separates the
* logic for determining possible static prefixes from the route provider.
*
* @author David Buchmann <[email protected]>
*/
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);
}
21 changes: 12 additions & 9 deletions RouteProviderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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);

Expand All @@ -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);
}
109 changes: 109 additions & 0 deletions Tests/Candidates/CandidatesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

/*
* This file is part of the Symfony CMF package.
*
* (c) 2011-2013 Symfony CMF
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Cmf\Component\Routing\Tests\Candidates;

use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Cmf\Component\Routing\Candidates\Candidates;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

class CandidatesTest extends \PHPUnit_Framework_Testcase
{
/**
* Everything is a candidate
*/
public function testIsCandidate()
{
$candidates = new Candidates();
$this->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
);

}
}
2 changes: 1 addition & 1 deletion Tests/Routing/ContentAwareGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Expand Down
12 changes: 10 additions & 2 deletions Tests/Routing/DynamicRouterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ///
Expand Down
Loading

0 comments on commit 6fb75b2

Please sign in to comment.