Skip to content

[Routing][WIP] Support routes parameters names that are longer than 32 characters #20327

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 43 additions & 9 deletions src/Symfony/Component/Routing/CompiledRoute.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,31 @@ class CompiledRoute implements \Serializable
private $hostRegex;
private $hostTokens;

/**
* @var array
*/
private $regexVariablesAliases;

/**
* @var array
*/
private $hostRegexVariablesAliases;

/**
* Constructor.
*
* @param string $staticPrefix The static prefix of the compiled route
* @param string $regex The regular expression to use to match this route
* @param array $tokens An array of tokens to use to generate URL for this route
* @param array $pathVariables An array of path variables
* @param string|null $hostRegex Host regex
* @param array $hostTokens Host tokens
* @param array $hostVariables An array of host variables
* @param array $variables An array of variables (variables defined in the path and in the host patterns)
* @param string $staticPrefix The static prefix of the compiled route
* @param string $regex The regular expression to use to match this route
* @param array $tokens An array of tokens to use to generate URL for this route
* @param array $pathVariables An array of path variables
* @param string|null $hostRegex Host regex
* @param array $hostTokens Host tokens
* @param array $hostVariables An array of host variables
* @param array $variables An array of variables (variables defined in the path and in the host patterns)
* @param array $regexVariablesAliases An array containing path variables aliases as keys and actual path variables names as values
* @param array $hostRegexVariablesAliases An array containing host variables aliases as keys and actual host variables names as values
*/
public function __construct($staticPrefix, $regex, array $tokens, array $pathVariables, $hostRegex = null, array $hostTokens = array(), array $hostVariables = array(), array $variables = array())
public function __construct($staticPrefix, $regex, array $tokens, array $pathVariables, $hostRegex = null, array $hostTokens = array(), array $hostVariables = array(), array $variables = array(), array $regexVariablesAliases = array(), array $hostRegexVariablesAliases = array())
{
$this->staticPrefix = (string) $staticPrefix;
$this->regex = $regex;
Expand All @@ -49,6 +61,8 @@ public function __construct($staticPrefix, $regex, array $tokens, array $pathVar
$this->hostTokens = $hostTokens;
$this->hostVariables = $hostVariables;
$this->variables = $variables;
$this->regexVariablesAliases = $regexVariablesAliases;
$this->hostRegexVariablesAliases = $hostRegexVariablesAliases;
}

/**
Expand All @@ -62,9 +76,11 @@ public function serialize()
'path_regex' => $this->regex,
'path_tokens' => $this->tokens,
'path_vars' => $this->pathVariables,
'path_regex_vars_aliases' => $this->regexVariablesAliases,
'host_regex' => $this->hostRegex,
'host_tokens' => $this->hostTokens,
'host_vars' => $this->hostVariables,
'host_regex_vars_aliases' => $this->hostRegexVariablesAliases,
));
}

Expand All @@ -79,9 +95,11 @@ public function unserialize($serialized)
$this->regex = $data['path_regex'];
$this->tokens = $data['path_tokens'];
$this->pathVariables = $data['path_vars'];
$this->regexVariablesAliases = $data['path_regex_vars_aliases'];
$this->hostRegex = $data['host_regex'];
$this->hostTokens = $data['host_tokens'];
$this->hostVariables = $data['host_vars'];
$this->hostRegexVariablesAliases = $data['host_regex_vars_aliases'];
}

/**
Expand Down Expand Up @@ -163,4 +181,20 @@ public function getHostVariables()
{
return $this->hostVariables;
}

/**
* @return array
*/
public function getRegexVariablesAliases()
{
return $this->regexVariablesAliases;
}

/**
* @return array
*/
public function getHostRegexVariablesAliases()
{
return $this->hostRegexVariablesAliases;
}
}
16 changes: 16 additions & 0 deletions src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -316,9 +316,25 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren
$vars = array();
if ($hostMatches) {
$vars[] = '$hostMatches';
foreach ($compiledRoute->getHostRegexVariablesAliases() as $hostRegexVariableAlias => $actualHostRegexVariableName) {
$code .= <<<EOF
\$hostMatches['$actualHostRegexVariableName'] = \$hostMatches['$hostRegexVariableAlias'];
unset(\$hostMatches['$hostRegexVariableAlias']);


EOF;
}
}
if ($matches) {
$vars[] = '$matches';
foreach ($compiledRoute->getRegexVariablesAliases() as $regexVariableAlias => $actualRegexVariableName) {
$code .= <<<EOF
\$matches['$actualRegexVariableName'] = \$matches['$regexVariableAlias'];
unset(\$matches['$regexVariableAlias']);


EOF;
}
}
$vars[] = "array('_route' => '$name')";

