Laravel 5.6 updates

Travis config update

Removed HHVM script as Laravel no longer support HHVM after releasing 5.3
This commit is contained in:
Manish Verma
2018-08-06 20:08:55 +05:30
parent 126fbb0255
commit 1ac0f42a58
2464 changed files with 65239 additions and 46734 deletions

View File

@@ -1,159 +0,0 @@
<?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;
/**
* Collection of routes.
*
* @author Arnaud Le Blanc <arnaud.lb@gmail.com>
*
* @internal
*/
class DumperCollection implements \IteratorAggregate
{
/**
* @var DumperCollection|null
*/
private $parent;
/**
* @var DumperCollection[]|DumperRoute[]
*/
private $children = array();
/**
* @var array
*/
private $attributes = array();
/**
* Returns the children routes and collections.
*
* @return self[]|DumperRoute[]
*/
public function all()
{
return $this->children;
}
/**
* Adds a route or collection.
*
* @param DumperRoute|DumperCollection The route or collection
*/
public function add($child)
{
if ($child instanceof self) {
$child->setParent($this);
}
$this->children[] = $child;
}
/**
* Sets children.
*
* @param array $children The children
*/
public function setAll(array $children)
{
foreach ($children as $child) {
if ($child instanceof self) {
$child->setParent($this);
}
}
$this->children = $children;
}
/**
* Returns an iterator over the children.
*
* @return \Iterator|DumperCollection[]|DumperRoute[] The iterator
*/
public function getIterator()
{
return new \ArrayIterator($this->children);
}
/**
* Returns the root of the collection.
*
* @return self The root collection
*/
public function getRoot()
{
return (null !== $this->parent) ? $this->parent->getRoot() : $this;
}
/**
* Returns the parent collection.
*
* @return self|null The parent collection or null if the collection has no parent
*/
protected function getParent()
{
return $this->parent;
}
/**
* Sets the parent collection.
*/
protected function setParent(self $parent)
{
$this->parent = $parent;
}
/**
* Returns true if the attribute is defined.
*
* @param string $name The attribute name
*
* @return bool true if the attribute is defined, false otherwise
*/
public function hasAttribute($name)
{
return array_key_exists($name, $this->attributes);
}
/**
* Returns an attribute by name.
*
* @param string $name The attribute name
* @param mixed $default Default value is the attribute doesn't exist
*
* @return mixed The attribute value
*/
public function getAttribute($name, $default = null)
{
return $this->hasAttribute($name) ? $this->attributes[$name] : $default;
}
/**
* Sets an attribute by name.
*
* @param string $name The attribute name
* @param mixed $value The attribute value
*/
public function setAttribute($name, $value)
{
$this->attributes[$name] = $value;
}
/**
* Sets multiple attributes.
*
* @param array $attributes The attributes
*/
public function setAttributes($attributes)
{
$this->attributes = $attributes;
}
}

View File

@@ -1,57 +0,0 @@
<?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;
use Symfony\Component\Routing\Route;
/**
* Container for a Route.
*
* @author Arnaud Le Blanc <arnaud.lb@gmail.com>
*
* @internal
*/
class DumperRoute
{
private $name;
private $route;
/**
* @param string $name The route name
* @param Route $route The route
*/
public function __construct($name, Route $route)
{
$this->name = $name;
$this->route = $route;
}
/**
* Returns the route name.
*
* @return string The route name
*/
public function getName()
{
return $this->name;
}
/**
* Returns the route.
*
* @return Route The route
*/
public function getRoute()
{
return $this->route;
}
}

View File

