2015-08-17 17:00:26 -07:00
< ? 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\Matcher\Dumper ;
2018-11-23 12:29:20 +00:00
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface ;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage ;
2015-08-17 17:00:26 -07:00
use Symfony\Component\Routing\Route ;
use Symfony\Component\Routing\RouteCollection ;
/**
* PhpMatcherDumper creates a PHP class able to match URLs for a given set of routes .
*
* @ author Fabien Potencier < fabien @ symfony . com >
* @ author Tobias Schultze < http :// tobion . de >
* @ author Arnaud Le Blanc < arnaud . lb @ gmail . com >
*/
class PhpMatcherDumper extends MatcherDumper
{
private $expressionLanguage ;
/**
* @ var ExpressionFunctionProviderInterface []
*/
private $expressionLanguageProviders = array ();
/**
* Dumps a set of routes to a PHP class .
*
* Available options :
*
* * class : The class name
* * base_class : The base class name
*
* @ param array $options An array of options
*
* @ return string A PHP class representing the matcher class
*/
public function dump ( array $options = array ())
{
$options = array_replace ( array (
'class' => 'ProjectUrlMatcher' ,
'base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher' ,
), $options );
// trailing slash support is only enabled if we know how to redirect the user
$interfaces = class_implements ( $options [ 'base_class' ]);
$supportsRedirections = isset ( $interfaces [ 'Symfony\\Component\\Routing\\Matcher\\RedirectableUrlMatcherInterface' ]);
return <<< EOF
< ? php
use Symfony\Component\Routing\Exception\MethodNotAllowedException ;
use Symfony\Component\Routing\Exception\ResourceNotFoundException ;
use Symfony\Component\Routing\RequestContext ;
/**
* This class has been auto - generated
* by the Symfony Routing Component .
*/
class { $ options [ 'class' ]} extends { $options [ 'base_class' ]}
{
public function __construct ( RequestContext \ $context )
{
\ $this -> context = \ $context ;
}
{ $this -> generateMatchMethod ( $supportsRedirections )}
}
EOF ;
}
public function addExpressionLanguageProvider ( ExpressionFunctionProviderInterface $provider )
{
$this -> expressionLanguageProviders [] = $provider ;
}
/**
* Generates the code for the match method implementing UrlMatcherInterface .
*
* @ param bool $supportsRedirections Whether redirections are supported by the base class
*
* @ return string Match method as PHP code
*/
private function generateMatchMethod ( $supportsRedirections )
{
$code = rtrim ( $this -> compileRoutes ( $this -> getRoutes (), $supportsRedirections ), " \n " );
return <<< EOF
2018-11-23 12:29:20 +00:00
public function match ( \ $rawPathinfo )
2015-08-17 17:00:26 -07:00
{
\ $allow = array ();
2018-11-23 12:29:20 +00:00
\ $pathinfo = rawurldecode ( \ $rawPathinfo );
\ $trimmedPathinfo = rtrim ( \ $pathinfo , '/' );
2015-08-17 17:00:26 -07:00
\ $context = \ $this -> context ;
2018-11-23 12:29:20 +00:00
\ $request = \ $this -> request ? : \ $this -> createRequest ( \ $pathinfo );
\ $requestMethod = \ $canonicalMethod = \ $context -> getMethod ();
if ( 'HEAD' === \ $requestMethod ) {
\ $canonicalMethod = 'GET' ;
}
2015-08-17 17:00:26 -07:00
$code
throw 0 < count ( \ $allow ) ? new MethodNotAllowedException ( array_unique ( \ $allow )) : new ResourceNotFoundException ();
}
EOF ;
}
/**
* Generates PHP code to match a RouteCollection with all its routes .
*
* @ param RouteCollection $routes A RouteCollection instance
* @ param bool $supportsRedirections Whether redirections are supported by the base class
*
* @ return string PHP code
*/
private function compileRoutes ( RouteCollection $routes , $supportsRedirections )
{
$fetchedHost = false ;
$groups = $this -> groupRoutesByHostRegex ( $routes );
$code = '' ;
foreach ( $groups as $collection ) {
if ( null !== $regex = $collection -> getAttribute ( 'host_regex' )) {
if ( ! $fetchedHost ) {
2018-11-23 12:29:20 +00:00
$code .= " \$ host = \$ context->getHost(); \n \n " ;
2015-08-17 17:00:26 -07:00
$fetchedHost = true ;
}
$code .= sprintf ( " if (preg_match(%s, \$ host, \$ hostMatches)) { \n " , var_export ( $regex , true ));
}
2018-11-23 12:29:20 +00:00
$tree = $this -> buildStaticPrefixCollection ( $collection );
$groupCode = $this -> compileStaticPrefixRoutes ( $tree , $supportsRedirections );
2015-08-17 17:00:26 -07:00
if ( null !== $regex ) {
// apply extra indention at each line (except empty ones)
$groupCode = preg_replace ( '/^.{2,}$/m' , ' $0' , $groupCode );
$code .= $groupCode ;
$code .= " } \n \n " ;
} else {
$code .= $groupCode ;
}
}
2018-11-23 12:29:20 +00:00
// used to display the Welcome Page in apps that don't define a homepage
$code .= " if ('/' === \$ pathinfo && ! \$ allow) { \n " ;
$code .= " throw new Symfony \ Component \R outing \ Exception \N oConfigurationException(); \n " ;
$code .= " } \n " ;
2015-08-17 17:00:26 -07:00
return $code ;
}
2018-11-23 12:29:20 +00:00
private function buildStaticPrefixCollection ( DumperCollection $collection )
{
$prefixCollection = new StaticPrefixCollection ();
foreach ( $collection as $dumperRoute ) {
$prefix = $dumperRoute -> getRoute () -> compile () -> getStaticPrefix ();
$prefixCollection -> addRoute ( $prefix , $dumperRoute );
}
$prefixCollection -> optimizeGroups ();
return $prefixCollection ;
}
2015-08-17 17:00:26 -07:00
/**
2018-11-23 12:29:20 +00:00
* Generates PHP code to match a tree of routes .
2015-08-17 17:00:26 -07:00
*
2018-11-23 12:29:20 +00:00
* @ param StaticPrefixCollection $collection A StaticPrefixCollection instance
2015-08-17 17:00:26 -07:00
* @ param bool $supportsRedirections Whether redirections are supported by the base class
2018-11-23 12:29:20 +00:00
* @ param string $ifOrElseIf either " if " or " elseif " to influence chaining
2015-08-17 17:00:26 -07:00
*
* @ return string PHP code
*/
2018-11-23 12:29:20 +00:00
private function compileStaticPrefixRoutes ( StaticPrefixCollection $collection , $supportsRedirections , $ifOrElseIf = 'if' )
2015-08-17 17:00:26 -07:00
{
$code = '' ;
$prefix = $collection -> getPrefix ();
2018-11-23 12:29:20 +00:00
if ( ! empty ( $prefix ) && '/' !== $prefix ) {
$code .= sprintf ( " %s (0 === strpos( \$ pathinfo, %s)) { \n " , $ifOrElseIf , var_export ( $prefix , true ));
2015-08-17 17:00:26 -07:00
}
2018-11-23 12:29:20 +00:00
$ifOrElseIf = 'if' ;
foreach ( $collection -> getItems () as $route ) {
if ( $route instanceof StaticPrefixCollection ) {
$code .= $this -> compileStaticPrefixRoutes ( $route , $supportsRedirections , $ifOrElseIf );
$ifOrElseIf = 'elseif' ;
2015-08-17 17:00:26 -07:00
} else {
2018-11-23 12:29:20 +00:00
$code .= $this -> compileRoute ( $route [ 1 ] -> getRoute (), $route [ 1 ] -> getName (), $supportsRedirections , $prefix ) . " \n " ;
$ifOrElseIf = 'if' ;
2015-08-17 17:00:26 -07:00
}
}
2018-11-23 12:29:20 +00:00
if ( ! empty ( $prefix ) && '/' !== $prefix ) {
2015-08-17 17:00:26 -07:00
$code .= " } \n \n " ;
// apply extra indention at each line (except empty ones)
$code = preg_replace ( '/^.{2,}$/m' , ' $0' , $code );
}
return $code ;
}
/**
* Compiles a single Route to PHP code used to match it against the path info .
*
* @ param Route $route A Route instance
* @ param string $name The name of the Route
* @ param bool $supportsRedirections Whether redirections are supported by the base class
* @ param string | null $parentPrefix The prefix of the parent collection used to optimize the code
*
* @ return string PHP code
*
* @ throws \LogicException
*/
private function compileRoute ( Route $route , $name , $supportsRedirections , $parentPrefix = null )
{
$code = '' ;
$compiledRoute = $route -> compile ();
$conditions = array ();
$hasTrailingSlash = false ;
$matches = false ;
$hostMatches = false ;
$methods = $route -> getMethods ();
2018-11-23 12:29:20 +00:00
$supportsTrailingSlash = $supportsRedirections && ( ! $methods || \in_array ( 'GET' , $methods ));
$regex = $compiledRoute -> getRegex ();
2015-08-17 17:00:26 -07:00
2018-11-23 12:29:20 +00:00
if ( ! \count ( $compiledRoute -> getPathVariables ()) && false !== preg_match ( '#^(.)\^(?P<url>.*?)\$\1#' . ( 'u' === substr ( $regex , - 1 ) ? 'u' : '' ), $regex , $m )) {
if ( $supportsTrailingSlash && '/' === substr ( $m [ 'url' ], - 1 )) {
$conditions [] = sprintf ( '%s === $trimmedPathinfo' , var_export ( rtrim ( str_replace ( '\\' , '' , $m [ 'url' ]), '/' ), true ));
2015-08-17 17:00:26 -07:00
$hasTrailingSlash = true ;
} else {
2018-11-23 12:29:20 +00:00
$conditions [] = sprintf ( '%s === $pathinfo' , var_export ( str_replace ( '\\' , '' , $m [ 'url' ]), true ));
2015-08-17 17:00:26 -07:00
}
} else {
if ( $compiledRoute -> getStaticPrefix () && $compiledRoute -> getStaticPrefix () !== $parentPrefix ) {
2015-08-27 12:03:05 -07:00
$conditions [] = sprintf ( '0 === strpos($pathinfo, %s)' , var_export ( $compiledRoute -> getStaticPrefix (), true ));
2015-08-17 17:00:26 -07:00
}
if ( $supportsTrailingSlash && $pos = strpos ( $regex , '/$' )) {
$regex = substr ( $regex , 0 , $pos ) . '/?$' . substr ( $regex , $pos + 2 );
$hasTrailingSlash = true ;
}
2015-08-27 12:03:05 -07:00
$conditions [] = sprintf ( 'preg_match(%s, $pathinfo, $matches)' , var_export ( $regex , true ));
2015-08-17 17:00:26 -07:00
$matches = true ;
}
if ( $compiledRoute -> getHostVariables ()) {
$hostMatches = true ;
}
if ( $route -> getCondition ()) {
$conditions [] = $this -> getExpressionLanguage () -> compile ( $route -> getCondition (), array ( 'context' , 'request' ));
}
$conditions = implode ( ' && ' , $conditions );
$code .= <<< EOF
// $name
if ( $conditions ) {
EOF ;
$gotoname = 'not_' . preg_replace ( '/[^A-Za-z0-9_]/' , '' , $name );
2018-11-23 12:29:20 +00:00
// the offset where the return value is appended below, with indendation
$retOffset = 12 + \strlen ( $code );
2015-08-17 17:00:26 -07:00
2018-11-23 12:29:20 +00:00
// optimize parameters array
if ( $matches || $hostMatches ) {
$vars = array ();
if ( $hostMatches ) {
$vars [] = '$hostMatches' ;
2015-08-17 17:00:26 -07:00
}
2018-11-23 12:29:20 +00:00
if ( $matches ) {
$vars [] = '$matches' ;
2015-08-17 17:00:26 -07:00
}
2018-11-23 12:29:20 +00:00
$vars [] = " array('_route' => ' $name ') " ;
$code .= sprintf (
" \$ ret = \$ this->mergeDefaults(array_replace(%s), %s); \n " ,
implode ( ', ' , $vars ),
str_replace ( " \n " , '' , var_export ( $route -> getDefaults (), true ))
);
} elseif ( $route -> getDefaults ()) {
$code .= sprintf ( " \$ ret = %s; \n " , str_replace ( " \n " , '' , var_export ( array_replace ( $route -> getDefaults (), array ( '_route' => $name )), true )));
} else {
$code .= sprintf ( " \$ ret = array('_route' => '%s'); \n " , $name );
2015-08-17 17:00:26 -07:00
}
if ( $hasTrailingSlash ) {
$code .= <<< EOF
2018-11-23 12:29:20 +00:00
if ( '/' === substr ( \ $pathinfo , - 1 )) {
// no-op
} elseif ( 'GET' !== \ $canonicalMethod ) {
goto $gotoname ;
} else {
return array_replace ( \ $ret , \ $this -> redirect ( \ $rawPathinfo . '/' , '$name' ));
2015-08-17 17:00:26 -07:00
}
EOF ;
}
2018-11-23 12:29:20 +00:00
if ( $methods ) {
$methodVariable = \in_array ( 'GET' , $methods ) ? '$canonicalMethod' : '$requestMethod' ;
$methods = implode ( " ', ' " , $methods );
}
2015-08-17 17:00:26 -07:00
if ( $schemes = $route -> getSchemes ()) {
if ( ! $supportsRedirections ) {
throw new \LogicException ( 'The "schemes" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.' );
}
$schemes = str_replace ( " \n " , '' , var_export ( array_flip ( $schemes ), true ));
2018-11-23 12:29:20 +00:00
if ( $methods ) {
$code .= <<< EOF
2015-08-17 17:00:26 -07:00
\ $requiredSchemes = $schemes ;
2018-11-23 12:29:20 +00:00
\ $hasRequiredScheme = isset ( \ $requiredSchemes [ \ $context -> getScheme ()]);
if ( ! in_array ( $methodVariable , array ( '$methods' ))) {
if ( \ $hasRequiredScheme ) {
\ $allow = array_merge ( \ $allow , array ( '$methods' ));
}
goto $gotoname ;
}
if ( ! \ $hasRequiredScheme ) {
if ( 'GET' !== \ $canonicalMethod ) {
goto $gotoname ;
}
return array_replace ( \ $ret , \ $this -> redirect ( \ $rawPathinfo , '$name' , key ( \ $requiredSchemes )));
2015-08-17 17:00:26 -07:00
}
EOF ;
2018-11-23 12:29:20 +00:00
} else {
$code .= <<< EOF
\ $requiredSchemes = $schemes ;
if ( ! isset ( \ $requiredSchemes [ \ $context -> getScheme ()])) {
if ( 'GET' !== \ $canonicalMethod ) {
goto $gotoname ;
}
2015-08-17 17:00:26 -07:00
2018-11-23 12:29:20 +00:00
return array_replace ( \ $ret , \ $this -> redirect ( \ $rawPathinfo , '$name' , key ( \ $requiredSchemes )));
2015-08-17 17:00:26 -07:00
}
2018-11-23 12:29:20 +00:00
EOF ;
}
} elseif ( $methods ) {
$code .= <<< EOF
if ( ! in_array ( $methodVariable , array ( '$methods' ))) {
\ $allow = array_merge ( \ $allow , array ( '$methods' ));
goto $gotoname ;
2015-08-17 17:00:26 -07:00
}
2018-11-23 12:29:20 +00:00
EOF ;
}
if ( $hasTrailingSlash || $schemes || $methods ) {
$code .= " return \$ ret; \n " ;
2015-08-17 17:00:26 -07:00
} else {
2018-11-23 12:29:20 +00:00
$code = substr_replace ( $code , 'return' , $retOffset , 6 );
2015-08-17 17:00:26 -07:00
}
$code .= " } \n " ;
2018-11-23 12:29:20 +00:00
if ( $hasTrailingSlash || $schemes || $methods ) {
2015-08-17 17:00:26 -07:00
$code .= " $gotoname : \n " ;
}
return $code ;
}
/**
* Groups consecutive routes having the same host regex .
*
* The result is a collection of collections of routes having the same host regex .
*
* @ param RouteCollection $routes A flat RouteCollection
*
* @ return DumperCollection A collection with routes grouped by host regex in sub - collections
*/
private function groupRoutesByHostRegex ( RouteCollection $routes )
{
$groups = new DumperCollection ();
$currentGroup = new DumperCollection ();
$currentGroup -> setAttribute ( 'host_regex' , null );
$groups -> add ( $currentGroup );
foreach ( $routes as $name => $route ) {
$hostRegex = $route -> compile () -> getHostRegex ();
if ( $currentGroup -> getAttribute ( 'host_regex' ) !== $hostRegex ) {
$currentGroup = new DumperCollection ();
$currentGroup -> setAttribute ( 'host_regex' , $hostRegex );
$groups -> add ( $currentGroup );
}
$currentGroup -> add ( new DumperRoute ( $name , $route ));
}
return $groups ;
}
private function getExpressionLanguage ()
{
if ( null === $this -> expressionLanguage ) {
if ( ! class_exists ( 'Symfony\Component\ExpressionLanguage\ExpressionLanguage' )) {
throw new \RuntimeException ( 'Unable to use expressions as the Symfony ExpressionLanguage component is not installed.' );
}
$this -> expressionLanguage = new ExpressionLanguage ( null , $this -> expressionLanguageProviders );
}
return $this -> expressionLanguage ;
}
}