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

234 lines
10 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 = '#';
/**
* This string defines the characters that are automatically considered separators in front of
* optional placeholders (with default and no static text following). Such a single separator
* can be left out together with the optional placeholder from matching and generating URLs.
*/
const SEPARATORS = '/,;.:-_~+*=@|';
/**
* {@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. "(?P<123>.+)".
*/
public static function compile(Route $route)
2010-02-17 13:53:31 +00:00
{
$staticPrefix = null;
$hostVariables = array();
$pathVariables = array();
$variables = array();
$tokens = array();
$regex = null;
$hostRegex = null;
$hostTokens = array();
if ('' !== $host = $route->getHost()) {
$result = self::compilePattern($route, $host, true);
$hostVariables = $result['variables'];
$variables = array_merge($variables, $hostVariables);
$hostTokens = $result['tokens'];
$hostRegex = $result['regex'];
}
2013-01-14 16:36:16 +00:00
$path = $route->getPath();
merged branch fabpot/routing-options (PR #6738) This PR was merged into the master branch. Commits ------- 9fc7def added the UPGRADE file for Symfony 3.0 e84cad2 [Routing] updated CHANGELOG 65eca8a [Routing] added new schemes and methods options to the annotation loader 5082994 [Routing] renamed pattern to path b357caf [Routing] renamed hostname pattern to just hostname e803f46 made schemes and methods available in XmlFileLoader d374e70 made schemes and methods available in YamlFileLoader 2834e7e added scheme and method setter in RouteCollection 10183de make scheme and method requirements first-class citizen in Route Discussion ---------- Routing options | Q | A | ------------- | --- | Bug fix? | no | New feature? | no | BC breaks? | no | Deprecations? | yes | Tests pass? | yes | Fixed tickets | #5989, #5990, #6049 | License | MIT In #5989, it has unanimously been decided to renamed `hostname_pattern` to `hostname` and `pattern` to `path`. That makes a lot of sense and I would like to do the renaming now as `hostname_pattern` is new in Symfony 2.2, so I'd like to avoid breaking BC just after the release. As we are modifying the route options, I've also included changes introduced by @Tobion in #6049 which were discussed in #5990. As everything is BC, I think it's wise to include that in 2.2. What do you think? --------------------------------------------------------------------------- by Tobion at 2013-01-14T18:25:53Z I agree it should be done in 2.2. Thanks for working on it. --------------------------------------------------------------------------- by vicb at 2013-01-14T23:11:12Z @fabpot "Everything is BC" until it breaks BC in 3.0, that's why I'd like to see [deprecations in PR summary](https://github.com/symfony/symfony-docs/pull/2116) what do you think ? --------------------------------------------------------------------------- by vicb at 2013-01-14T23:16:40Z it would also be great to update the CHANGELOG with deprecations (it could also help people answering your question) --------------------------------------------------------------------------- by fabpot at 2013-01-15T07:07:03Z @vicb: I've just updated the CHANGELOG and created the UPGRADE file for 3.0. --------------------------------------------------------------------------- by vicb at 2013-01-15T07:15:32Z @fabpot thanks.
2013-01-15 16:25:04 +00:00
$result = self::compilePattern($route, $path, false);
$staticPrefix = $result['staticPrefix'];
$pathVariables = $result['variables'];
$variables = array_merge($variables, $pathVariables);
$tokens = $result['tokens'];
$regex = $result['regex'];
return new CompiledRoute(
$staticPrefix,
$regex,
$tokens,
$pathVariables,
$hostRegex,
$hostTokens,
$hostVariables,
array_unique($variables)
);
}
private static function compilePattern(Route $route, $pattern, $isHost)
{
$tokens = array();
2011-04-25 11:03:41 +01:00
$variables = array();
$matches = array();
2011-04-25 11:03:41 +01:00
$pos = 0;
$defaultSeparator = $isHost ? '.' : '/';
// 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) : '';
$isSeparator = '' !== $precedingChar && false !== strpos(static::SEPARATORS, $precedingChar);
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 ($isSeparator && strlen($precedingText) > 1) {
$tokens[] = array('text', substr($precedingText, 0, -1));
} elseif (!$isSeparator && strlen($precedingText) > 0) {
$tokens[] = array('text', $precedingText);
}
$regexp = $route->getRequirement($varName);
if (null === $regexp) {
$followingPattern = (string) substr($pattern, $pos);
// Find the next static character after the variable that functions as a separator. By default, this separator and '/'
// are disallowed for the variable. This default requirement makes sure that optional variables can be matched at all
// and that the generating-matching-combination of URLs unambiguous, i.e. the params used for generating the URL are
// the same that will be matched. Example: new Route('/{page}.{_format}', array('_format' => 'html'))
// If {page} would also match the separating dot, {_format} would never match as {page} will eagerly consume everything.
// 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 = self::findNextSeparator($followingPattern);
$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.
2012-12-11 10:49:22 +00:00
// Given the above example, there is no point in backtracking into {page} (that forbids the dot) when a dot must follow
// after it. This optimization cannot be applied when the next char is no real separator or when the next variable is
// directly adjacent, e.g. '/{x}{y}'.
$regexp .= '+';
}
}
$tokens[] = array('variable', $isSeparator ? $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
$firstOptional = PHP_INT_MAX;
if (!$isHost) {
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 .= self::computeRegexp($tokens, $i, $firstOptional);
}
2010-02-17 13:53:31 +00:00
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,
2011-04-25 11:03:41 +01:00
);
}
/**
* 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 that functions as separator (or empty string when none available)
*/
private static 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]) && false !== strpos(static::SEPARATORS, $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 static 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(?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));
}
}
2012-04-18 09:41:11 +01:00
return $regexp;
}
}
}
2010-02-17 13:53:31 +00:00
}