@@ -13,6 +13,7 @@ namespace Symfony\Component\Routing\Matcher\Dumper;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
@@ -22,10 +23,12 @@ use Symfony\Component\Routing\RouteCollection;
* @author Fabien Potencier <fabien@symfony.com>
* @author Tobias Schultze <http://tobion.de>
* @author Arnaud Le Blanc <arnaud.lb@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
class PhpMatcherDumper extends MatcherDumper
{
private $expressionLanguage;
private $signalingException;
/**
* @var ExpressionFunctionProviderInterface[]
@@ -53,7 +56,7 @@ class PhpMatcherDumper extends MatcherDumper
// 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']);
$supportsRedirections = isset($interfaces[RedirectableUrlMatcherInterface::class]);
return <<<EOF
<?php
@@ -86,70 +89,107 @@ EOF;
/**
* 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)
private function generateMatchMethod(bool $supportsRedirections): string
{
$code = rtrim($this->compileRoutes($this->getRoutes(), $supportsRedirections), "\n");
// Group hosts by same-suffix, re-order when possible
$matchHost = false;
$routes = new StaticPrefixCollection();
foreach ($this->getRoutes()->all() as $name => $route) {
if ($host = $route->getHost()) {
$matchHost = true;
$host = '/'.strtr(strrev($host), '}.{', '(/)');
}
return <<<EOF
public function match(\$rawPathinfo)
$routes->addRoute($host ?: '/(.*)', array($name, $route));
}
$routes = $matchHost ? $routes->populateCollection(new RouteCollection()) : $this->getRoutes();
$code = rtrim($this->compileRoutes($routes, $matchHost), "\n");
$fetchHost = $matchHost ? " \$host = strtolower(\$context->getHost());\n" : '';
$code = <<<EOF
{
\$allow = array();
\$allow = \$allowSchemes = array();
\$pathinfo = rawurldecode(\$rawPathinfo);
\$trimmedPathinfo = rtrim(\$pathinfo, '/');
\$context = \$this->context;
\$request = \$this->request ?: \$this->createRequest(\$pathinfo);
\$requestMethod = \$canonicalMethod = \$context->getMethod();
{$fetchHost}
if ('HEAD' === \$requestMethod) {
\$canonicalMethod = 'GET';
}
$code
throw 0 < count(\$allow) ? new MethodNotAllowedException(array_unique(\$allow)) : new ResourceNotFoundException();
}
EOF;
if ($supportsRedirections) {
return <<<'EOF'
public function match($pathinfo)
{
$allow = $allowSchemes = array();
if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
return $ret;
}
if ($allow) {
throw new MethodNotAllowedException(array_keys($allow));
}
if (!in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
// no-op
} elseif ($allowSchemes) {
redirect_scheme:
$scheme = $this->context->getScheme();
$this->context->setScheme(key($allowSchemes));
try {
if ($ret = $this->doMatch($pathinfo)) {
return $this->redirect($pathinfo, $ret['_route'], $this->context->getScheme()) + $ret;
}
} finally {
$this->context->setScheme($scheme);
}
} elseif ('/' !== $pathinfo) {
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
return $this->redirect($pathinfo, $ret['_route']) + $ret;
}
if ($allowSchemes) {
goto redirect_scheme;
}
}
throw new ResourceNotFoundException();
}
private function doMatch(string $rawPathinfo, array &$allow = array(), array &$allowSchemes = array()): ?array
EOF
.$code."\n return null;\n }";
}
return " public function match(\$rawPathinfo)\n".$code."\n throw \$allow ? new MethodNotAllowedException(array_keys(\$allow)) : new ResourceNotFoundException();\n }";
}
/**
* 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)
private function compileRoutes(RouteCollection $routes, bool $matchHost): string
{
$fetchedHost = false;
$groups = $this->groupRoutesByHostRegex($routes);
$code = '';
list($staticRoutes, $dynamicRoutes) = $this->groupStaticRoutes($routes);
foreach ($groups as $collection) {
if (null !== $regex = $collection->getAttribute('host_regex')) {
if (!$fetchedHost) {
$code .= " \$host = \$context->getHost();\n\n";
$fetchedHost = true;
$code = $this->compileStaticRoutes($staticRoutes, $matchHost);
$chunkLimit = \count($dynamicRoutes);
while (true) {
try {
$this->signalingException = new \RuntimeException('preg_match(): Compilation failed: regular expression is too large');
$code .= $this->compileDynamicRoutes($dynamicRoutes, $matchHost, $chunkLimit);
break;
} catch (\Exception $e) {
if (1 < $chunkLimit && $this->signalingException === $e) {
$chunkLimit = 1 + ($chunkLimit >> 1);
continue;
}
$code .= sprintf(" if (preg_match(%s, \$host, \$hostMatches)) {\n", var_export($regex, true));
}
$tree = $this->buildStaticPrefixCollection($collection);
$groupCode = $this->compileStaticPrefixRoutes($tree, $supportsRedirections);
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;
throw $e;
}
}
@@ -161,55 +201,383 @@ EOF;
return $code;
}
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;
}
/**
* Generates PHP code to match a tree of routes.
*
* @param StaticPrefixCollection $collection A StaticPrefixCollection instance
* @param bool $supportsRedirections Whether redirections are supported by the base class
* @param string $ifOrElseIf either "if" or "elseif" to influence chaining
*
* @return string PHP code
* Splits static routes from dynamic routes, so that they can be matched first, using a simple switch.
*/
private function compileStaticPrefixRoutes(StaticPrefixCollection $collection, $supportsRedirections, $ifOrElseIf = 'if')
private function groupStaticRoutes(RouteCollection $collection): array
{
$code = '';
$prefix = $collection->getPrefix();
$staticRoutes = $dynamicRegex = array();
$dynamicRoutes = new RouteCollection();
if (!empty($prefix) && '/' !== $prefix) {
$code .= sprintf(" %s (0 === strpos(\$pathinfo, %s)) {\n", $ifOrElseIf, var_export($prefix, true));
}
foreach ($collection->all() as $name => $route) {
$compiledRoute = $route->compile();
$hostRegex = $compiledRoute->getHostRegex();
$regex = $compiledRoute->getRegex();
if (!$compiledRoute->getPathVariables()) {
$host = !$compiledRoute->getHostVariables() ? $route->getHost() : '';
$url = $route->getPath();
foreach ($dynamicRegex as list($hostRx, $rx)) {
if (preg_match($rx, $url) && (!$host || !$hostRx || preg_match($hostRx, $host))) {
$dynamicRegex[] = array($hostRegex, $regex);
$dynamicRoutes->add($name, $route);
continue 2;
}
}
$ifOrElseIf = 'if';
foreach ($collection->getItems() as $route) {
if ($route instanceof StaticPrefixCollection) {
$code .= $this->compileStaticPrefixRoutes($route, $supportsRedirections, $ifOrElseIf);
$ifOrElseIf = 'elseif';
$staticRoutes[$url][$name] = $route;
} else {
$code .= $this->compileRoute($route[1]->getRoute(), $route[1]->getName(), $supportsRedirections, $prefix)."\n";
$ifOrElseIf = 'if';
$dynamicRegex[] = array($hostRegex, $regex);
$dynamicRoutes->add($name, $route);
}
}
if (!empty($prefix) && '/' !== $prefix) {
$code .= " }\n\n";
// apply extra indention at each line (except empty ones)
$code = preg_replace('/^.{2,}$/m', ' $0', $code);
return array($staticRoutes, $dynamicRoutes);
}
/**
* Compiles static routes in a switch statement.
*
* Condition-less paths are put in a static array in the switch's default, with generic matching logic.
* Paths that can match two or more routes, or have user-specified conditions are put in separate switch's cases.
*
* @throws \LogicException
*/
private function compileStaticRoutes(array $staticRoutes, bool $matchHost): string
{
if (!$staticRoutes) {
return '';
}
$code = $default = '';
foreach ($staticRoutes as $url => $routes) {
if (1 === \count($routes)) {
foreach ($routes as $name => $route) {
}
if (!$route->getCondition()) {
$defaults = $route->getDefaults();
if (isset($defaults['_canonical_route'])) {
$name = $defaults['_canonical_route'];
unset($defaults['_canonical_route']);
}
$default .= sprintf(
"%s => array(%s, %s, %s, %s),\n",
self::export($url),
self::export(array('_route' => $name) + $defaults),
self::export(!$route->compile()->getHostVariables() ? $route->getHost() : $route->compile()->getHostRegex() ?: null),
self::export(array_flip($route->getMethods()) ?: null),
self::export(array_flip($route->getSchemes()) ?: null)
);
continue;
}
}
$code .= sprintf(" case %s:\n", self::export($url));
foreach ($routes as $name => $route) {
$code .= $this->compileRoute($route, $name, true);
}
$code .= " break;\n";
}
if ($default) {
$code .= <<<EOF
default:
\$routes = array(
{$this->indent($default, 4)} );
if (!isset(\$routes[\$pathinfo])) {
break;
}
list(\$ret, \$requiredHost, \$requiredMethods, \$requiredSchemes) = \$routes[\$pathinfo];
{$this->compileSwitchDefault(false, $matchHost)}
EOF;
}
return sprintf(" switch (\$pathinfo) {\n%s }\n\n", $this->indent($code));
}
/**
* Compiles a regular expression followed by a switch statement to match dynamic routes.
*
* The regular expression matches both the host and the pathinfo at the same time. For stellar performance,
* it is built as a tree of patterns, with re-ordering logic to group same-prefix routes together when possible.
*
* Patterns are named so that we know which one matched (https://pcre.org/current/doc/html/pcre2syntax.html#SEC23).
* This name is used to "switch" to the additional logic required to match the final route.
*
* Condition-less paths are put in a static array in the switch's default, with generic matching logic.
* Paths that can match two or more routes, or have user-specified conditions are put in separate switch's cases.
*
* Last but not least:
* - Because it is not possibe to mix unicode/non-unicode patterns in a single regexp, several of them can be generated.
* - The same regexp can be used several times when the logic in the switch rejects the match. When this happens, the
* matching-but-failing subpattern is blacklisted by replacing its name by "(*F)", which forces a failure-to-match.
* To ease this backlisting operation, the name of subpatterns is also the string offset where the replacement should occur.
*/
private function compileDynamicRoutes(RouteCollection $collection, bool $matchHost, int $chunkLimit): string
{
if (!$collection->all()) {
return '';
}
$code = '';
$state = (object) array(
'regex' => '',
'switch' => '',
'default' => '',
'mark' => 0,
'markTail' => 0,
'hostVars' => array(),
'vars' => array(),
);
$state->getVars = static function ($m) use ($state) {
if ('_route' === $m[1]) {
return '?:';
}
$state->vars[] = $m[1];
return '';
};
$chunkSize = 0;
$prev = null;
$perModifiers = array();
foreach ($collection->all() as $name => $route) {
preg_match('#[a-zA-Z]*$#', $route->compile()->getRegex(), $rx);
if ($chunkLimit < ++$chunkSize || $prev !== $rx[0] && $route->compile()->getPathVariables()) {
$chunkSize = 1;
$routes = new RouteCollection();
$perModifiers[] = array($rx[0], $routes);
$prev = $rx[0];
}
$routes->add($name, $route);
}
foreach ($perModifiers as list($modifiers, $routes)) {
$prev = false;
$perHost = array();
foreach ($routes->all() as $name => $route) {
$regex = $route->compile()->getHostRegex();
if ($prev !== $regex) {
$routes = new RouteCollection();
$perHost[] = array($regex, $routes);
$prev = $regex;
}
$routes->add($name, $route);
}
$prev = false;
$rx = '{^(?';
$code .= "\n {$state->mark} => ".self::export($rx);
$state->mark += \strlen($rx);
$state->regex = $rx;
foreach ($perHost as list($hostRegex, $routes)) {
if ($matchHost) {
if ($hostRegex) {
preg_match('#^.\^(.*)\$.[a-zA-Z]*$#', $hostRegex, $rx);
$state->vars = array();
$hostRegex = '(?i:'.preg_replace_callback('#\?P<([^>]++)>#', $state->getVars, $rx[1]).')\.';
$state->hostVars = $state->vars;
} else {
$hostRegex = '(?:(?:[^./]*+\.)++)';
$state->hostVars = array();
}
$state->mark += \strlen($rx = ($prev ? ')' : '')."|{$hostRegex}(?");
$code .= "\n .".self::export($rx);
$state->regex .= $rx;
$prev = true;
}
$tree = new StaticPrefixCollection();
foreach ($routes->all() as $name => $route) {
preg_match('#^.\^(.*)\$.[a-zA-Z]*$#', $route->compile()->getRegex(), $rx);
$state->vars = array();
$regex = preg_replace_callback('#\?P<([^>]++)>#', $state->getVars, $rx[1]);
$tree->addRoute($regex, array($name, $regex, $state->vars, $route));
}
$code .= $this->compileStaticPrefixCollection($tree, $state);
}
if ($matchHost) {
$code .= "\n .')'";
$state->regex .= ')';
}
$rx = ")$}{$modifiers}";
$code .= "\n .'{$rx}',";
$state->regex .= $rx;
$state->markTail = 0;
// if the regex is too large, throw a signaling exception to recompute with smaller chunk size
set_error_handler(function ($type, $message) { throw 0 === strpos($message, $this->signalingException->getMessage()) ? $this->signalingException : new \ErrorException($message); });
try {
preg_match($state->regex, '');
} finally {
restore_error_handler();
}
}
if ($state->default) {
$state->switch .= <<<EOF
default:
\$routes = array(
{$this->indent($state->default, 4)} );
list(\$ret, \$vars, \$requiredMethods, \$requiredSchemes) = \$routes[\$m];
{$this->compileSwitchDefault(true, $matchHost)}
EOF;
}
$matchedPathinfo = $matchHost ? '$host.\'.\'.$pathinfo' : '$pathinfo';
unset($state->getVars);
return <<<EOF
\$matchedPathinfo = {$matchedPathinfo};
\$regexList = array({$code}
);
foreach (\$regexList as \$offset => \$regex) {
while (preg_match(\$regex, \$matchedPathinfo, \$matches)) {
switch (\$m = (int) \$matches['MARK']) {
{$this->indent($state->switch, 3)} }
if ({$state->mark} === \$m) {
break;
}
\$regex = substr_replace(\$regex, 'F', \$m - \$offset, 1 + strlen(\$m));
\$offset += strlen(\$m);
}
}
EOF;
}
/**
* Compiles a regexp tree of subpatterns that matches nested same-prefix routes.
*
* @param \stdClass $state A simple state object that keeps track of the progress of the compilation,
* and gathers the generated switch's "case" and "default" statements
*/
private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \stdClass $state, int $prefixLen = 0): string
{
$code = '';
$prevRegex = null;
$routes = $tree->getRoutes();
foreach ($routes as $i => $route) {
if ($route instanceof StaticPrefixCollection) {
$prevRegex = null;
$prefix = substr($route->getPrefix(), $prefixLen);
$state->mark += \strlen($rx = "|{$prefix}(?");
$code .= "\n .".self::export($rx);
$state->regex .= $rx;
$code .= $this->indent($this->compileStaticPrefixCollection($route, $state, $prefixLen + \strlen($prefix)));
$code .= "\n .')'";
$state->regex .= ')';
++$state->markTail;
continue;
}
list($name, $regex, $vars, $route) = $route;
$compiledRoute = $route->compile();
if ($compiledRoute->getRegex() === $prevRegex) {
$state->switch = substr_replace($state->switch, $this->compileRoute($route, $name, false)."\n", -19, 0);
continue;
}
$state->mark += 3 + $state->markTail + \strlen($regex) - $prefixLen;
$state->markTail = 2 + \strlen($state->mark);
$rx = sprintf('|%s(*:%s)', substr($regex, $prefixLen), $state->mark);
$code .= "\n .".self::export($rx);
$state->regex .= $rx;
$vars = array_merge($state->hostVars, $vars);
if (!$route->getCondition() && (!\is_array($next = $routes[1 + $i] ?? null) || $regex !== $next[1])) {
$prevRegex = null;
$defaults = $route->getDefaults();
if (isset($defaults['_canonical_route'])) {
$name = $defaults['_canonical_route'];
unset($defaults['_canonical_route']);
}
$state->default .= sprintf(
"%s => array(%s, %s, %s, %s),\n",
$state->mark,
self::export(array('_route' => $name) + $defaults),
self::export($vars),
self::export(array_flip($route->getMethods()) ?: null),
self::export(array_flip($route->getSchemes()) ?: null)
);
} else {
$prevRegex = $compiledRoute->getRegex();
$combine = ' $matches = array(';
foreach ($vars as $j => $m) {
$combine .= sprintf('%s => $matches[%d] ?? null, ', self::export($m), 1 + $j);
}
$combine = $vars ? substr_replace($combine, ");\n\n", -2) : '';
$state->switch .= <<<EOF
case {$state->mark}:
{$combine}{$this->compileRoute($route, $name, false)}
break;
EOF;
}
}
return $code;
}
/**
* A simple helper to compiles the switch's "default" for both static and dynamic routes.
*/
private function compileSwitchDefault(bool $hasVars, bool $matchHost): string
{
if ($hasVars) {
$code = <<<EOF
foreach (\$vars as \$i => \$v) {
if (isset(\$matches[1 + \$i])) {
\$ret[\$v] = \$matches[1 + \$i];
}
}
EOF;
} elseif ($matchHost) {
$code = <<<EOF
if (\$requiredHost) {
if ('#' !== \$requiredHost[0] ? \$requiredHost !== \$host : !preg_match(\$requiredHost, \$host, \$hostMatches)) {
break;
}
if ('#' === \$requiredHost[0] && \$hostMatches) {
\$hostMatches['_route'] = \$ret['_route'];
\$ret = \$this->mergeDefaults(\$hostMatches, \$ret);
}
}
EOF;
} else {
$code = '';
}
$code .= <<<EOF
\$hasRequiredScheme = !\$requiredSchemes || isset(\$requiredSchemes[\$context->getScheme()]);
if (\$requiredMethods && !isset(\$requiredMethods[\$canonicalMethod]) && !isset(\$requiredMethods[\$requestMethod])) {
if (\$hasRequiredScheme) {
\$allow += \$requiredMethods;
}
break;
}
if (!\$hasRequiredScheme) {
\$allowSchemes += \$requiredSchemes;
break;
}
return \$ret;
EOF;
return $code;
}
@@ -217,132 +585,97 @@ EOF;
/**
* 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)
private function compileRoute(Route $route, string $name, bool $checkHost): string
{
$code = '';
$compiledRoute = $route->compile();
$conditions = array();
$hasTrailingSlash = false;
$matches = false;
$hostMatches = false;
$methods = $route->getMethods();
$supportsTrailingSlash = $supportsRedirections && (!$methods || \in_array('GET', $methods));
$regex = $compiledRoute->getRegex();
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));
$hasTrailingSlash = true;
} else {
$conditions[] = sprintf('%s === $pathinfo', var_export(str_replace('\\', '', $m['url']), true));
}
} else {
if ($compiledRoute->getStaticPrefix() && $compiledRoute->getStaticPrefix() !== $parentPrefix) {
$conditions[] = sprintf('0 === strpos($pathinfo, %s)', var_export($compiledRoute->getStaticPrefix(), true));
}
if ($supportsTrailingSlash && $pos = strpos($regex, '/$')) {
$regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2);
$hasTrailingSlash = true;
}
$conditions[] = sprintf('preg_match(%s, $pathinfo, $matches)', var_export($regex, true));
$matches = true;
}
if ($compiledRoute->getHostVariables()) {
$hostMatches = true;
}
$matches = (bool) $compiledRoute->getPathVariables();
$hostMatches = (bool) $compiledRoute->getHostVariables();
$methods = array_flip($route->getMethods());
if ($route->getCondition()) {
$conditions[] = $this->getExpressionLanguage()->compile($route->getCondition(), array('context', 'request'));
$expression = $this->getExpressionLanguage()->compile($route->getCondition(), array('context', 'request'));
if (false !== strpos($expression, '$request')) {
$conditions[] = '($request = $request ?? $this->request ?: $this->createRequest($pathinfo))';
}
$conditions[] = $expression;
}
if (!$checkHost || !$compiledRoute->getHostRegex()) {
// no-op
} elseif ($hostMatches) {
$conditions[] = sprintf('preg_match(%s, $host, $hostMatches)', self::export($compiledRoute->getHostRegex()));
} else {
$conditions[] = sprintf('%s === $host', self::export($route->getHost()));
}
$conditions = implode(' && ', $conditions);
$code .= <<<EOF
if ($conditions) {
$code .= <<<EOF
// $name
if ($conditions) {
EOF;
} else {
$code .= " // {$name}\n";
}
$gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name);
// the offset where the return value is appended below, with indendation
$retOffset = 12 + \strlen($code);
$defaults = $route->getDefaults();
if (isset($defaults['_canonical_route'])) {
$name = $defaults['_canonical_route'];
unset($defaults['_canonical_route']);
}
// optimize parameters array
if ($matches || $hostMatches) {
$vars = array();
if ($hostMatches) {
$vars[] = '$hostMatches';
}
if ($matches) {
$vars = array("array('_route' => '$name')");
if ($matches || ($hostMatches && !$checkHost)) {
$vars[] = '$matches';
}
$vars[] = "array('_route' => '$name')";
if ($hostMatches && $checkHost) {
$vars[] = '$hostMatches';
}
$code .= sprintf(
" \$ret = \$this->mergeDefaults(array_replace(%s), %s);\n",
implode(', ', $vars),
str_replace("\n", '', var_export($route->getDefaults(), true))
" \$ret = \$this->mergeDefaults(%s, %s);\n",
implode(' + ', $vars),
self::export($defaults)
);
} elseif ($route->getDefaults()) {
$code .= sprintf(" \$ret = %s;\n", str_replace("\n", '', var_export(array_replace($route->getDefaults(), array('_route' => $name)), true)));
} elseif ($defaults) {
$code .= sprintf(" \$ret = %s;\n", self::export(array('_route' => $name) + $defaults));
} else {
$code .= sprintf(" \$ret = array('_route' => '%s');\n", $name);
}
if ($hasTrailingSlash) {
$code .= <<<EOF
if ('/' === substr(\$pathinfo, -1)) {
// no-op
} elseif ('GET' !== \$canonicalMethod) {
goto $gotoname;
} else {
return array_replace(\$ret, \$this->redirect(\$rawPathinfo.'/', '$name'));
}
EOF;
}
if ($methods) {
$methodVariable = \in_array('GET', $methods) ? '$canonicalMethod' : '$requestMethod';
$methods = implode("', '", $methods);
$methodVariable = isset($methods['GET']) ? '$canonicalMethod' : '$requestMethod';
$methods = self::export($methods);
}
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));
$schemes = self::export(array_flip($schemes));
if ($methods) {
$code .= <<<EOF
\$requiredSchemes = $schemes;
\$hasRequiredScheme = isset(\$requiredSchemes[\$context->getScheme()]);
if (!in_array($methodVariable, array('$methods'))) {
if (!isset((\$a = {$methods})[{$methodVariable}])) {
if (\$hasRequiredScheme) {
\$allow = array_merge(\$allow, array('$methods'));
\$allow += \$a;
}
goto $gotoname;
}
if (!\$hasRequiredScheme) {
if ('GET' !== \$canonicalMethod) {
goto $gotoname;
}
return array_replace(\$ret, \$this->redirect(\$rawPathinfo, '$name', key(\$requiredSchemes)));
\$allowSchemes += \$requiredSchemes;
goto $gotoname;
}
@@ -351,11 +684,8 @@ EOF;
$code .= <<<EOF
\$requiredSchemes = $schemes;
if (!isset(\$requiredSchemes[\$context->getScheme()])) {
if ('GET' !== \$canonicalMethod) {
goto $gotoname;
}
return array_replace(\$ret, \$this->redirect(\$rawPathinfo, '$name', key(\$requiredSchemes)));
\$allowSchemes += \$requiredSchemes;
goto $gotoname;
}
@@ -363,8 +693,8 @@ EOF;
}
} elseif ($methods) {
$code .= <<<EOF
if (!in_array($methodVariable, array('$methods'))) {
\$allow = array_merge(\$allow, array('$methods'));
if (!isset((\$a = {$methods})[{$methodVariable}])) {
\$allow += \$a;
goto $gotoname;
}
@@ -372,47 +702,22 @@ EOF;
EOF;
}
if ($hasTrailingSlash || $schemes || $methods) {
if ($schemes || $methods) {
$code .= " return \$ret;\n";
} else {
$code = substr_replace($code, 'return', $retOffset, 6);
}
$code .= " }\n";
if ($conditions) {
$code .= " }\n";
} elseif ($schemes || $methods) {
$code .= ' ';
}
if ($hasTrailingSlash || $schemes || $methods) {
if ($schemes || $methods) {
$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;
return $conditions ? $this->indent($code) : $code;
}
private function getExpressionLanguage()
@@ -426,4 +731,48 @@ EOF;
return $this->expressionLanguage;
}
private function indent($code, $level = 1)
{
return preg_replace('/^./m', str_repeat(' ', $level).'$0', $code);
}
/**
* @internal
*/
public static function export($value): string
{
if (null === $value) {
return 'null';
}
if (!\is_array($value)) {
if (\is_object($value)) {
throw new \InvalidArgumentException('Symfony\Component\Routing\Route cannot contain objects.');
}
return str_replace("\n", '\'."\n".\'', var_export($value, true));
}
if (!$value) {
return 'array()';
}
$i = 0;
$export = 'array(';
foreach ($value as $k => $v) {
if ($i === $k) {
++$i;
} else {
$export .= self::export($k).' => ';
if (\is_int($k) && $i < $k) {
$i = 1 + $k;
}
}
$export .= self::export($v).', ';
}
return substr_replace($export, ')', -2);
}
}

View File

@@ -11,44 +11,49 @@
namespace Symfony\Component\Routing\Matcher\Dumper;
use Symfony\Component\Routing\RouteCollection;
/**
* Prefix tree of routes preserving routes order.
*
* @author Frank de Jonge <info@frankdejonge.nl>
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class StaticPrefixCollection
{
/**
* @var string
*/
private $prefix;
/**
* @var array[]|StaticPrefixCollection[]
* @var string[]
*/
private $staticPrefixes = array();
/**
* @var string[]
*/
private $prefixes = array();
/**
* @var array[]|self[]
*/
private $items = array();
/**
* @var int
*/
private $matchStart = 0;
public function __construct($prefix = '')
public function __construct(string $prefix = '/')
{
$this->prefix = $prefix;
}
public function getPrefix()
public function getPrefix(): string
{
return $this->prefix;
}
/**
* @return mixed[]|StaticPrefixCollection[]
* @return array[]|self[]
*/
public function getItems()
public function getRoutes(): array
{
return $this->items;
}
@@ -56,183 +61,142 @@ class StaticPrefixCollection
/**
* Adds a route to a group.
*
* @param string $prefix
* @param mixed $route
* @param array|self $route
*/
public function addRoute($prefix, $route)
public function addRoute(string $prefix, $route)
{
$prefix = '/' === $prefix ? $prefix : rtrim($prefix, '/');
$this->guardAgainstAddingNotAcceptedRoutes($prefix);
list($prefix, $staticPrefix) = $this->getCommonPrefix($prefix, $prefix);
if ($this->prefix === $prefix) {
// When a prefix is exactly the same as the base we move up the match start position.
// This is needed because otherwise routes that come afterwards have higher precedence
// than a possible regular expression, which goes against the input order sorting.
$this->items[] = array($prefix, $route);
$this->matchStart = \count($this->items);
for ($i = \count($this->items) - 1; 0 <= $i; --$i) {
$item = $this->items[$i];
return;
}
list($commonPrefix, $commonStaticPrefix) = $this->getCommonPrefix($prefix, $this->prefixes[$i]);
if ($this->prefix === $commonPrefix) {
// the new route and a previous one have no common prefix, let's see if they are exclusive to each others
if ($this->prefix !== $staticPrefix && $this->prefix !== $this->staticPrefixes[$i]) {
// the new route and the previous one have exclusive static prefixes
continue;
}
if ($this->prefix === $staticPrefix && $this->prefix === $this->staticPrefixes[$i]) {
// the new route and the previous one have no static prefix
break;
}
if ($this->prefixes[$i] !== $this->staticPrefixes[$i] && $this->prefix === $this->staticPrefixes[$i]) {
// the previous route is non-static and has no static prefix
break;
}
if ($prefix !== $staticPrefix && $this->prefix === $staticPrefix) {
// the new route is non-static and has no static prefix
break;
}
foreach ($this->items as $i => $item) {
if ($i < $this->matchStart) {
continue;
}
if ($item instanceof self && $item->accepts($prefix)) {
if ($item instanceof self && $this->prefixes[$i] === $commonPrefix) {
// the new route is a child of a previous one, let's nest it
$item->addRoute($prefix, $route);
} else {
// the new route and a previous one have a common prefix, let's merge them
$child = new self($commonPrefix);
list($child->prefixes[0], $child->staticPrefixes[0]) = $child->getCommonPrefix($this->prefixes[$i], $this->prefixes[$i]);
list($child->prefixes[1], $child->staticPrefixes[1]) = $child->getCommonPrefix($prefix, $prefix);
$child->items = array($this->items[$i], $route);
return;
$this->staticPrefixes[$i] = $commonStaticPrefix;
$this->prefixes[$i] = $commonPrefix;
$this->items[$i] = $child;
}
$group = $this->groupWithItem($item, $prefix, $route);
if ($group instanceof self) {
$this->items[$i] = $group;
return;
}
return;
}
// No optimised case was found, in this case we simple add the route for possible
// grouping when new routes are added.
$this->items[] = array($prefix, $route);
$this->staticPrefixes[] = $staticPrefix;
$this->prefixes[] = $prefix;
$this->items[] = $route;
}
/**
* Tries to combine a route with another route or group.
*
* @param StaticPrefixCollection|array $item
* @param string $prefix
* @param mixed $route
*
* @return null|StaticPrefixCollection
* Linearizes back a set of nested routes into a collection.
*/
private function groupWithItem($item, $prefix, $route)
public function populateCollection(RouteCollection $routes): RouteCollection
{
$itemPrefix = $item instanceof self ? $item->prefix : $item[0];
$commonPrefix = $this->detectCommonPrefix($prefix, $itemPrefix);
if (!$commonPrefix) {
return;
foreach ($this->items as $route) {
if ($route instanceof self) {
$route->populateCollection($routes);
} else {
$routes->add(...$route);
}
}
$child = new self($commonPrefix);
if ($item instanceof self) {
$child->items = array($item);
} else {
$child->addRoute($item[0], $item[1]);
}
$child->addRoute($prefix, $route);
return $child;
return $routes;
}
/**
* Checks whether a prefix can be contained within the group.
* Gets the full and static common prefixes between two route patterns.
*
* @param string $prefix
*
* @return bool Whether a prefix could belong in a given group
* The static prefix stops at last at the first opening bracket.
*/
private function accepts($prefix)
{
return '' === $this->prefix || 0 === strpos($prefix, $this->prefix);
}
/**
* Detects whether there's a common prefix relative to the group prefix and returns it.
*
* @param string $prefix
* @param string $anotherPrefix
*
* @return false|string A common prefix, longer than the base/group prefix, or false when none available
*/
private function detectCommonPrefix($prefix, $anotherPrefix)
private function getCommonPrefix(string $prefix, string $anotherPrefix): array
{
$baseLength = \strlen($this->prefix);
$commonLength = $baseLength;
$end = min(\strlen($prefix), \strlen($anotherPrefix));
$staticLength = null;
set_error_handler(array(__CLASS__, 'handleError'));
for ($i = $baseLength; $i <= $end; ++$i) {
if (substr($prefix, 0, $i) !== substr($anotherPrefix, 0, $i)) {
for ($i = $baseLength; $i < $end && $prefix[$i] === $anotherPrefix[$i]; ++$i) {
if ('(' === $prefix[$i]) {
$staticLength = $staticLength ?? $i;
for ($j = 1 + $i, $n = 1; $j < $end && 0 < $n; ++$j) {
if ($prefix[$j] !== $anotherPrefix[$j]) {
break 2;
}
if ('(' === $prefix[$j]) {
++$n;
} elseif (')' === $prefix[$j]) {
--$n;
} elseif ('\\' === $prefix[$j] && (++$j === $end || $prefix[$j] !== $anotherPrefix[$j])) {
--$j;
break;
}
}
if (0 < $n) {
break;
}
if (('?' === ($prefix[$j] ?? '') || '?' === ($anotherPrefix[$j] ?? '')) && ($prefix[$j] ?? '') !== ($anotherPrefix[$j] ?? '')) {
break;
}
$subPattern = substr($prefix, $i, $j - $i);
if ($prefix !== $anotherPrefix && !preg_match('/^\(\[[^\]]++\]\+\+\)$/', $subPattern) && !preg_match('{(?<!'.$subPattern.')}', '')) {
// sub-patterns of variable length are not considered as common prefixes because their greediness would break in-order matching
break;
}
$i = $j - 1;
} elseif ('\\' === $prefix[$i] && (++$i === $end || $prefix[$i] !== $anotherPrefix[$i])) {
--$i;
break;
}
$commonLength = $i;
}
restore_error_handler();
if ($i < $end && 0b10 === (\ord($prefix[$i]) >> 6) && preg_match('//u', $prefix.' '.$anotherPrefix)) {
do {
// Prevent cutting in the middle of an UTF-8 characters
--$i;
} while (0b10 === (\ord($prefix[$i]) >> 6));
}
$commonPrefix = rtrim(substr($prefix, 0, $commonLength), '/');
if (\strlen($commonPrefix) > $baseLength) {
return $commonPrefix;
}
return false;
return array(substr($prefix, 0, $i), substr($prefix, 0, $staticLength ?? $i));
}
/**
* Optimizes the tree by inlining items from groups with less than 3 items.
*/
public function optimizeGroups()
public static function handleError($type, $msg)
{
$index = -1;
while (isset($this->items[++$index])) {
$item = $this->items[$index];
if ($item instanceof self) {
$item->optimizeGroups();
// When a group contains only two items there's no reason to optimize because at minimum
// the amount of prefix check is 2. In this case inline the group.
if ($item->shouldBeInlined()) {
array_splice($this->items, $index, 1, $item->items);
// Lower index to pass through the same index again after optimizing.
// The first item of the replacements might be a group needing optimization.
--$index;
}
}
}
}
private function shouldBeInlined()
{
if (\count($this->items) >= 3) {
return false;
}
foreach ($this->items as $item) {
if ($item instanceof self) {
return true;
}
}
foreach ($this->items as $item) {
if (\is_array($item) && $item[0] === $this->prefix) {
return false;
}
}
return true;
}
/**
* Guards against adding incompatible prefixes in a group.
*
* @param string $prefix
*
* @throws \LogicException when a prefix does not belong in a group
*/
private function guardAgainstAddingNotAcceptedRoutes($prefix)
{
if (!$this->accepts($prefix)) {
$message = sprintf('Could not add route with prefix %s to collection with prefix %s', $prefix, $this->prefix);
throw new \LogicException($message);
}
return 0 === strpos($msg, 'preg_match(): Compilation failed: lookbehind assertion is not fixed length');
}
}

View File

@@ -11,8 +11,8 @@
namespace Symfony\Component\Routing\Matcher;
use Symfony\Component\Routing\Exception\ExceptionInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Route;
/**
* @author Fabien Potencier <fabien@symfony.com>
@@ -25,41 +25,40 @@ abstract class RedirectableUrlMatcher extends UrlMatcher implements Redirectable
public function match($pathinfo)
{
try {
$parameters = parent::match($pathinfo);
return parent::match($pathinfo);
} catch (ResourceNotFoundException $e) {
if ('/' === substr($pathinfo, -1) || !\in_array($this->context->getMethod(), array('HEAD', 'GET'))) {
if (!\in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
throw $e;
}
try {
$parameters = parent::match($pathinfo.'/');
if ($this->allowSchemes) {
redirect_scheme:
$scheme = $this->context->getScheme();
$this->context->setScheme(current($this->allowSchemes));
try {
$ret = parent::match($pathinfo);
return array_replace($parameters, $this->redirect($pathinfo.'/', isset($parameters['_route']) ? $parameters['_route'] : null));
} catch (ResourceNotFoundException $e2) {
return $this->redirect($pathinfo, $ret['_route'] ?? null, $this->context->getScheme()) + $ret;
} catch (ExceptionInterface $e2) {
throw $e;
} finally {
$this->context->setScheme($scheme);
}
} elseif ('/' === $pathinfo) {
throw $e;
} else {
try {
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
$ret = parent::match($pathinfo);
return $this->redirect($pathinfo, $ret['_route'] ?? null) + $ret;
} catch (ExceptionInterface $e2) {
if ($this->allowSchemes) {
goto redirect_scheme;
}
throw $e;
}
}
}
return $parameters;
}
/**
* {@inheritdoc}
*/
protected function handleRouteRequirements($pathinfo, $name, Route $route)
{
// expression condition
if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), array('context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)))) {
return array(self::REQUIREMENT_MISMATCH, null);
}
// check HTTP scheme requirement
$scheme = $this->context->getScheme();
$schemes = $route->getSchemes();
if ($schemes && !$route->hasScheme($scheme)) {
return array(self::ROUTE_MATCH, $this->redirect($pathinfo, $name, current($schemes)));
}
return array(self::REQUIREMENT_MATCH, null);
}
}

View File

@@ -33,7 +33,19 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
const ROUTE_MATCH = 2;
protected $context;
/**
* Collects HTTP methods that would be allowed for the request.
*/
protected $allow = array();
/**
* Collects URI schemes that would be allowed for the request.
*
* @internal
*/
protected $allowSchemes = array();
protected $routes;
protected $request;
protected $expressionLanguage;
@@ -70,7 +82,7 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
*/
public function match($pathinfo)
{
$this->allow = array();
$this->allow = $this->allowSchemes = array();
if ($ret = $this->matchCollection(rawurldecode($pathinfo), $this->routes)) {
return $ret;
@@ -141,7 +153,7 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
continue;
}
// check HTTP method requirement
$hasRequiredScheme = !$route->getSchemes() || $route->hasScheme($this->context->getScheme());
if ($requiredMethods = $route->getMethods()) {
// HEAD and GET are equivalent as per RFC
if ('HEAD' === $method = $this->context->getMethod()) {
@@ -149,7 +161,7 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
}
if (!\in_array($method, $requiredMethods)) {
if (self::REQUIREMENT_MATCH === $status[0]) {
if ($hasRequiredScheme) {
$this->allow = array_merge($this->allow, $requiredMethods);
}
@@ -157,6 +169,12 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
}
}
if (!$hasRequiredScheme) {
$this->allowSchemes = array_merge($this->allowSchemes, $route->getSchemes());
continue;
}
return $this->getAttributes($route, $name, array_replace($matches, $hostMatches, isset($status[1]) ? $status[1] : array()));
}
}
@@ -176,9 +194,14 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
*/
protected function getAttributes(Route $route, $name, array $attributes)
{
$defaults = $route->getDefaults();
if (isset($defaults['_canonical_route'])) {
$name = $defaults['_canonical_route'];
unset($defaults['_canonical_route']);
}
$attributes['_route'] = $name;
return $this->mergeDefaults($attributes, $route->getDefaults());
return $this->mergeDefaults($attributes, $defaults);
}
/**
@@ -197,11 +220,7 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
return array(self::REQUIREMENT_MISMATCH, null);
}
// check HTTP scheme requirement
$scheme = $this->context->getScheme();
$status = $route->getSchemes() && !$route->hasScheme($scheme) ? self::REQUIREMENT_MISMATCH : self::REQUIREMENT_MATCH;
return array($status, null);
return array(self::REQUIREMENT_MATCH, null);
}
/**
@@ -215,7 +234,7 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
protected function mergeDefaults($params, $defaults)
{
foreach ($params as $key => $value) {
if (!\is_int($key)) {
if (!\is_int($key) && null !== $value) {
$defaults[$key] = $value;
}
}