Skip to content

Commit 6fb75b2

Browse files
committed
Merge pull request #98 from symfony-cmf/candidates
adding candidates subsystem
2 parents 1a41473 + 8f9db0c commit 6fb75b2

File tree

7 files changed

+335
-13
lines changed

7 files changed

+335
-13
lines changed

Candidates/Candidates.php

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony CMF package.
5+
*
6+
* (c) 2011-2014 Symfony CMF
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Cmf\Component\Routing\Candidates;
13+
14+
use Doctrine\ODM\PHPCR\DocumentManager;
15+
use Symfony\Component\HttpFoundation\Request;
16+
17+
/**
18+
* A straightforward strategy that splits the URL on "/".
19+
*
20+
* If locales is set, additionally generates candidates removing the locale if
21+
* it is one of the configured locales, for non-locale specific URLs.
22+
*
23+
* @author David Buchmann <[email protected]>
24+
*/
25+
class Candidates implements CandidatesInterface
26+
{
27+
/**
28+
* @var array
29+
*/
30+
protected $locales;
31+
32+
/**
33+
* A limit to apply to the number of candidates generated.
34+
*
35+
* This is to prevent abusive requests with a lot of "/". The limit is per
36+
* batch, that is if a locale matches you could get as many as 2 * $limit
37+
* candidates if the URL has that many slashes.
38+
*
39+
* @var int
40+
*/
41+
protected $limit;
42+
43+
/**
44+
* @param array $locales The locales to support.
45+
* @param int $limit A limit to apply to the candidates generated.
46+
*/
47+
public function __construct(array $locales = array(), $limit = 20)
48+
{
49+
$this->setLocales($locales);
50+
$this->limit = $limit;
51+
}
52+
53+
/**
54+
* Set the locales to support by this strategy.
55+
*
56+
* @param array $locales The locales to support.
57+
*/
58+
public function setLocales(array $locales)
59+
{
60+
$this->locales = $locales;
61+
}
62+
63+
/**
64+
* {@inheritDoc}
65+
*
66+
* Always returns true.
67+
*/
68+
public function isCandidate($name)
69+
{
70+
return true;
71+
}
72+
73+
/**
74+
* {@inheritDoc}
75+
*
76+
* Does nothing.
77+
*/
78+
public function restrictQuery($queryBuilder)
79+
{
80+
}
81+
82+
/**
83+
* {@inheritDoc}
84+
*/
85+
public function getCandidates(Request $request)
86+
{
87+
$url = $request->getPathInfo();
88+
$candidates = $this->getCandidatesFor($url);
89+
90+
$locale = $this->determineLocale($url);
91+
if ($locale) {
92+
$candidates = array_unique(array_merge($candidates, $this->getCandidatesFor(substr($url, strlen($locale) + 1))));
93+
}
94+
95+
return $candidates;
96+
}
97+
98+
/**
99+
* Determine the locale of this URL.
100+
*
101+
* @param string $url The url to determine the locale from.
102+
*
103+
* @return string|boolean The locale if $url starts with one of the allowed locales.
104+
*/
105+
protected function determineLocale($url)
106+
{
107+
if (!count($this->locales)) {
108+
return false;
109+
}
110+
111+
$matches = array();
112+
if (preg_match('#(' . implode('|', $this->locales) . ')(/|$)#', $url, $matches)) {
113+
return $matches[1];
114+
}
115+
116+
return false;
117+
}
118+
119+
/**
120+
* Handle a possible format extension and split the $url on "/".
121+
*
122+
* $prefix is prepended to every candidate generated.
123+
*
124+
* @param string $url The URL to split.
125+
* @param string $prefix A prefix to prepend to every pattern.
126+
*
127+
* @return array Paths that could represent routes that match $url and are
128+
* child of $prefix.
129+
*/
130+
protected function getCandidatesFor($url, $prefix = '')
131+
{
132+
$candidates = array();
133+
if ('/' !== $url) {
134+
// handle format extension, like .html or .json
135+
if (preg_match('/(.+)\.[a-z]+$/i', $url, $matches)) {
136+
$candidates[] = $prefix . $url;
137+
$url = $matches[1];
138+
}
139+
140+
$part = $url;
141+
$count = 0;
142+
while (false !== ($pos = strrpos($part, '/'))) {
143+
if (++$count > $this->limit) {
144+
return $candidates;
145+
}
146+
$candidates[] = $prefix . $part;
147+
$part = substr($url, 0, $pos);
148+
}
149+
}
150+
151+
$candidates[] = $prefix ?: '/';
152+
153+
return $candidates;
154+
}
155+
}

