144 lines
5.6 KiB
PHP
144 lines
5.6 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the Symfony package.
|
|
*
|
|
* (c) Fabien Potencier <fabien@symfony.com>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Symfony\Component\Routing;
|
|
|
|
/**
|
|
* RouteCompiler compiles Route instances to CompiledRoute instances.
|
|
*
|
|
* @author Fabien Potencier <fabien@symfony.com>
|
|
*/
|
|
class RouteCompiler implements RouteCompilerInterface
|
|
{
|
|
const REGEX_DELIMITER = '#';
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*
|
|
* @throws \LogicException If a variable is referenced more than once or if a required variable
|
|
* has a default value that doesn't meet its own requirement.
|
|
*/
|
|
public function compile(Route $route)
|
|
{
|
|
$pattern = $route->getPattern();
|
|
$len = strlen($pattern);
|
|
$tokens = array();
|
|
$variables = array();
|
|
$pos = 0;
|
|
preg_match_all('#.\{(\w+)\}#', $pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
|
|
foreach ($matches as $match) {
|
|
if ($text = substr($pattern, $pos, $match[0][1] - $pos)) {
|
|
$tokens[] = array('text', $text);
|
|
}
|
|
|
|
$pos = $match[0][1] + strlen($match[0][0]);
|
|
$var = $match[1][0];
|
|
|
|
if ($req = $route->getRequirement($var)) {
|
|
$regexp = $req;
|
|
} else {
|
|
// Use the character preceding the variable as a separator
|
|
$separators = array($match[0][0][0]);
|
|
|
|
if ($pos !== $len) {
|
|
// Use the character following the variable as the separator when available
|
|
$separators[] = $pattern[$pos];
|
|
}
|
|
$regexp = sprintf('[^%s]+', preg_quote(implode('', array_unique($separators)), self::REGEX_DELIMITER));
|
|
}
|
|
|
|
$tokens[] = array('variable', $match[0][0][0], $regexp, $var);
|
|
|
|
if (in_array($var, $variables)) {
|
|
throw new \LogicException(sprintf('Route pattern "%s" cannot reference variable name "%s" more than once.', $route->getPattern(), $var));
|
|
}
|
|
|
|
$variables[] = $var;
|
|
}
|
|
|
|
if ($pos < $len) {
|
|
$tokens[] = array('text', substr($pattern, $pos));
|
|
}
|
|
|
|
// find the first optional token and validate the default values for non-optional variables
|
|
$optional = true;
|
|
$firstOptional = INF;
|
|
for ($i = count($tokens) - 1; $i >= 0; $i--) {
|
|
$token = $tokens[$i];
|
|
if ('variable' === $token[0] && $route->hasDefault($token[3])) {
|
|
if ($optional) {
|
|
$firstOptional = $i;
|
|
} elseif (!preg_match('#^'.$token[2].'$#', $route->getDefault($token[3]))) {
|
|
throw new \LogicException(sprintf('The default value "%s" of the required variable "%s" in pattern "%s" does not match the requirement "%s". ' .
|
|
'This route definition makes no sense because this default can neither be used as default for generating URLs nor can it ever be returned by the matching process. ' .
|
|
'You should change the default to something that meets the requirement or remove it.',
|
|
$route->getDefault($token[3]), $token[3], $route->getPattern(), $token[2]
|
|
));
|
|
}
|
|
} else {
|
|
$optional = false;
|
|
}
|
|
}
|
|
|
|
// compute the matching regexp
|
|
$regexp = '';
|
|
for ($i = 0, $nbToken = count($tokens); $i < $nbToken; $i++) {
|
|
$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
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Computes the regexp used to match a specific token. It can be static text or a subpattern.
|
|
*
|
|
* @param array $tokens The route tokens
|
|
* @param integer $index The index of the current token
|
|
* @param integer $firstOptional The index of the first optional token
|
|
*
|
|
* @return string The regexp pattern for a single token
|
|
*/
|
|
private function computeRegexp(array $tokens, $index, $firstOptional)
|
|
{
|
|
$token = $tokens[$index];
|
|
if ('text' === $token[0]) {
|
|
// Text tokens
|
|
return preg_quote($token[1], self::REGEX_DELIMITER);
|
|
} else {
|
|
// Variable tokens
|
|
if (0 === $index && 0 === $firstOptional) {
|
|
// When the only token is an optional variable token, the separator is required
|
|
return sprintf('%s(?<%s>%s)?', preg_quote($token[1], self::REGEX_DELIMITER), $token[3], $token[2]);
|
|
} else {
|
|
$regexp = sprintf('%s(?<%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));
|
|
}
|
|
}
|
|
|
|
return $regexp;
|
|
}
|
|
}
|
|
}
|
|
}
|