Expand Down
106 changes: 75 additions & 31 deletions src/Symfony/Component/Routing/RouteCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public static function compile(Route $route)
$hostVariables = array();
$variables = array();
$hostRegex = null;
$hostRegexVariablesAliases = array();
$hostTokens = array();

if ('' !== $host = $route->getHost()) {
Expand All @@ -50,6 +51,7 @@ public static function compile(Route $route)

$hostTokens = $result['tokens'];
$hostRegex = $result['regex'];
$hostRegexVariablesAliases = $result['regex_variables_aliases'];
}

$path = $route->getPath();
Expand All @@ -63,6 +65,7 @@ public static function compile(Route $route)

$tokens = $result['tokens'];
$regex = $result['regex'];
$regexVariablesAliases = $result['regex_variables_aliases'];

return new CompiledRoute(
$staticPrefix,
Expand All @@ -72,7 +75,9 @@ public static function compile(Route $route)
$hostRegex,
$hostTokens,
$hostVariables,
array_unique($variables)
array_unique($variables),
$regexVariablesAliases,
$hostRegexVariablesAliases
);
}

Expand Down Expand Up @@ -142,10 +147,12 @@ private static function compilePattern(Route $route, $pattern, $isHost)
$tokens[] = array('text', substr($pattern, $pos));
}

$tokensCount = count($tokens);

