Skip to content

Commit 402359b

Browse files
committed
[Routing] added hostname matching support to RouteCompiler
1 parent add3658 commit 402359b

File tree

2 files changed

+139
-12
lines changed

2 files changed

+139
-12
lines changed

src/Symfony/Component/Routing/RouteCompiler.php

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,58 @@ class RouteCompiler implements RouteCompilerInterface
3737
*/
3838
public function compile(Route $route)
3939
{
40+
$staticPrefix = null;
41+
$hostnameVariables = array();
42+
$pathVariables = array();
43+
$variables = array();
44+
$tokens = array();
45+
$regex = null;
46+
$hostnameRegex = null;
47+
$hostnameTokens = array();
48+
49+
if (null !== $hostnamePattern = $route->getHostnamePattern()) {
50+
51+
$result = $this->compilePattern($route, $hostnamePattern, true);
52+
53+
$hostnameVariables = $result['variables'];
54+
$variables = array_merge($variables, $hostnameVariables);
55+
56+
$hostnameTokens = $result['tokens'];
57+
$hostnameRegex = $result['regex'];
58+
}
59+
4060
$pattern = $route->getPattern();
61+
62+
$result = $this->compilePattern($route, $pattern, false);
63+
64+
$staticPrefix = $result['staticPrefix'];
65+
66+
$pathVariables = $result['variables'];
67+
$variables = array_merge($variables, $pathVariables);
68+
69+
$tokens = $result['tokens'];
70+
$regex = $result['regex'];
71+
72+
return new CompiledRoute(
73+
$staticPrefix,
74+
$regex,
75+
$tokens,
76+
array_unique($variables),
77+
$pathVariables,
78+
$hostnameVariables,
79+
$hostnameRegex,
80+
$hostnameTokens
81+
);
82+
}
83+
84+
private function compilePattern(Route $route, $pattern, $isHostname)
85+
{
86+
$len = strlen($pattern);
4187
$tokens = array();
4288
$variables = array();
4389
$matches = array();
4490
$pos = 0;
91+
$defaultSeparator = $isHostname ? '.' : '/';
4592

4693
// Match all variables enclosed in "{}" and iterate over them. But we only want to match the innermost variable
4794
// in case of nested "{}", e.g. {foo{bar}}. This in ensured because \w does not match "{" or "}" itself.
@@ -78,7 +125,11 @@ public function compile(Route $route)
78125
// Also even if {_format} was not optional the requirement prevents that {page} matches something that was originally
79126
// part of {_format} when generating the URL, e.g. _format = 'mobile.html'.
80127
$nextSeparator = $this->findNextSeparator($followingPattern);
81-
$regexp = sprintf('[^/%s]+', '/' !== $nextSeparator && '' !== $nextSeparator ? preg_quote($nextSeparator, self::REGEX_DELIMITER) : '');
128+
$regexp = sprintf(
129+
'[^%s%s]+',
130+
preg_quote($defaultSeparator, self::REGEX_DELIMITER),
131+
$defaultSeparator !== $nextSeparator && '' !== $nextSeparator ? preg_quote($nextSeparator, self::REGEX_DELIMITER) : ''
132+
);
82133
if (('' !== $nextSeparator && !preg_match('#^\{\w+\}#', $followingPattern)) || '' === $followingPattern) {
83134
// When we have a separator, which is disallowed for the variable, we can optimize the regex with a possessive
84135
// quantifier. This prevents useless backtracking of PCRE and improves performance by 20% for matching those patterns.
@@ -99,12 +150,14 @@ public function compile(Route $route)
99150

100151
// find the first optional token
101152
$firstOptional = INF;
102-
for ($i = count($tokens) - 1; $i >= 0; $i--) {
103-
$token = $tokens[$i];
104-
if ('variable' === $token[0] && $route->hasDefault($token[3])) {
105-
$firstOptional = $i;
106-
} else {
107-
break;
153+
if (!$isHostname) {
154+
for ($i = count($tokens) - 1; $i >= 0; $i--) {
155+
$token = $tokens[$i];
156+
if ('variable' === $token[0] && $route->hasDefault($token[3])) {
157+
$firstOptional = $i;
158+
} else {
159+
break;
160+
}
108161
}
109162
}
110163

@@ -114,11 +167,11 @@ public function compile(Route $route)
114167
$regexp .= $this->computeRegexp($tokens, $i, $firstOptional);
115168
}
116169

117-
return new CompiledRoute(
118-
'text' === $tokens[0][0] ? $tokens[0][1] : '',
119-
self::REGEX_DELIMITER.'^'.$regexp.'$'.self::REGEX_DELIMITER.'s',
120-
array_reverse($tokens),
121-
$variables
170+
return array(
171+
'staticPrefix' => 'text' === $tokens[0][0] ? $tokens[0][1] : '',
172+
'regex' => self::REGEX_DELIMITER.'^'.$regexp.'$'.self::REGEX_DELIMITER.'s',
173+
'tokens' => array_reverse($tokens),
174+
'variables' => $variables,
122175
);
123176
}
124177

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,78 @@ public function getNumericVariableNames()
179179
array('1e2')
180180
);
181181
}
182+
183+
/**
184+
* @dataProvider provideCompileWithHostnameData
185+
*/
186+
public function testCompileWithHostname($name, $arguments, $prefix, $regex, $variables, $pathVariables, $tokens, $hostnameRegex, $hostnameVariables, $hostnameTokens)
187+
{
188+
$r = new \ReflectionClass('Symfony\\Component\\Routing\\Route');
189+
$route = $r->newInstanceArgs($arguments);
190+
191+
$compiled = $route->compile();
192+
$this->assertEquals($prefix, $compiled->getStaticPrefix(), $name.' (static prefix)');
193+
$this->assertEquals($regex, str_replace(array("\n", ' '), '', $compiled->getRegex()), $name.' (regex)');
194+
$this->assertEquals($variables, $compiled->getVariables(), $name.' (variables)');
195+
$this->assertEquals($pathVariables, $compiled->getPathVariables(), $name.' (path variables)');
196+
$this->assertEquals($tokens, $compiled->getTokens(), $name.' (tokens)');
197+
198+
199+
$this->assertEquals($hostnameRegex, str_replace(array("\n", ' '), '', $compiled->getHostnameRegex()), $name.' (hostname regex)');
200+
$this->assertEquals($hostnameVariables, $compiled->getHostnameVariables(), $name.' (hostname variables)');
201+
$this->assertEquals($hostnameTokens, $compiled->getHostnameTokens(), $name.' (hostname tokens)');
202+
}
203+
204+
public function provideCompileWithHostnameData()
205+
{
206+
return array(
207+
array(
208+
'Route with hostname pattern',
209+
array('/hello', array(), array(), array(), 'www.example.com'),
210+
'/hello', '#^/hello$#s', array(), array(), array(
211+
array('text', '/hello'),
212+
),
213+
'#^www\.example\.com$#s', array(), array(
214+
array('text', 'www.example.com'),
215+
),
216+
),
217+
array(
218+
'Route with hostname pattern and some variables',
219+
array('/hello/{name}', array(), array(), array(), 'www.example.{tld}'),
220+
'/hello', '#^/hello/(?<name>[^/]++)$#s', array('tld', 'name'), array('name'), array(
221+
array('variable', '/', '[^/]++', 'name'),
222+
array('text', '/hello'),
223+
),
224+
'#^www\.example\.(?<tld>[^\.]++)$#s', array('tld'), array(
225+
array('variable', '.', '[^\.]++', 'tld'),
226+
array('text', 'www.example'),
227+
),
228+
),
229+
array(
230+
'Route with variable at begining of hostname',
231+
array('/hello', array(), array(), array(), '{locale}.example.{tld}'),
232+
'/hello', '#^/hello$#s', array('locale', 'tld'), array(), array(
233+
array('text', '/hello'),
234+
),
235+
'#^(?<locale>[^\.]++)\.example\.(?<tld>[^\.]++)$#s', array('locale', 'tld'), array(
236+
array('variable', '.', '[^\.]++', 'tld'),
237+
array('text', '.example'),
238+
array('variable', '', '[^\.]++', 'locale'),
239+
),
240+
),
241+
array(
242+
'Route with hostname variables that has a default value',
243+
array('/hello', array('locale' => 'a', 'tld' => 'b'), array(), array(), '{locale}.example.{tld}'),
244+
'/hello', '#^/hello$#s', array('locale', 'tld'), array(), array(
245+
array('text', '/hello'),
246+
),
247+
'#^(?<locale>[^\.]++)\.example\.(?<tld>[^\.]++)$#s', array('locale', 'tld'), array(
248+
array('variable', '.', '[^\.]++', 'tld'),
249+
array('text', '.example'),
250+
array('variable', '', '[^\.]++', 'locale'),
251+
),
252+
),
253+
);
254+
}
182255
}
256+

0 commit comments

Comments
 (0)