From 402359ba9d65c63a7191949e05b231a7dd51fca6 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Sat, 14 Apr 2012 19:15:20 +0200 Subject: [PATCH] [Routing] added hostname matching support to RouteCompiler --- .../Component/Routing/RouteCompiler.php | 77 ++++++++++++++++--- .../Routing/Tests/RouteCompilerTest.php | 74 ++++++++++++++++++ 2 files changed, 139 insertions(+), 12 deletions(-) diff --git a/src/Symfony/Component/Routing/RouteCompiler.php b/src/Symfony/Component/Routing/RouteCompiler.php index 03d3e38644..418303d138 100644 --- a/src/Symfony/Component/Routing/RouteCompiler.php +++ b/src/Symfony/Component/Routing/RouteCompiler.php @@ -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, ); } diff --git a/src/Symfony/Component/Routing/Tests/RouteCompilerTest.php b/src/Symfony/Component/Routing/Tests/RouteCompilerTest.php index 1c87ca3257..d8827df58b 100644 --- a/src/Symfony/Component/Routing/Tests/RouteCompilerTest.php +++ b/src/Symfony/Component/Routing/Tests/RouteCompilerTest.php @@ -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/(?[^/]++)$#s', array('tld', 'name'), array('name'), array( + array('variable', '/', '[^/]++', 'name'), + array('text', '/hello'), + ), + '#^www\.example\.(?[^\.]++)$#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'), + ), + '#^(?[^\.]++)\.example\.(?[^\.]++)$#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'), + ), + '#^(?[^\.]++)\.example\.(?[^\.]++)$#s', array('locale', 'tld'), array( + array('variable', '.', '[^\.]++', 'tld'), + array('text', '.example'), + array('variable', '', '[^\.]++', 'locale'), + ), + ), + ); + } } +