// find the first optional token
$firstOptional = PHP_INT_MAX;
if (!$isHost) {
for ($i = count($tokens) - 1; $i >= 0; --$i) {
for ($i = $tokensCount - 1; $i >= 0; --$i) {
$token = $tokens[$i];
if ('variable' === $token[0] && $route->hasDefault($token[3])) {
$firstOptional = $i;
Expand All @@ -157,15 +164,34 @@ private static function compilePattern(Route $route, $pattern, $isHost)

// compute the matching regexp
$regexp = '';
for ($i = 0, $nbToken = count($tokens); $i < $nbToken; ++$i) {
$regexp .= self::computeRegexp($tokens, $i, $firstOptional);
$regexpVariablesAliases = array();
for ($i = 0, $nbToken = $tokensCount; $i < $nbToken; ++$i) {
$token = $tokens[$i];
switch ($token[0]) {
case 'text':
$regexp .= self::computeRegexpForTextToken($token);
break;
case 'variable':
list($tokenRegexp, $regexpVariableName) = self::computeRegexpForVariableToken($token, $i, $tokensCount, $firstOptional, $variables);
$regexp .= $tokenRegexp;

$variableName = $token[3];
if ($regexpVariableName !== $variableName) {
$regexpVariablesAliases[$regexpVariableName] = $variableName;
}

break;
default:
throw new \LogicException('The token type should be "text" or "variable".');
}
}

return array(
'staticPrefix' => 'text' === $tokens[0][0] ? $tokens[0][1] : '',
'regex' => self::REGEX_DELIMITER.'^'.$regexp.'$'.self::REGEX_DELIMITER.'s'.($isHost ? 'i' : ''),
'tokens' => array_reverse($tokens),
'variables' => $variables,
'regex_variables_aliases' => $regexpVariablesAliases,
);
}

Expand All @@ -189,41 +215,59 @@ private static function findNextSeparator($pattern)
}

/**
* Computes the regexp used to match a specific token. It can be static text or a subpattern.
* Computes the regexp used to match a static text token.
*
* @param array $token The static text token
*
* @param array $tokens The route tokens
* @param int $index The index of the current token
* @return string The regexp pattern of the token
*/
private static function computeRegexpForTextToken(array $token)
{
return preg_quote($token[1], self::REGEX_DELIMITER);
}

/**
* Computes the regexp used to match a subpattern token.
*
* @param array $token The subpattern token
* @param int $index The index of the token
* @param int $tokensCount The total number of tokens of the route
* @param int $firstOptional The index of the first optional token
* @param array $variables All the variables names of the route
*
* @return string The regexp pattern for a single token
* @return array An array containing the regexp pattern of the token, and the variable name that is used in this regexp pattern
*/
private static function computeRegexp(array $tokens, $index, $firstOptional)
private static function computeRegexpForVariableToken(array $token, $index, $tokensCount, $firstOptional, array $variables)
{
$token = $tokens[$index];
if ('text' === $token[0]) {
// Text tokens
return preg_quote($token[1], self::REGEX_DELIMITER);
$variableName = $token[3];
// 32 is the maximum length for a PCRE subpattern name => http://pcre.org/current/doc/html/pcre2pattern.html#SEC16
if (strlen($variableName) > 32) {
$i = 0;
do {
$variableName = sprintf('variableAlias%s', ++$i);
} while (in_array($variableName, $variables));
}

if (0 === $index && 0 === $firstOptional) {
// When the only token is an optional variable token, the separator is required
$regexp = sprintf('%s(?P<%s>%s)?', preg_quote($token[1], self::REGEX_DELIMITER), $variableName, $token[2]);
} else {
// Variable tokens
if (0 === $index && 0 === $firstOptional) {
// When the only token is an optional variable token, the separator is required
return sprintf('%s(?P<%s>%s)?', preg_quote($token[1], self::REGEX_DELIMITER), $token[3], $token[2]);
} else {
$regexp = sprintf('%s(?P<%s>%s)', preg_quote($token[1], self::REGEX_DELIMITER), $token[3], $token[2]);
if ($index >= $firstOptional) {
// Enclose each optional token in a subpattern to make it optional.
// "?:" means it is non-capturing, i.e. the portion of the subject string that
// matched the optional subpattern is not passed back.
$regexp = "(?:$regexp";
$nbTokens = count($tokens);
if ($nbTokens - 1 == $index) {
// Close the optional subpatterns
$regexp .= str_repeat(')?', $nbTokens - $firstOptional - (0 === $firstOptional ? 1 : 0));
}
$regexp = sprintf('%s(?P<%s>%s)', preg_quote($token[1], self::REGEX_DELIMITER), $variableName, $token[2]);
if ($index >= $firstOptional) {
// Enclose each optional token in a subpattern to make it optional.
// "?:" means it is non-capturing, i.e. the portion of the subject string that
// matched the optional subpattern is not passed back.
$regexp = "(?:$regexp";
if ($tokensCount - 1 == $index) {
// Close the optional subpatterns
$regexp .= str_repeat(')?', $tokensCount - $firstOptional - (0 === $firstOptional ? 1 : 0));
}

return $regexp;
}
}

return array(
$regexp,
$variableName,
);
}
}
20 changes: 20 additions & 0 deletions src/Symfony/Component/Routing/Tests/RouteCompilerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,15 @@ public function provideCompileData()
array('text', '/foo'),
),
),

array(
'Route with a variable name longer than 32 characters',
array('/foo/{pneumonoultramicroscopicsilicovolcanoconiosis}'),
'/foo', '#^/foo/(?P<variableAlias1>[^/]++)$#s', array('pneumonoultramicroscopicsilicovolcanoconiosis'), array(
array('variable', '/', '[^/]++', 'pneumonoultramicroscopicsilicovolcanoconiosis'),
array('text', '/foo'),
),
),
);
}

Expand Down Expand Up @@ -262,6 +271,17 @@ public function provideCompileWithHostData()
array('variable', '', '[^\.]++', 'locale'),
),
),
array(
'Route with a variable name longer than 32 characters in the host',
array('/hello', array(), array(), array(), 'www.example.{pneumonoultramicroscopicsilicovolcanoconiosis}'),
'/hello', '#^/hello$#s', array('pneumonoultramicroscopicsilicovolcanoconiosis'), array(), array(
array('text', '/hello'),
),
'#^www\.example\.(?P<variableAlias1>[^\.]++)$#si', array('pneumonoultramicroscopicsilicovolcanoconiosis'), array(
array('variable', '.', '[^\.]++', 'pneumonoultramicroscopicsilicovolcanoconiosis'),
array('text', 'www.example'),
),
),
);
}
}