Candidates/CandidatesInterface.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony CMF package.
5+
*
6+
* (c) 2011-2014 Symfony CMF
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Cmf\Component\Routing\Candidates;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
16+
/**
17+
* Candidates is a subsystem useful for the route provider. It separates the
18+
* logic for determining possible static prefixes from the route provider.
19+
*
20+
* @author David Buchmann <[email protected]>
21+
*/
22+
interface CandidatesInterface
23+
{
24+
/**
25+
* @param Request $request
26+
*
27+
* @return array a list of PHPCR-ODM ids
28+
*/
29+
function getCandidates(Request $request);
30+
31+
/**
32+
* Determine if $name is a valid candidate, e.g. in getRouteByName.
33+
*
34+
* @param string $name
35+
*
36+
* @return boolean
37+
*/
38+
function isCandidate($name);
39+
40+
/**
41+
* Provide a best effort query restriction to limit a query to only find
42+
* routes that are supported.
43+
*
44+
* @param object $queryBuilder A query builder suited for the storage backend.
45+
*/
46+
function restrictQuery($queryBuilder);
47+
}

RouteProviderInterface.php

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
namespace Symfony\Cmf\Component\Routing;
1414

1515
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\Routing\Exception\RouteNotFoundException;
17+
use Symfony\Component\Routing\Route;
18+
use Symfony\Component\Routing\RouteCollection;
1619

1720
/**
1821
* Interface for the route provider the DynamicRouter is using.
@@ -41,7 +44,7 @@ interface RouteProviderInterface
4144
*
4245
* @param Request $request A request against which to match.
4346
*
44-
* @return \Symfony\Component\Routing\RouteCollection with all Routes that
47+
* @return RouteCollection with all Routes that
4548
* could potentially match $request. Empty collection if nothing can
4649
* match.
4750
*/
@@ -50,12 +53,12 @@ public function getRouteCollectionForRequest(Request $request);
5053
/**
5154
* Find the route using the provided route name.
5255
*
53-
* @param string $name the route name to fetch
56+
* @param string $name The route name to fetch.
5457
*
55-
* @return \Symfony\Component\Routing\Route
58+
* @return Route
5659
*
57-
* @throws \Symfony\Component\Routing\Exception\RouteNotFoundException if
58-
* there is no route with that name in this repository
60+
* @throws RouteNotFoundException If there is no route with that name in
61+
* this repository
5962
*/
6063
public function getRouteByName($name);
6164

