[Routing] added hostname matching support to RouteCompiler

This commit is contained in:
Arnaud Le Blanc 2012-04-14 19:15:20 +02:00
parent add3658001
commit 402359ba9d
2 changed files with 139 additions and 12 deletions

View File

@ -37,11 +37,58 @@ class RouteCompiler implements RouteCompilerInterface
*/
public function compile(Route $route)
{
$staticPrefix = null;
$hostnameVariables = array();
$pathVariables = array();
$variables = array();
$tokens = array();
$regex = null;
$hostnameRegex = null;
$hostnameTokens = array();
if (null !== $hostnamePattern = $route->getHostnamePattern()) {
$result = $this->compilePattern($route, $hostnamePattern, true);
$hostnameVariables = $result['variables'];
$variables = array_merge($variables, $hostnameVariables);
$hostnameTokens = $result['tokens'];
$hostnameRegex = $result['regex'];
}
$pattern = $route->getPattern();
$result = $this->compilePattern($route, $pattern, false);
$staticPrefix = $result['staticPrefix'];
$pathVariables = $result['variables'];
$variables = array_merge($variables, $pathVariables);
$tokens = $result['tokens'];
$regex = $result['regex'];
return new CompiledRoute(
$staticPrefix,
$regex,
$tokens,
array_unique($variables),
$pathVariables,
$hostnameVariables,
$hostnameRegex,
$hostnameTokens
);
}
private function compilePattern(Route $route, $pattern, $isHostname)
{
$len = strlen($pattern);
$tokens = array();
$variables = array();
$matches = array();
$pos = 0;
$defaultSeparator = $isHostname ? '.' : '/';
// Match all variables enclosed in "{}" and iterate over them. But we only want to match the innermost variable
// in case of nested "{}", e.g. {foo{bar}}. This in ensured because \w does not match "{" or "}" itself.
@ -78,7 +125,11 @@ class RouteCompiler implements RouteCompilerInterface
// Also even if {_format} was not optional the requirement prevents that {page} matches something that was originally
// part of {_format} when generating the URL, e.g. _format = 'mobile.html'.
$nextSeparator = $this->findNextSeparator($followingPattern);
$regexp = sprintf('[^/%s]+', '/' !== $nextSeparator && '' !== $nextSeparator ? preg_quote($nextSeparator, self::REGEX_DELIMITER) : '');
$regexp = sprintf(
'[^%s%s]+',
preg_quote($defaultSeparator, self::REGEX_DELIMITER),
$defaultSeparator !== $nextSeparator && '' !== $nextSeparator ? preg_quote($nextSeparator, self::REGEX_DELIMITER) : ''
);
if (('' !== $nextSeparator && !preg_match('#^\{\w+\}#', $followingPattern)) || '' === $followingPattern) {
// When we have a separator, which is disallowed for the variable, we can optimize the regex with a possessive
// quantifier. This prevents useless backtracking of PCRE and improves performance by 20% for matching those patterns.
@ -99,12 +150,14 @@ class RouteCompiler implements RouteCompilerInterface
// find the first optional token
$firstOptional = INF;
for ($i = count($tokens) - 1; $i >= 0; $i--) {
$token = $tokens[$i];
if ('variable' === $token[0] && $route->hasDefault($token[3])) {
$firstOptional = $i;
} else {
break;
if (!$isHostname) {
for ($i = count($tokens) - 1; $i >= 0; $i--) {
$token = $tokens[$i];
if ('variable' === $token[0] && $route->hasDefault($token[3])) {
$firstOptional = $i;
} else {
break;
}
}
}
@ -114,11 +167,11 @@ class RouteCompiler implements RouteCompilerInterface
$regexp .= $this->computeRegexp($tokens, $i, $firstOptional);
}
return new CompiledRoute(
'text' === $tokens[0][0] ? $tokens[0][1] : '',
self::REGEX_DELIMITER.'^'.$regexp.'$'.self::REGEX_DELIMITER.'s',
array_reverse($tokens),
$variables
return array(
'staticPrefix' => 'text' === $tokens[0][0] ? $tokens[0][1] : '',
'regex' => self::REGEX_DELIMITER.'^'.$regexp.'$'.self::REGEX_DELIMITER.'s',
'tokens' => array_reverse($tokens),
'variables' => $variables,
);
}

View File

@ -179,4 +179,78 @@ class RouteCompilerTest extends \PHPUnit_Framework_TestCase
array('1e2')
);
}
/**
* @dataProvider provideCompileWithHostnameData
*/
public function testCompileWithHostname($name, $arguments, $prefix, $regex, $variables, $pathVariables, $tokens, $hostnameRegex, $hostnameVariables, $hostnameTokens)
{
$r = new \ReflectionClass('Symfony\\Component\\Routing\\Route');
$route = $r->newInstanceArgs($arguments);
$compiled = $route->compile();
$this->assertEquals($prefix, $compiled->getStaticPrefix(), $name.' (static prefix)');
$this->assertEquals($regex, str_replace(array("\n", ' '), '', $compiled->getRegex()), $name.' (regex)');
$this->assertEquals($variables, $compiled->getVariables(), $name.' (variables)');
$this->assertEquals($pathVariables, $compiled->getPathVariables(), $name.' (path variables)');
$this->assertEquals($tokens, $compiled->getTokens(), $name.' (tokens)');
$this->assertEquals($hostnameRegex, str_replace(array("\n", ' '), '', $compiled->getHostnameRegex()), $name.' (hostname regex)');
$this->assertEquals($hostnameVariables, $compiled->getHostnameVariables(), $name.' (hostname variables)');
$this->assertEquals($hostnameTokens, $compiled->getHostnameTokens(), $name.' (hostname tokens)');
}
public function provideCompileWithHostnameData()
{
return array(
array(
'Route with hostname pattern',
array('/hello', array(), array(), array(), 'www.example.com'),
'/hello', '#^/hello$#s', array(), array(), array(
array('text', '/hello'),
),
'#^www\.example\.com$#s', array(), array(
array('text', 'www.example.com'),
),
),
array(
'Route with hostname pattern and some variables',
array('/hello/{name}', array(), array(), array(), 'www.example.{tld}'),
'/hello', '#^/hello/(?<name>[^/]++)$#s', array('tld', 'name'), array('name'), array(
array('variable', '/', '[^/]++', 'name'),
array('text', '/hello'),
),
'#^www\.example\.(?<tld>[^\.]++)$#s', array('tld'), array(
array('variable', '.', '[^\.]++', 'tld'),
array('text', 'www.example'),
),
),
array(
'Route with variable at begining of hostname',
array('/hello', array(), array(), array(), '{locale}.example.{tld}'),
'/hello', '#^/hello$#s', array('locale', 'tld'), array(), array(
array('text', '/hello'),
),
'#^(?<locale>[^\.]++)\.example\.(?<tld>[^\.]++)$#s', array('locale', 'tld'), array(
array('variable', '.', '[^\.]++', 'tld'),
array('text', '.example'),
array('variable', '', '[^\.]++', 'locale'),
),
),
array(
'Route with hostname variables that has a default value',
array('/hello', array('locale' => 'a', 'tld' => 'b'), array(), array(), '{locale}.example.{tld}'),
'/hello', '#^/hello$#s', array('locale', 'tld'), array(), array(
array('text', '/hello'),
),
'#^(?<locale>[^\.]++)\.example\.(?<tld>[^\.]++)$#s', array('locale', 'tld'), array(
array('variable', '.', '[^\.]++', 'tld'),
array('text', '.example'),
array('variable', '', '[^\.]++', 'locale'),
),
),
);
}
}