Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 190ccf8

Browse files
committed
merged branch Tobion/compiler (PR #4225)
This PR was merged into the master branch. Commits ------- 005a9a3 [Routing] fixed RouteCompiler for adjacent and nested placeholders Discussion ---------- [2.2] [Routing] fixed RouteCompiler for adjacent placeholders Tests pass: yes Feature addition: no Fixes: #4215 BC break: no - Nesting placeholders works now properly, e.g. `/{foo{bar}}`. Only `bar` is a variable, the rest is static text. - Variables without separator work now correctly too, e.g. `/{w}{x}{y}{z}.{_format}`. All variables will have the correct default regex in the current manner, that is exclude the previous static char and the next static char (which means disregarding the placeholder in between that have no seperator). As you can see, I have not modified any tests, only added some. So this change is BC. The compiler now works under all conditions and does not fail for such patterns. --------------------------------------------------------------------------- by Tobion at 2012-05-08T22:34:58Z ready for review / merging Thanks @snc for giving a helpful tip. --------------------------------------------------------------------------- by Tobion at 2012-06-12T23:22:54Z fabpot told me, he doesn't like PRs that both fix and enhance stuff. So I reworked this PR so that it only contains the bug fixes. The new PR #4563 contains the feature addition. --------------------------------------------------------------------------- by Tobion at 2012-06-26T12:33:43Z ping @fabpot --------------------------------------------------------------------------- by Tobion at 2012-08-14T16:29:27Z Why wait for 2.2? It's a bugfix only, without BC break. --------------------------------------------------------------------------- by Tobion at 2012-08-31T17:04:37Z @fabpot I think this should go into 2.1
2 parents b2dd04d + 005a9a3 commit 190ccf8

File tree

4 files changed

+104
-26
lines changed

4 files changed

+104
-26
lines changed

src/Symfony/Component/Routing/RouteCompiler.php

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* RouteCompiler compiles Route instances to CompiledRoute instances.
1616
*
1717
* @author Fabien Potencier <[email protected]>
18+
* @author Tobias Schultze <http://tobion.de>
1819
*/
1920
class RouteCompiler implements RouteCompilerInterface
2021
{
@@ -30,45 +31,50 @@ class RouteCompiler implements RouteCompilerInterface
3031
public function compile(Route $route)
3132
{
3233
$pattern = $route->getPattern();
33-
$len = strlen($pattern);
3434
$tokens = array();
3535
$variables = array();
36+
$matches = array();
3637
$pos = 0;
37-
preg_match_all('#.\{(\w+)\}#', $pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
38-
foreach ($matches as $match) {
39-
if ($text = substr($pattern, $pos, $match[0][1] - $pos)) {
40-
$tokens[] = array('text', $text);
41-
}
38+
$lastSeparator = '';
4239

40+
// Match all variables enclosed in "{}" and iterate over them. But we only want to match the innermost variable
41+
// in case of nested "{}", e.g. {foo{bar}}. This in ensured because \w does not match "{" or "}" itself.
42+
preg_match_all('#\{\w+\}#', $pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
43+
foreach ($matches as $match) {
44+
$varName = substr($match[0][0], 1, -1);
45+
// get all static text preceding the current variable
46+
$precedingText = substr($pattern, $pos, $match[0][1] - $pos);
4347
$pos = $match[0][1] + strlen($match[0][0]);
44-
$var = $match[1][0];
45-
46-
if (null !== $req = $route->getRequirement($var)) {
47-
$regexp = $req;
48-
} else {
49-
// Use the character preceding the variable as a separator
50-
$separators = array($match[0][0][0]);
48+
$precedingChar = strlen($precedingText) > 0 ? substr($precedingText, -1) : '';
5149

52-
if ($pos !== $len) {
53-
// Use the character following the variable as the separator when available
54-
$separators[] = $pattern[$pos];
55-
}
56-
$regexp = sprintf('[^%s]+', preg_quote(implode('', array_unique($separators)), self::REGEX_DELIMITER));
50+
if (is_numeric($varName)) {
51+
throw new \DomainException(sprintf('Variable name "%s" cannot be numeric in route pattern "%s". Please use a different name.', $varName, $pattern));
52+
}
53+
if (in_array($varName, $variables)) {
54+
throw new \LogicException(sprintf('Route pattern "%s" cannot reference variable name "%s" more than once.', $pattern, $varName));
5755
}
5856

59-
$tokens[] = array('variable', $match[0][0][0], $regexp, $var);
60-
61-
if (is_numeric($var)) {
62-
throw new \DomainException(sprintf('Variable name "%s" cannot be numeric in route pattern "%s". Please use a different name.', $var, $route->getPattern()));
57+
if (strlen($precedingText) > 1) {
58+
$tokens[] = array('text', substr($precedingText, 0, -1));
6359
}
64-
if (in_array($var, $variables)) {
65-
throw new \LogicException(sprintf('Route pattern "%s" cannot reference variable name "%s" more than once.', $route->getPattern(), $var));
60+
// use the character preceding the variable as a separator
61+
// save it for later use as default separator for variables that follow directly without having a preceding char e.g. "/{x}{y}"
62+
if ('' !== $precedingChar) {
63+
$lastSeparator = $precedingChar;
64+
}
65+
66+
$regexp = $route->getRequirement($varName);
67+
if (null === $regexp) {
68+
// use the character following the variable (ignoring other placeholders) as a separator when it's not the same as the preceding separator
69+
$nextSeparator = $this->findNextSeparator(substr($pattern, $pos));
70+
$regexp = sprintf('[^%s]+', preg_quote($lastSeparator !== $nextSeparator ? $lastSeparator.$nextSeparator : $lastSeparator, self::REGEX_DELIMITER));
6671
}
6772

68-
$variables[] = $var;
73+
$tokens[] = array('variable', $precedingChar, $regexp, $varName);
74+
$variables[] = $varName;
6975
}
7076

71-
if ($pos < $len) {
77+
if ($pos < strlen($pattern)) {
7278
$tokens[] = array('text', substr($pattern, $pos));
7379
}
7480

@@ -97,6 +103,25 @@ public function compile(Route $route)
97103
);
98104
}
99105

106+
/**
107+
* Returns the next static character in the Route pattern that will serve as a separator.
108+
*
109+
* @param string $pattern The route pattern
110+
*
111+
* @return string The next static character (or empty string when none available)
112+
*/
113+
private function findNextSeparator($pattern)
114+
{
115+
if ('' == $pattern) {
116+
// return empty string if pattern is empty or false (false which can be returned by substr)
117+
return '';
118+
}
119+
// first remove all placeholders from the pattern so we can find the next real static character
120+
$pattern = preg_replace('#\{\w+\}#', '', $pattern);
121+
122+
return isset($pattern[0]) ? $pattern[0] : '';
123+
}
124+
100125
/**
101126
* Computes the regexp used to match a specific token. It can be static text or a subpattern.
102127
*

src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,19 @@ public function testEncodingOfRelativePathSegments()
255255
$this->assertSame('/app.php/a./.a/a../..a/...', $this->getGenerator($routes)->generate('test'));
256256
}
257257

258+
public function testAdjacentVariables()
259+
{
260+
$routes = $this->getRoutes('test', new Route('/{x}{y}{z}.{_format}', array('z' => 'default-z', '_format' => 'html'), array('y' => '\d+')));
261+
$generator = $this->getGenerator($routes);
262+
$this->assertSame('/app.php/foo123', $generator->generate('test', array('x' => 'foo', 'y' => '123')));
263+
$this->assertSame('/app.php/foo123bar.xml', $generator->generate('test', array('x' => 'foo', 'y' => '123', 'z' => 'bar', '_format' => 'xml')));
264+
265+
// The default requirement for 'x' should not allow the separator '.' in this case because it would otherwise match everything
266+
// and following optional variables like _format could never match.
267+
$this->setExpectedException('Symfony\Component\Routing\Exception\InvalidParameterException');
268+
$generator->generate('test', array('x' => 'do.t', 'y' => '123', 'z' => 'bar', '_format' => 'xml'));
269+
}
270+
258271
protected function getGenerator(RouteCollection $routes, array $parameters = array(), $logger = null)
259272
{
260273
$context = new RequestContext('/app.php');

src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,26 @@ public function testMatchingIsEager()
232232
$this->assertEquals(array('foo' => 'text1-text2-text3', 'bar' => 'text4', '_route' => 'test'), $matcher->match('/text1-text2-text3-text4-'));
233233
}
234234

235+
public function testAdjacentVariables()
236+
{
237+
$coll = new RouteCollection();
238+
$coll->add('test', new Route('/{w}{x}{y}{z}.{_format}', array('z' => 'default-z', '_format' => 'html'), array('y' => 'y|Y')));
239+
240+
$matcher = new UrlMatcher($coll, new RequestContext());
241+
// 'w' eagerly matches as much as possible and the other variables match the remaining chars.
242+
// This also shows that the variables w-z must all exclude the separating char (the dot '.' in this case) by default requirement.
243+
// Otherwise they would also comsume '.xml' and _format would never match as it's an optional variable.
244+
$this->assertEquals(array('w' => 'wwwww', 'x' => 'x', 'y' => 'Y', 'z' => 'Z','_format' => 'xml', '_route' => 'test'), $matcher->match('/wwwwwxYZ.xml'));
245+
// As 'y' has custom requirement and can only be of value 'y|Y', it will leave 'ZZZ' to variable z.
246+
// So with carefully chosen requirements adjacent variables, can be useful.
247+
$this->assertEquals(array('w' => 'wwwww', 'x' => 'x', 'y' => 'y', 'z' => 'ZZZ','_format' => 'html', '_route' => 'test'), $matcher->match('/wwwwwxyZZZ'));
248+
// z and _format are optional.
249+
$this->assertEquals(array('w' => 'wwwww', 'x' => 'x', 'y' => 'y', 'z' => 'default-z','_format' => 'html', '_route' => 'test'), $matcher->match('/wwwwwxy'));
250+
251+
$this->setExpectedException('Symfony\Component\Routing\Exception\ResourceNotFoundException');
252+
$matcher->match('/wxy.html');
253+
}
254+
235255
/**
236256
* @expectedException Symfony\Component\Routing\Exception\ResourceNotFoundException
237257
*/

src/Symfony/Component/Routing/Tests/RouteCompilerTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,26 @@ public function provideCompileData()
120120
array('text', '/foo'),
121121
)),
122122

123+
array(
124+
'Route with nested placeholders',
125+
array('/{static{var}static}'),
126+
'/{stati', '#^/\{static(?<var>[^cs]+)static\}$#s', array('var'), array(
127+
array('text', 'static}'),
128+
array('variable', 'c', '[^cs]+', 'var'),
129+
array('text', '/{stati'),
130+
)),
131+
132+
array(
133+
'Route without separator between variables',
134+
array('/{w}{x}{y}{z}.{_format}', array('z' => 'default-z', '_format' => 'html'), array('y' => '(y|Y)')),
135+
'', '#^/(?<w>[^/\.]+)(?<x>[^/\.]+)(?<y>(y|Y))(?:(?<z>[^/\.]+)(?:\.(?<_format>[^\.]+))?)?$#s', array('w', 'x', 'y', 'z', '_format'), array(
136+
array('variable', '.', '[^\.]+', '_format'),
137+
array('variable', '', '[^/\.]+', 'z'),
138+
array('variable', '', '(y|Y)', 'y'),
139+
array('variable', '', '[^/\.]+', 'x'),
140+
array('variable', '/', '[^/\.]+', 'w'),
141+
)),
142+
123143
array(
124144
'Route with a format',
125145
array('/foo/{bar}.{_format}'),

0 commit comments

Comments
 (0)