@@ -77,11 +80,11 @@ public function getRouteByName($name);
7780
* that the DynamicRouter will only call this method once per
7881
* DynamicRouter::getRouteCollection() call.
7982
*
80-
* @param array|null $names the list of names to retrieve, in case of null
81-
* the provider will determine what routes to return
83+
* @param array|null $names The list of names to retrieve, In case of null,
84+
* the provider will determine what routes to return.
8285
*
83-
* @return \Symfony\Component\Routing\Route[] iteratable with the keys
84-
* as names of the $names argument.
86+
* @return Route[] Iterable list with the keys being the names from the
87+
* $names array.
8588
*/
8689
public function getRoutesByNames($names);
8790
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony CMF package.
5+
*
6+
* (c) 2011-2013 Symfony CMF
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Cmf\Component\Routing\Tests\Candidates;
13+
14+
use Doctrine\Common\Collections\ArrayCollection;
15+
use Symfony\Cmf\Component\Routing\Candidates\Candidates;
16+
use Symfony\Component\HttpFoundation\Request;
17+
use Symfony\Component\Routing\Route;
18+
use Symfony\Component\Routing\RouteCollection;
19+
20+
class CandidatesTest extends \PHPUnit_Framework_Testcase
21+
{
22+
/**
23+
* Everything is a candidate
24+
*/
25+
public function testIsCandidate()
26+
{
27+
$candidates = new Candidates();
28+
$this->assertTrue($candidates->isCandidate('/routes'));
29+
$this->assertTrue($candidates->isCandidate('/routes/my/path'));
30+
}
31+
32+
/**
33+
* Nothing should be called on the query builder
34+
*/
35+
public function testRestrictQuery()
36+
{
37+
$candidates = new Candidates();
38+
$candidates->restrictQuery(null);
39+
}
40+
41+
public function testGetCandidates()
42+
{
43+
$request = Request::create('/my/path.html');
44+
45+
$candidates = new Candidates();
46+
$paths = $candidates->getCandidates($request);
47+
48+
$this->assertEquals(
49+
array(
50+
'/my/path.html',
51+
'/my/path',
52+
'/my',
53+
'/',
54+
),
55+
$paths
56+
);
57+
}
58+
59+
public function testGetCandidatesLocales()
60+
{
61+
$candidates = new Candidates(array('de', 'fr'));
62+
63+
$request = Request::create('/fr/path.html');
64+
$paths = $candidates->getCandidates($request);
65+
66+
$this->assertEquals(
67+
array(
68+
'/fr/path.html',
69+
'/fr/path',
70+
'/fr',
71+
'/',
72+
'/path.html',
73+
'/path'
74+
),
75+
$paths
76+
);
77+
78+
$request = Request::create('/it/path.html');
79+
$paths = $candidates->getCandidates($request);
80+
81+
$this->assertEquals(
82+
array(
83+
'/it/path.html',
84+
'/it/path',
85+
'/it',
86+
'/',
87+
),
88+
$paths
89+
);
90+
}
91+
92+
public function testGetCandidatesLimit()
93+
{
94+
$candidates = new Candidates(array(), 1);
95+
96+
$request = Request::create('/my/path/is/deep.html');
97+
98+
$paths = $candidates->getCandidates($request);
99+
100+
$this->assertEquals(
101+
array(
102+
'/my/path/is/deep.html',
103+
'/my/path/is/deep',
104+
),
105+
$paths
106+
);
107+
108+
}
109+
}

Tests/Routing/ContentAwareGeneratorTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ public function testGetRouteDebugMessage()
437437
*/
438438
class TestableContentAwareGenerator extends ContentAwareGenerator
439439
{
440-
protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $absolute, $hostTokens = null)
440+
protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, array $requiredSchemes = array())
441441
{
442442
return 'result_url';
443443
}

Tests/Routing/DynamicRouterTest.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,19 @@ public function testContext()
5858
$this->assertSame($this->context, $this->router->getContext());
5959
}
6060

61-
public function testRouteCollection()
61+
public function testRouteCollectionEmpty()
6262
{
6363
$collection = $this->router->getRouteCollection();
6464
$this->assertInstanceOf('Symfony\Component\Routing\RouteCollection', $collection);
65-
// TODO: once this is implemented, check content of collection
65+
}
66+
67+
public function testRouteCollectionLazy()
68+
{
69+
$provider = $this->getMock('Symfony\Cmf\Component\Routing\RouteProviderInterface');
70+
$router = new DynamicRouter($this->context, $this->matcher, $this->generator, '', null, $provider);
71+
72+
$collection = $router->getRouteCollection();
73+
$this->assertInstanceOf('Symfony\Cmf\Component\Routing\LazyRouteCollection', $collection);
6674
}
6775

6876
/// generator tests ///

0 commit comments

Comments
 (0)