This repository has been archived on 2023-08-20. You can view files and clone it, but cannot push or open issues or pull requests.
symfony/src/Symfony/Component/Routing/RouteCompiler.php

164 lines
6.6 KiB
PHP
Raw Normal View History

2010-02-17 13:53:31 +00:00
<?php
/*
* This file is part of the Symfony package.
2010-02-17 13:53:31 +00:00
*
* (c) Fabien Potencier <fabien@symfony.com>
2010-02-17 13:53:31 +00:00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
2010-02-17 13:53:31 +00:00
*/
namespace Symfony\Component\Routing;
2010-02-17 13:53:31 +00:00
/**
* RouteCompiler compiles Route instances to CompiledRoute instances.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Tobias Schultze <http://tobion.de>
2010-02-17 13:53:31 +00:00
*/
class RouteCompiler implements RouteCompilerInterface
{
const REGEX_DELIMITER = '#';
/**
* {@inheritDoc}
*
* @throws \LogicException If a variable is referenced more than once
* @throws \DomainException If a variable name is numeric because PHP raises an error for such
* subpatterns in PCRE and thus would break matching, e.g. "(?<123>.+)".
*/
public function compile(Route $route)
2010-02-17 13:53:31 +00:00
{
2011-04-25 11:03:41 +01:00
$pattern = $route->getPattern();
$tokens = array();
2011-04-25 11:03:41 +01:00
$variables = array();
$matches = array();
2011-04-25 11:03:41 +01:00
$pos = 0;
$lastSeparator = '';
// 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.
preg_match_all('#\{\w+\}#', $pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
foreach ($matches as $match) {
$varName = substr($match[0][0], 1, -1);
// get all static text preceding the current variable
$precedingText = substr($pattern, $pos, $match[0][1] - $pos);
2011-04-25 11:03:41 +01:00
$pos = $match[0][1] + strlen($match[0][0]);
$precedingChar = strlen($precedingText) > 0 ? substr($precedingText, -1) : '';
2010-02-17 13:53:31 +00:00
if (is_numeric($varName)) {
throw new \DomainException(sprintf('Variable name "%s" cannot be numeric in route pattern "%s". Please use a different name.', $varName, $pattern));
}
if (in_array($varName, $variables)) {
throw new \LogicException(sprintf('Route pattern "%s" cannot reference variable name "%s" more than once.', $pattern, $varName));
2011-04-25 11:03:41 +01:00
}
2010-02-17 13:53:31 +00:00
if (strlen($precedingText) > 1) {
$tokens[] = array('text', substr($precedingText, 0, -1));
}
// use the character preceding the variable as a separator
// save it for later use as default separator for variables that follow directly without having a preceding char e.g. "/{x}{y}"
if ('' !== $precedingChar) {
$lastSeparator = $precedingChar;
}
$regexp = $route->getRequirement($varName);
if (null === $regexp) {
// use the character following the variable (ignoring other placeholders) as a separator when it's not the same as the preceding separator
$nextSeparator = $this->findNextSeparator(substr($pattern, $pos));
$regexp = sprintf('[^%s]+', preg_quote($lastSeparator !== $nextSeparator ? $lastSeparator.$nextSeparator : $lastSeparator, self::REGEX_DELIMITER));
}
$tokens[] = array('variable', $precedingChar, $regexp, $varName);
$variables[] = $varName;
}
if ($pos < strlen($pattern)) {
2011-04-25 11:03:41 +01:00
$tokens[] = array('text', substr($pattern, $pos));
}
// find the first optional token
2011-04-25 11:03:41 +01:00
$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;
}
}
2010-02-17 13:53:31 +00:00
2011-04-25 11:03:41 +01:00
// compute the matching regexp
$regexp = '';
for ($i = 0, $nbToken = count($tokens); $i < $nbToken; $i++) {
$regexp .= $this->computeRegexp($tokens, $i, $firstOptional);
}
2010-02-17 13:53:31 +00:00
2011-04-25 11:03:41 +01:00
return new CompiledRoute(
'text' === $tokens[0][0] ? $tokens[0][1] : '',
self::REGEX_DELIMITER.'^'.$regexp.'$'.self::REGEX_DELIMITER.'s',
2011-04-25 11:03:41 +01:00
array_reverse($tokens),
$variables
);
}
/**
* Returns the next static character in the Route pattern that will serve as a separator.
*
* @param string $pattern The route pattern
*
* @return string The next static character (or empty string when none available)
*/
private function findNextSeparator($pattern)
{
if ('' == $pattern) {
// return empty string if pattern is empty or false (false which can be returned by substr)
return '';
}
// first remove all placeholders from the pattern so we can find the next real static character
$pattern = preg_replace('#\{\w+\}#', '', $pattern);
return isset($pattern[0]) ? $pattern[0] : '';
}
/**
* 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];
2012-05-21 21:27:15 +01:00
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));
}
}
2012-04-18 09:41:11 +01:00
return $regexp;
}
}
}
2010-02-17 13:53:31 +00:00
}