2010-02-17 13:53:31 +00:00
< ? php
/*
2011-01-15 13:29:43 +00:00
* This file is part of the Symfony package .
2010-02-17 13:53:31 +00:00
*
2011-03-06 11:40:06 +00:00
* ( c ) Fabien Potencier < fabien @ symfony . com >
2010-02-17 13:53:31 +00:00
*
2011-01-15 13:29:43 +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
*/
2011-01-15 13:29:43 +00:00
namespace Symfony\Component\Routing ;
2010-02-17 13:53:31 +00:00
/**
* RouteCompiler compiles Route instances to CompiledRoute instances .
*
2011-03-06 11:40:06 +00:00
* @ author Fabien Potencier < fabien @ symfony . com >
2012-05-08 09:14:10 +01:00
* @ author Tobias Schultze < http :// tobion . de >
2010-02-17 13:53:31 +00:00
*/
class RouteCompiler implements RouteCompilerInterface
{
2012-04-12 02:30:01 +01:00
const REGEX_DELIMITER = '#' ;
2012-09-03 22:10:00 +01:00
/**
* 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 = '/,;.:-_~+*=@|' ;
2016-11-10 14:01:21 +00:00
/**
* The maximum supported length of a PCRE subpattern name
* http :// pcre . org / current / doc / html / pcre2pattern . html #SEC16.
*
* @ internal
*/
const VARIABLE_MAXIMUM_LENGTH = 32 ;
2010-05-06 12:25:53 +01:00
/**
2014-04-15 06:57:34 +01:00
* { @ inheritdoc }
2012-04-27 14:39:00 +01:00
*
2012-09-10 10:27:26 +01:00
* @ throws \LogicException If a variable is referenced more than once
2017-09-11 10:28:55 +01:00
* @ throws \DomainException if a variable name starts with a digit or if it is too long to be successfully used as
* a PCRE subpattern
2010-05-06 12:25:53 +01:00
*/
2012-12-27 03:11:08 +00:00
public static function compile ( Route $route )
2010-02-17 13:53:31 +00:00
{
2013-01-21 16:57:28 +00:00
$hostVariables = array ();
2012-04-14 18:15:20 +01:00
$variables = array ();
2013-01-21 16:57:28 +00:00
$hostRegex = null ;
$hostTokens = array ();
2012-04-14 18:15:20 +01:00
2013-01-21 16:57:28 +00:00
if ( '' !== $host = $route -> getHost ()) {
$result = self :: compilePattern ( $route , $host , true );
2012-04-14 18:15:20 +01:00
2013-01-21 16:57:28 +00:00
$hostVariables = $result [ 'variables' ];
2015-01-31 22:26:34 +00:00
$variables = $hostVariables ;
2012-04-14 18:15:20 +01:00
2013-01-21 16:57:28 +00:00
$hostTokens = $result [ 'tokens' ];
$hostRegex = $result [ 'regex' ];
2012-04-14 18:15:20 +01:00
}
2013-01-14 16:36:16 +00:00
$path = $route -> getPath ();
2012-04-14 18:15:20 +01:00
2013-01-15 16:25:04 +00:00
$result = self :: compilePattern ( $route , $path , false );
2012-04-14 18:15:20 +01:00
$staticPrefix = $result [ 'staticPrefix' ];
$pathVariables = $result [ 'variables' ];
$variables = array_merge ( $variables , $pathVariables );
$tokens = $result [ 'tokens' ];
$regex = $result [ 'regex' ];
return new CompiledRoute (
$staticPrefix ,
$regex ,
$tokens ,
$pathVariables ,
2013-01-21 16:57:28 +00:00
$hostRegex ,
$hostTokens ,
$hostVariables ,
2012-04-25 02:25:08 +01:00
array_unique ( $variables )
2012-04-14 18:15:20 +01:00
);
}
2013-01-21 16:57:28 +00:00
private static function compilePattern ( Route $route , $pattern , $isHost )
2012-04-14 18:15:20 +01:00
{
2010-05-06 12:25:53 +01:00
$tokens = array ();
2011-04-25 11:03:41 +01:00
$variables = array ();
2012-05-08 09:14:10 +01:00
$matches = array ();
2011-04-25 11:03:41 +01:00
$pos = 0 ;
2013-01-21 16:57:28 +00:00
$defaultSeparator = $isHost ? '.' : '/' ;
2012-04-27 14:39:00 +01:00
2012-05-08 09:14:10 +01:00
// 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 ]);
2012-05-08 09:14:10 +01:00
$precedingChar = strlen ( $precedingText ) > 0 ? substr ( $precedingText , - 1 ) : '' ;
2012-09-03 22:10:00 +01:00
$isSeparator = '' !== $precedingChar && false !== strpos ( static :: SEPARATORS , $precedingChar );
2010-02-17 13:53:31 +00:00
2016-11-10 14:01:21 +00:00
// A PCRE subpattern name must start with a non-digit. Also a PHP variable cannot start with a digit so the
// variable would not be usable as a Controller action argument.
if ( preg_match ( '/^\d/' , $varName )) {
throw new \DomainException ( sprintf ( 'Variable name "%s" cannot start with a digit in route pattern "%s". Please use a different name.' , $varName , $pattern ));
2012-05-08 09:14:10 +01:00
}
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
2016-11-10 14:01:21 +00:00
if ( strlen ( $varName ) > self :: VARIABLE_MAXIMUM_LENGTH ) {
throw new \DomainException ( sprintf ( 'Variable name "%s" cannot be longer than %s characters in route pattern "%s". Please use a shorter name.' , $varName , self :: VARIABLE_MAXIMUM_LENGTH , $pattern ));
}
2012-09-03 22:10:00 +01:00
if ( $isSeparator && strlen ( $precedingText ) > 1 ) {
2012-05-08 09:14:10 +01:00
$tokens [] = array ( 'text' , substr ( $precedingText , 0 , - 1 ));
2012-09-03 22:10:00 +01:00
} elseif ( ! $isSeparator && strlen ( $precedingText ) > 0 ) {
$tokens [] = array ( 'text' , $precedingText );
2012-05-08 09:14:10 +01:00
}
2012-09-03 22:10:00 +01:00
2012-05-08 09:14:10 +01:00
$regexp = $route -> getRequirement ( $varName );
if ( null === $regexp ) {
2012-09-08 11:51:33 +01:00
$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'.
2012-12-27 03:11:08 +00:00
$nextSeparator = self :: findNextSeparator ( $followingPattern );
2012-04-14 18:15:20 +01:00
$regexp = sprintf (
'[^%s%s]+' ,
preg_quote ( $defaultSeparator , self :: REGEX_DELIMITER ),
$defaultSeparator !== $nextSeparator && '' !== $nextSeparator ? preg_quote ( $nextSeparator , self :: REGEX_DELIMITER ) : ''
);
2012-09-08 12:51:18 +01:00
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
2012-09-08 12:51:18 +01:00
// 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 .= '+' ;
}
2012-02-14 10:41:45 +00:00
}
2012-09-03 22:10:00 +01:00
$tokens [] = array ( 'variable' , $isSeparator ? $precedingChar : '' , $regexp , $varName );
2012-05-08 09:14:10 +01:00
$variables [] = $varName ;
2010-05-06 12:25:53 +01:00
}
2012-05-08 09:14:10 +01:00
if ( $pos < strlen ( $pattern )) {
2011-04-25 11:03:41 +01:00
$tokens [] = array ( 'text' , substr ( $pattern , $pos ));
2010-05-06 12:25:53 +01:00
}
2012-09-05 16:44:05 +01:00
// find the first optional token
2013-03-05 14:28:25 +00:00
$firstOptional = PHP_INT_MAX ;
2013-01-21 16:57:28 +00:00
if ( ! $isHost ) {
2015-03-31 00:07:44 +01:00
for ( $i = count ( $tokens ) - 1 ; $i >= 0 ; -- $i ) {
2012-04-14 18:15:20 +01:00
$token = $tokens [ $i ];
if ( 'variable' === $token [ 0 ] && $route -> hasDefault ( $token [ 3 ])) {
$firstOptional = $i ;
} else {
break ;
}
2010-05-06 12:25:53 +01:00
}
}
2010-02-17 13:53:31 +00:00
2011-04-25 11:03:41 +01:00
// compute the matching regexp
2012-04-10 13:42:59 +01:00
$regexp = '' ;
2015-03-31 00:07:44 +01:00
for ( $i = 0 , $nbToken = count ( $tokens ); $i < $nbToken ; ++ $i ) {
2012-12-27 03:11:08 +00:00
$regexp .= self :: computeRegexp ( $tokens , $i , $firstOptional );
2010-05-06 12:25:53 +01:00
}
2010-02-17 13:53:31 +00:00
2012-04-14 18:15:20 +01:00
return array (
'staticPrefix' => 'text' === $tokens [ 0 ][ 0 ] ? $tokens [ 0 ][ 1 ] : '' ,
2017-12-07 09:40:47 +00:00
'regex' => self :: REGEX_DELIMITER . '^' . $regexp . '$' . self :: REGEX_DELIMITER . 'sD' . ( $isHost ? 'i' : '' ),
2012-04-14 18:15:20 +01:00
'tokens' => array_reverse ( $tokens ),
'variables' => $variables ,
2011-04-25 11:03:41 +01:00
);
2010-05-06 12:25:53 +01:00
}
2012-04-10 13:42:59 +01:00
2012-05-08 09:14:10 +01:00
/**
* Returns the next static character in the Route pattern that will serve as a separator .
*
* @ param string $pattern The route pattern
*
2012-09-03 22:10:00 +01:00
* @ return string The next static character that functions as separator ( or empty string when none available )
2012-05-08 09:14:10 +01:00
*/
2012-12-27 03:11:08 +00:00
private static function findNextSeparator ( $pattern )
2012-05-08 09:14:10 +01:00
{
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 );
2012-09-03 22:10:00 +01:00
return isset ( $pattern [ 0 ]) && false !== strpos ( static :: SEPARATORS , $pattern [ 0 ]) ? $pattern [ 0 ] : '' ;
2012-05-08 09:14:10 +01:00
}
2012-04-10 13:42:59 +01:00
/**
2012-04-12 02:30:01 +01:00
* Computes the regexp used to match a specific token . It can be static text or a subpattern .
2012-04-10 13:42:59 +01:00
*
2014-11-30 13:33:44 +00:00
* @ param array $tokens The route tokens
* @ param int $index The index of the current token
* @ param int $firstOptional The index of the first optional token
2012-04-10 13:42:59 +01:00
*
2012-04-12 02:30:01 +01:00
* @ return string The regexp pattern for a single token
2012-04-10 13:42:59 +01:00
*/
2012-12-27 03:11:08 +00:00
private static function computeRegexp ( array $tokens , $index , $firstOptional )
2012-04-10 13:42:59 +01:00
{
$token = $tokens [ $index ];
2012-05-21 21:27:15 +01:00
if ( 'text' === $token [ 0 ]) {
2012-04-10 13:42:59 +01:00
// Text tokens
2012-04-12 02:30:01 +01:00
return preg_quote ( $token [ 1 ], self :: REGEX_DELIMITER );
2012-04-10 13:42:59 +01:00
} else {
// Variable tokens
2012-05-20 04:43:44 +01:00
if ( 0 === $index && 0 === $firstOptional ) {
2012-04-10 13:42:59 +01:00
// When the only token is an optional variable token, the separator is required
2012-11-19 12:02:22 +00:00
return sprintf ( '%s(?P<%s>%s)?' , preg_quote ( $token [ 1 ], self :: REGEX_DELIMITER ), $token [ 3 ], $token [ 2 ]);
2012-04-10 13:42:59 +01:00
} else {
2012-11-19 12:02:22 +00:00
$regexp = sprintf ( '%s(?P<%s>%s)' , preg_quote ( $token [ 1 ], self :: REGEX_DELIMITER ), $token [ 3 ], $token [ 2 ]);
2012-04-10 13:42:59 +01:00
if ( $index >= $firstOptional ) {
2012-04-12 02:30:01 +01:00
// 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.
2012-04-10 13:42:59 +01:00
$regexp = " (?: $regexp " ;
2012-04-12 02:30:01 +01:00
$nbTokens = count ( $tokens );
2012-04-10 13:42:59 +01:00
if ( $nbTokens - 1 == $index ) {
// Close the optional subpatterns
2015-03-21 10:51:07 +00:00
$regexp .= str_repeat ( ')?' , $nbTokens - $firstOptional - ( 0 === $firstOptional ? 1 : 0 ));
2012-04-10 13:42:59 +01:00
}
}
2012-04-18 09:41:11 +01:00
2012-04-10 13:42:59 +01:00
return $regexp ;
}
}
}
2010-02-17 13:53:31 +00:00
}