From f933f7048330ee168050ad2f174b1be0d6486d1a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 29 Jan 2018 18:46:50 +0100 Subject: [PATCH] [Routing] Match 77.7x faster by compiling routes in one regexp --- .../Generator/Dumper/PhpGeneratorDumper.php | 4 +- .../Matcher/Dumper/DumperCollection.php | 159 ----- .../Routing/Matcher/Dumper/DumperRoute.php | 43 -- .../Matcher/Dumper/PhpMatcherDumper.php | 674 +++++++++++++----- .../Matcher/Dumper/StaticPrefixCollection.php | 175 ++--- .../Component/Routing/Matcher/UrlMatcher.php | 2 +- .../Tests/Fixtures/dumper/url_matcher0.php | 3 +- .../Tests/Fixtures/dumper/url_matcher1.php | 455 +++++------- .../Tests/Fixtures/dumper/url_matcher2.php | 552 ++++++-------- .../Tests/Fixtures/dumper/url_matcher3.php | 73 +- .../Tests/Fixtures/dumper/url_matcher4.php | 94 +-- .../Tests/Fixtures/dumper/url_matcher5.php | 268 +++---- .../Tests/Fixtures/dumper/url_matcher6.php | 236 ++---- .../Tests/Fixtures/dumper/url_matcher7.php | 312 +++----- .../Tests/Fixtures/dumper/url_matcher8.php | 79 ++ .../Tests/Fixtures/dumper/url_matcher9.php | 50 ++ .../Matcher/Dumper/DumperCollectionTest.php | 34 - .../Matcher/Dumper/PhpMatcherDumperTest.php | 17 +- .../Dumper/StaticPrefixCollectionTest.php | 84 +-- .../Routing/Tests/Matcher/UrlMatcherTest.php | 25 + 20 files changed, 1548 insertions(+), 1791 deletions(-) delete mode 100644 src/Symfony/Component/Routing/Matcher/Dumper/DumperCollection.php delete mode 100644 src/Symfony/Component/Routing/Matcher/Dumper/DumperRoute.php create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher8.php create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher9.php delete mode 100644 src/Symfony/Component/Routing/Tests/Matcher/Dumper/DumperCollectionTest.php diff --git a/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php b/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php index 60bdf1da35..0cb87f1163 100644 --- a/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php +++ b/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Routing\Generator\Dumper; +use Symfony\Component\Routing\Matcher\Dumper\PhpMatcherDumper; + /** * PhpGeneratorDumper creates a PHP class able to generate URLs for a given set of routes. * @@ -88,7 +90,7 @@ EOF; $properties[] = $compiledRoute->getHostTokens(); $properties[] = $route->getSchemes(); - $routes .= sprintf(" '%s' => %s,\n", $name, str_replace("\n", '', var_export($properties, true))); + $routes .= sprintf(" '%s' => %s,\n", $name, PhpMatcherDumper::export($properties)); } $routes .= ' )'; diff --git a/src/Symfony/Component/Routing/Matcher/Dumper/DumperCollection.php b/src/Symfony/Component/Routing/Matcher/Dumper/DumperCollection.php deleted file mode 100644 index 6916297b8c..0000000000 --- a/src/Symfony/Component/Routing/Matcher/Dumper/DumperCollection.php +++ /dev/null @@ -1,159 +0,0 @@ - - * - * 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 - * - * @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(DumperCollection $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; - } -} diff --git a/src/Symfony/Component/Routing/Matcher/Dumper/DumperRoute.php b/src/Symfony/Component/Routing/Matcher/Dumper/DumperRoute.php deleted file mode 100644 index 948bef9f12..0000000000 --- a/src/Symfony/Component/Routing/Matcher/Dumper/DumperRoute.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * 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 - * - * @internal - */ -class DumperRoute -{ - private $name; - private $route; - - public function __construct(string $name, Route $route) - { - $this->name = $name; - $this->route = $route; - } - - public function getName(): string - { - return $this->name; - } - - public function getRoute(): Route - { - return $this->route; - } -} diff --git a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php index 5d1839b2de..42e5ed80b0 100644 --- a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php +++ b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php @@ -22,6 +22,7 @@ use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; * @author Fabien Potencier * @author Tobias Schultze * @author Arnaud Le Blanc + * @author Nicolas Grekas */ class PhpMatcherDumper extends MatcherDumper { @@ -93,7 +94,21 @@ EOF; */ private function generateMatchMethod($supportsRedirections) { - $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 = '/'.str_replace('.', '/', rtrim(explode('}', strrev($host), 2)[0], '.')); + } + + $routes->addRoute($host ?: '/', array($name, $route)); + } + $routes = $matchHost ? $routes->populateCollection(new RouteCollection()) : $this->getRoutes(); + + $code = rtrim($this->compileRoutes($routes, $supportsRedirections, $matchHost), "\n"); + $fetchHost = $matchHost ? " \$host = strtolower(\$context->getHost());\n" : ''; return <<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(); + throw \$allow ? new MethodNotAllowedException(array_keys(\$allow)) : new ResourceNotFoundException(); } EOF; } @@ -124,34 +138,12 @@ EOF; * * @return string PHP code */ - private function compileRoutes(RouteCollection $routes, $supportsRedirections) + private function compileRoutes(RouteCollection $routes, $supportsRedirections, $matchHost) { - $fetchedHost = false; - $groups = $this->groupRoutesByHostRegex($routes); - $code = ''; + list($staticRoutes, $dynamicRoutes) = $this->groupStaticRoutes($routes, $supportsRedirections); - foreach ($groups as $collection) { - if (null !== $regex = $collection->getAttribute('host_regex')) { - if (!$fetchedHost) { - $code .= " \$host = \$context->getHost();\n\n"; - $fetchedHost = true; - } - - $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; - } - } + $code = $this->compileStaticRoutes($staticRoutes, $supportsRedirections, $matchHost); + $code .= $this->compileDynamicRoutes($dynamicRoutes, $supportsRedirections, $matchHost); if ('' === $code) { $code .= " if ('/' === \$pathinfo) {\n"; @@ -162,55 +154,391 @@ 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, bool $supportsRedirections): 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 ($hasTrailingSlash = $supportsRedirections && $pos = strpos($regex, '/$')) { + $regex = substr_replace($regex, '/?$', $pos, 2); + } + if (!$compiledRoute->getPathVariables()) { + $host = !$compiledRoute->getHostVariables() ? $route->getHost() : ''; + $url = $route->getPath(); + if ($hasTrailingSlash) { + $url = rtrim($url, '/'); + } + 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] = array($hasTrailingSlash, $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 $supportsRedirections, bool $matchHost): string + { + if (!$staticRoutes) { + return ''; } + $code = $default = ''; + $checkTrailingSlash = false; + + foreach ($staticRoutes as $url => $routes) { + if (1 === count($routes)) { + foreach ($routes as $name => list($hasTrailingSlash, $route)) { + } + + if (!$route->getCondition()) { + if (!$supportsRedirections && $route->getSchemes()) { + throw new \LogicException('The "schemes" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.'); + } + $checkTrailingSlash = $checkTrailingSlash || $hasTrailingSlash; + $default .= sprintf( + "%s => array(%s, %s, %s, %s),\n", + self::export($url), + self::export(array('_route' => $name) + $route->getDefaults()), + 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).($hasTrailingSlash ? ', true' : '') + ); + continue; + } + } + + $code .= sprintf(" case %s:\n", self::export($url)); + foreach ($routes as $name => list($hasTrailingSlash, $route)) { + $code .= $this->compileRoute($route, $name, $supportsRedirections, $hasTrailingSlash); + } + $code .= " break;\n"; + } + + $matchedPathinfo = $supportsRedirections ? '$trimmedPathinfo' : '$pathinfo'; + + if ($default) { + $code .= <<indent($default, 4)} ); + + if (!isset(\$routes[{$matchedPathinfo}])) { + break; + } + list(\$ret, \$requiredHost, \$requiredMethods, \$requiredSchemes) = \$routes[{$matchedPathinfo}]; +{$this->compileSwitchDefault(false, $matchedPathinfo, $matchHost, $supportsRedirections, $checkTrailingSlash)} +EOF; + } + + return sprintf(" switch (%s) {\n%s }\n\n", $matchedPathinfo, $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 $supportsRedirections, bool $matchHost): string + { + if (!$collection->all()) { + return ''; + } + $code = ''; + $state = (object) array( + 'switch' => '', + 'default' => '', + 'mark' => 0, + 'markTail' => 0, + 'supportsRedirections' => $supportsRedirections, + 'checkTrailingSlash' => false, + 'hostVars' => array(), + 'vars' => array(), + ); + $state->getVars = static function ($m) use ($state) { + if ('_route' === $m[1]) { + return '?:'; + } + + $state->vars[] = $m[1]; + + return ''; + }; + + $prev = null; + $perModifiers = array(); + foreach ($collection->all() as $name => $route) { + preg_match('#[a-zA-Z]*$#', $route->compile()->getRegex(), $rx); + if ($prev !== $rx[0] && $route->compile()->getPathVariables()) { + $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; + $code .= "\n {$state->mark} => '{^(?'"; + $state->mark += 4; + + 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 += 3 + $prev + strlen($hostRegex); + $code .= "\n .".self::export(($prev ? ')' : '')."|{$hostRegex}(?"); + $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 .')'"; + } + $code .= "\n .')$}{$modifiers}',"; + } + + if ($state->default) { + $state->switch .= <<indent($state->default, 4)} ); + + list(\$ret, \$vars, \$requiredMethods, \$requiredSchemes) = \$routes[\$m]; +{$this->compileSwitchDefault(true, '$m', $matchHost, $supportsRedirections, $state->checkTrailingSlash)} +EOF; + } + + $matchedPathinfo = $matchHost ? '$host.$pathinfo' : '$pathinfo'; + unset($state->getVars); + + return << \$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) + { + $code = ''; + $prevRegex = null; + $routes = $tree->getRoutes(); + + foreach ($routes as $i => $route) { + if ($route instanceof StaticPrefixCollection) { + $prevRegex = null; + $prefix = substr($route->getPrefix(), $prefixLen); + $state->mark += 3 + strlen($prefix); + $code .= "\n .".self::export("|{$prefix}(?"); + $code .= $this->indent($this->compileStaticPrefixCollection($route, $state, $prefixLen + strlen($prefix))); + $code .= "\n .')'"; + $state->markTail += 1; + continue; + } + + list($name, $regex, $vars, $route) = $route; + $compiledRoute = $route->compile(); + $hasTrailingSlash = $state->supportsRedirections && '' !== $regex && '/' === $regex[-1]; + + if ($compiledRoute->getRegex() === $prevRegex) { + $state->switch = substr_replace($state->switch, $this->compileRoute($route, $name, $state->supportsRedirections, $hasTrailingSlash)."\n", -19, 0); + continue; + } + + $methods = array_flip($route->getMethods()); + $hasTrailingSlash = $hasTrailingSlash && (!$methods || isset($methods['GET'])); + $state->mark += 3 + $state->markTail + $hasTrailingSlash + strlen($regex) - $prefixLen; + $state->markTail = 2 + strlen($state->mark); + $code .= "\n ."; + $code .= self::export(sprintf('|%s(*:%s)', substr($regex, $prefixLen).($hasTrailingSlash ? '?' : ''), $state->mark)); + $vars = array_merge($state->hostVars, $vars); + + if (!$route->getCondition() && (!is_array($next = $routes[1 + $i] ?? null) || $regex !== $next[1])) { + $prevRegex = null; + $state->checkTrailingSlash = $state->checkTrailingSlash || $hasTrailingSlash; + $state->default .= sprintf( + "%s => array(%s, %s, %s, %s),\n", + $state->mark, + self::export(array('_route' => $name) + $route->getDefaults()), + self::export($vars), + self::export($methods ?: null), + self::export(array_flip($route->getSchemes()) ?: null).($hasTrailingSlash ? ', true' : '') + ); + } 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 .= <<mark}: +{$combine}{$this->compileRoute($route, $name, $state->supportsRedirections, $hasTrailingSlash)} + break; + +EOF; + } + } + + return $code; + } + + /** + * A simple helper to compiles the switch's "default" for both static and dynamic routes. + */ + private function compileSwitchDefault(bool $hasVars, string $routesKey, bool $matchHost, bool $supportsRedirections, bool $checkTrailingSlash) + { + if ($hasVars) { + $code = << \$v) { + if (isset(\$matches[1 + \$i])) { + \$ret[\$v] = \$matches[1 + \$i]; + } + } + +EOF; + } elseif ($matchHost) { + $code = <<mergeDefaults(\$hostMatches, \$ret); + } + } + +EOF; + } else { + $code = ''; + } + if ($supportsRedirections && $checkTrailingSlash) { + $code .= <<redirect(\$rawPathinfo.'/', \$ret['_route'])); + } + +EOF; + } + if ($supportsRedirections) { + $code .= <<getScheme()])) { + if ('GET' !== \$canonicalMethod) { + \$allow['GET'] = 'GET'; + break; + } + + return array_replace(\$ret, \$this->redirect(\$rawPathinfo, \$ret['_route'], key(\$requiredSchemes))); + } + +EOF; + } + $code .= <<compile(); $conditions = array(); - $hasTrailingSlash = false; - $matches = false; - $hostMatches = false; - $methods = $route->getMethods(); + $matches = (bool) $compiledRoute->getPathVariables(); + $hostMatches = (bool) $compiledRoute->getHostVariables(); + $methods = array_flip($route->getMethods()); + $supportsTrailingSlash = $supportsRedirections && (!$methods || isset($methods['GET'])); - $supportsTrailingSlash = $supportsRedirections && (!$methods || in_array('GET', $methods)); - $regex = $compiledRoute->getRegex(); - - if (!count($compiledRoute->getPathVariables()) && false !== preg_match('#^(.)\^(?P.*?)\$\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; + if ($hasTrailingSlash && !$supportsTrailingSlash) { + $hasTrailingSlash = false; + $conditions[] = "'/' === \$pathinfo[-1]"; } 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 (!$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 .= <<mergeDefaults(array_replace(%s), %s);\n", implode(', ', $vars), - str_replace("\n", '', var_export($route->getDefaults(), true)) + self::export($route->getDefaults()) ); } elseif ($route->getDefaults()) { - $code .= sprintf(" \$ret = %s;\n", str_replace("\n", '', var_export(array_replace($route->getDefaults(), array('_route' => $name)), true))); + $code .= sprintf(" \$ret = %s;\n", self::export(array_replace($route->getDefaults(), array('_route' => $name)))); } else { $code .= sprintf(" \$ret = array('_route' => '%s');\n", $name); } if ($hasTrailingSlash) { $code .= <<redirect(\$rawPathinfo.'/', '$name')); @@ -323,12 +645,12 @@ EOF; 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)); $code .= <<getScheme()])) { if ('GET' !== \$canonicalMethod) { - \$allow[] = 'GET'; + \$allow['GET'] = 'GET'; goto $gotoname; } @@ -340,56 +662,17 @@ EOF; } if ($methods) { - if (1 === count($methods)) { - if ('HEAD' === $methods[0]) { - $code .= <<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() @@ -446,4 +704,44 @@ 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)) { + 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); + } } diff --git a/src/Symfony/Component/Routing/Matcher/Dumper/StaticPrefixCollection.php b/src/Symfony/Component/Routing/Matcher/Dumper/StaticPrefixCollection.php index e0117890cd..dbc42caf52 100644 --- a/src/Symfony/Component/Routing/Matcher/Dumper/StaticPrefixCollection.php +++ b/src/Symfony/Component/Routing/Matcher/Dumper/StaticPrefixCollection.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Routing\Matcher\Dumper; +use Symfony\Component\Routing\RouteCollection; + /** * Prefix tree of routes preserving routes order. * @@ -20,24 +22,24 @@ namespace Symfony\Component\Routing\Matcher\Dumper; */ class StaticPrefixCollection { - /** - * @var string - */ private $prefix; + private $staticPrefix; + private $matchStart = 0; /** - * @var array[]|StaticPrefixCollection[] + * @var string[] + */ + private $prefixes = array(); + + /** + * @var array[]|self[] */ private $items = array(); - /** - * @var int - */ - private $matchStart = 0; - - public function __construct(string $prefix = '') + public function __construct(string $prefix = '/', string $staticPrefix = '/') { $this->prefix = $prefix; + $this->staticPrefix = $staticPrefix; } public function getPrefix(): string @@ -46,9 +48,9 @@ class StaticPrefixCollection } /** - * @return mixed[]|StaticPrefixCollection[] + * @return array[]|self[] */ - public function getItems(): array + public function getRoutes(): array { return $this->items; } @@ -56,28 +58,26 @@ class StaticPrefixCollection /** * Adds a route to a group. * - * @param string $prefix - * @param mixed $route + * @param array|self $route */ public function addRoute(string $prefix, $route) { - $prefix = '/' === $prefix ? $prefix : rtrim($prefix, '/'); $this->guardAgainstAddingNotAcceptedRoutes($prefix); + list($prefix, $staticPrefix) = $this->detectCommonPrefix($prefix, $prefix) ?: array(rtrim($prefix, '/') ?: '/', '/'); - if ($this->prefix === $prefix) { + if ($this->staticPrefix === $staticPrefix) { // 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->prefixes[] = $prefix; + $this->items[] = $route; $this->matchStart = count($this->items); return; } - foreach ($this->items as $i => $item) { - if ($i < $this->matchStart) { - continue; - } + for ($i = $this->matchStart; $i < \count($this->items); ++$i) { + $item = $this->items[$i]; if ($item instanceof self && $item->accepts($prefix)) { $item->addRoute($prefix, $route); @@ -85,9 +85,8 @@ class StaticPrefixCollection return; } - $group = $this->groupWithItem($item, $prefix, $route); - - if ($group instanceof self) { + if ($group = $this->groupWithItem($i, $prefix, $route)) { + $this->prefixes[$i] = $group->getPrefix(); $this->items[$i] = $group; return; @@ -96,33 +95,43 @@ class StaticPrefixCollection // 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->prefixes[] = $prefix; + $this->items[] = $route; + } + + /** + * Linearizes back a set of nested routes into a collection. + */ + public function populateCollection(RouteCollection $routes): RouteCollection + { + foreach ($this->items as $route) { + if ($route instanceof self) { + $route->populateCollection($routes); + } else { + $routes->add(...$route); + } + } + + return $routes; } /** * Tries to combine a route with another route or group. - * - * @param StaticPrefixCollection|array $item - * @param string $prefix - * @param mixed $route - * - * @return null|StaticPrefixCollection */ - private function groupWithItem($item, string $prefix, $route) + private function groupWithItem(int $i, string $prefix, $route): ?self { - $itemPrefix = $item instanceof self ? $item->prefix : $item[0]; - $commonPrefix = $this->detectCommonPrefix($prefix, $itemPrefix); - - if (!$commonPrefix) { - return; + if (!$commonPrefix = $this->detectCommonPrefix($prefix, $this->prefixes[$i])) { + return null; } - $child = new self($commonPrefix); + $child = new self(...$commonPrefix); + $item = $this->items[$i]; if ($item instanceof self) { + $child->prefixes = array($commonPrefix[0]); $child->items = array($item); } else { - $child->addRoute($item[0], $item[1]); + $child->addRoute($this->prefixes[$i], $item); } $child->addRoute($prefix, $route); @@ -141,76 +150,48 @@ class StaticPrefixCollection /** * Detects whether there's a common prefix relative to the group prefix and returns it. * - * @return false|string A common prefix, longer than the base/group prefix, or false when none available + * @return null|array A common prefix, longer than the base/group prefix, or null when none available */ - private function detectCommonPrefix(string $prefix, string $anotherPrefix) + private function detectCommonPrefix(string $prefix, string $anotherPrefix): ?array { $baseLength = strlen($this->prefix); - $commonLength = $baseLength; $end = min(strlen($prefix), strlen($anotherPrefix)); + $staticLength = null; - 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; + } + $i = $j; + } elseif ('\\' === $prefix[$i] && (++$i === $end || $prefix[$i] !== $anotherPrefix[$i])) { + --$i; break; } - - $commonLength = $i; } - $commonPrefix = rtrim(substr($prefix, 0, $commonLength), '/'); + $staticLength = $staticLength ?? $i; + $commonPrefix = rtrim(substr($prefix, 0, $i), '/'); if (strlen($commonPrefix) > $baseLength) { - return $commonPrefix; + return array($commonPrefix, rtrim(substr($prefix, 0, $staticLength), '/') ?: '/'); } - return false; - } - - /** - * Optimizes the tree by inlining items from groups with less than 3 items. - */ - public function optimizeGroups(): void - { - $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(): bool - { - 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; + return null; } /** @@ -218,7 +199,7 @@ class StaticPrefixCollection * * @throws \LogicException when a prefix does not belong in a group */ - private function guardAgainstAddingNotAcceptedRoutes(string $prefix) + private function guardAgainstAddingNotAcceptedRoutes(string $prefix): void { if (!$this->accepts($prefix)) { $message = sprintf('Could not add route with prefix %s to collection with prefix %s', $prefix, $this->prefix); diff --git a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php index 3cba7e66f8..4d56af1a87 100644 --- a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php @@ -213,7 +213,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; } } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher0.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher0.php index 59253f0749..8f32dd2770 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher0.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher0.php @@ -21,7 +21,6 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher $pathinfo = rawurldecode($rawPathinfo); $trimmedPathinfo = rtrim($pathinfo, '/'); $context = $this->context; - $request = $this->request ?: $this->createRequest($pathinfo); $requestMethod = $canonicalMethod = $context->getMethod(); if ('HEAD' === $requestMethod) { @@ -32,6 +31,6 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher throw new Symfony\Component\Routing\Exception\NoConfigurationException(); } - throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException(); + throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException(); } } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php index 29f1f5096b..d3d2826f5f 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php @@ -21,294 +21,209 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher $pathinfo = rawurldecode($rawPathinfo); $trimmedPathinfo = rtrim($pathinfo, '/'); $context = $this->context; - $request = $this->request ?: $this->createRequest($pathinfo); $requestMethod = $canonicalMethod = $context->getMethod(); + $host = strtolower($context->getHost()); if ('HEAD' === $requestMethod) { $canonicalMethod = 'GET'; } - if (0 === strpos($pathinfo, '/foo')) { - // foo - if (preg_match('#^/foo/(?Pbaz|symfony)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo')), array ( 'def' => 'test',)); - } + switch ($pathinfo) { + default: + $routes = array( + '/test/baz' => array(array('_route' => 'baz'), null, null, null), + '/test/baz.html' => array(array('_route' => 'baz2'), null, null, null), + '/test/baz3/' => array(array('_route' => 'baz3'), null, null, null), + '/foofoo' => array(array('_route' => 'foofoo', 'def' => 'test'), null, null, null), + '/spa ce' => array(array('_route' => 'space'), null, null, null), + '/multi/new' => array(array('_route' => 'overridden2'), null, null, null), + '/multi/hey/' => array(array('_route' => 'hey'), null, null, null), + '/ababa' => array(array('_route' => 'ababa'), null, null, null), + '/route1' => array(array('_route' => 'route1'), 'a.example.com', null, null), + '/c2/route2' => array(array('_route' => 'route2'), 'a.example.com', null, null), + '/route4' => array(array('_route' => 'route4'), 'a.example.com', null, null), + '/c2/route3' => array(array('_route' => 'route3'), 'b.example.com', null, null), + '/route5' => array(array('_route' => 'route5'), 'c.example.com', null, null), + '/route6' => array(array('_route' => 'route6'), null, null, null), + '/route11' => array(array('_route' => 'route11'), '#^(?P[^\\.]++)\\.example\\.com$#sDi', null, null), + '/route12' => array(array('_route' => 'route12', 'var1' => 'val'), '#^(?P[^\\.]++)\\.example\\.com$#sDi', null, null), + '/route17' => array(array('_route' => 'route17'), null, null, null), + ); - // foofoo - if ('/foofoo' === $pathinfo) { - return array ( 'def' => 'test', '_route' => 'foofoo',); - } + if (!isset($routes[$pathinfo])) { + break; + } + list($ret, $requiredHost, $requiredMethods, $requiredSchemes) = $routes[$pathinfo]; - } + 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); + } + } - elseif (0 === strpos($pathinfo, '/bar')) { - // bar - if (preg_match('#^/bar/(?P[^/]++)$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'bar')), array ()); - if ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_bar; + if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) { + $allow += $requiredMethods; + break; } return $ret; - } - not_bar: + } - // barhead - if (0 === strpos($pathinfo, '/barhead') && preg_match('#^/barhead/(?P[^/]++)$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'barhead')), array ()); - if ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_barhead; + $matchedPathinfo = $host.$pathinfo; + $regexList = array( + 0 => '{^(?' + .'|[^/]*+(?' + .'|/foo/(baz|symfony)(*:34)' + .'|/bar(?' + .'|/([^/]++)(*:57)' + .'|head/([^/]++)(*:77)' + .')' + .'|/test/([^/]++)(?' + .'|/(*:103)' + .')' + .'|/([\']+)(*:119)' + .'|/a(?' + .'|/b\'b/([^/]++)(?' + .'|(*:148)' + .'|(*:156)' + .')' + .'|/(.*)(*:170)' + .'|/b\'b/([^/]++)(?' + .'|(*:194)' + .'|(*:202)' + .')' + .')' + .'|/multi/hello(?:/([^/]++))?(*:238)' + .'|/([^/]++)/b/([^/]++)(*:266)' + .'|/([^/]++)/b/([^/]++)(*:294)' + .'|/aba/([^/]++)(*:315)' + .')|(?i:([^\\.]++)\\.example\\.com)(?' + .'|/route1(?' + .'|3/([^/]++)(*:375)' + .'|4/([^/]++)(*:393)' + .')' + .')|(?i:c\\.example\\.com)(?' + .'|/route15/([^/]++)(*:443)' + .')|[^/]*+(?' + .'|/route16/([^/]++)(*:478)' + .'|/a(?' + .'|/a\\.\\.\\.(*:499)' + .'|/b(?' + .'|/([^/]++)(*:521)' + .'|/c/([^/]++)(*:540)' + .')' + .')' + .')' + .')$}sD', + ); + + foreach ($regexList as $offset => $regex) { + while (preg_match($regex, $matchedPathinfo, $matches)) { + switch ($m = (int) $matches['MARK']) { + case 103: + $matches = array('foo' => $matches[1] ?? null); + + // baz4 + return $this->mergeDefaults(array_replace($matches, array('_route' => 'baz4')), array()); + + // baz5 + $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'baz5')), array()); + if (!isset(($a = array('POST' => 0))[$requestMethod])) { + $allow += $a; + goto not_baz5; + } + + return $ret; + not_baz5: + + // baz.baz6 + $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'baz.baz6')), array()); + if (!isset(($a = array('PUT' => 0))[$requestMethod])) { + $allow += $a; + goto not_bazbaz6; + } + + return $ret; + not_bazbaz6: + + break; + case 148: + $matches = array('foo' => $matches[1] ?? null); + + // foo1 + $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'foo1')), array()); + if (!isset(($a = array('PUT' => 0))[$requestMethod])) { + $allow += $a; + goto not_foo1; + } + + return $ret; + not_foo1: + + break; + case 194: + $matches = array('foo1' => $matches[1] ?? null); + + // foo2 + return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo2')), array()); + + break; + case 266: + $matches = array('_locale' => $matches[1] ?? null, 'foo' => $matches[2] ?? null); + + // foo3 + return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo3')), array()); + + break; + default: + $routes = array( + 34 => array(array('_route' => 'foo', 'def' => 'test'), array('bar'), null, null), + 57 => array(array('_route' => 'bar'), array('foo'), array('GET' => 0, 'HEAD' => 1), null), + 77 => array(array('_route' => 'barhead'), array('foo'), array('GET' => 0), null), + 119 => array(array('_route' => 'quoter'), array('quoter'), null, null), + 156 => array(array('_route' => 'bar1'), array('bar'), null, null), + 170 => array(array('_route' => 'overridden'), array('var'), null, null), + 202 => array(array('_route' => 'bar2'), array('bar1'), null, null), + 238 => array(array('_route' => 'helloWorld', 'who' => 'World!'), array('who'), null, null), + 294 => array(array('_route' => 'bar3'), array('_locale', 'bar'), null, null), + 315 => array(array('_route' => 'foo4'), array('foo'), null, null), + 375 => array(array('_route' => 'route13'), array('var1', 'name'), null, null), + 393 => array(array('_route' => 'route14', 'var1' => 'val'), array('var1', 'name'), null, null), + 443 => array(array('_route' => 'route15'), array('name'), null, null), + 478 => array(array('_route' => 'route16', 'var1' => 'val'), array('name'), null, null), + 499 => array(array('_route' => 'a'), array(), null, null), + 521 => array(array('_route' => 'b'), array('var'), null, null), + 540 => array(array('_route' => 'c'), array('var'), null, null), + ); + + list($ret, $vars, $requiredMethods, $requiredSchemes) = $routes[$m]; + + foreach ($vars as $i => $v) { + if (isset($matches[1 + $i])) { + $ret[$v] = $matches[1 + $i]; + } + } + + if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) { + $allow += $requiredMethods; + break; + } + + return $ret; } - return $ret; - } - not_barhead: - - } - - elseif (0 === strpos($pathinfo, '/test')) { - if (0 === strpos($pathinfo, '/test/baz')) { - // baz - if ('/test/baz' === $pathinfo) { - return array('_route' => 'baz'); + if (540 === $m) { + break; } - - // baz2 - if ('/test/baz.html' === $pathinfo) { - return array('_route' => 'baz2'); - } - - // baz3 - if ('/test/baz3/' === $pathinfo) { - return array('_route' => 'baz3'); - } - + $regex = substr_replace($regex, 'F', $m - $offset, 1 + strlen($m)); + $offset += strlen($m); } - - // baz4 - if (preg_match('#^/test/(?P[^/]++)/$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'baz4')), array ()); - } - - // baz5 - if (preg_match('#^/test/(?P[^/]++)/$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'baz5')), array ()); - if ('POST' !== $canonicalMethod) { - $allow[] = 'POST'; - goto not_baz5; - } - - return $ret; - } - not_baz5: - - // baz.baz6 - if (preg_match('#^/test/(?P[^/]++)/$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'baz.baz6')), array ()); - if ('PUT' !== $canonicalMethod) { - $allow[] = 'PUT'; - goto not_bazbaz6; - } - - return $ret; - } - not_bazbaz6: - } - // quoter - if (preg_match('#^/(?P[\']+)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'quoter')), array ()); - } - - // space - if ('/spa ce' === $pathinfo) { - return array('_route' => 'space'); - } - - if (0 === strpos($pathinfo, '/a')) { - if (0 === strpos($pathinfo, '/a/b\'b')) { - // foo1 - if (preg_match('#^/a/b\'b/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo1')), array ()); - } - - // bar1 - if (preg_match('#^/a/b\'b/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar1')), array ()); - } - - } - - // overridden - if (preg_match('#^/a/(?P.*)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'overridden')), array ()); - } - - if (0 === strpos($pathinfo, '/a/b\'b')) { - // foo2 - if (preg_match('#^/a/b\'b/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo2')), array ()); - } - - // bar2 - if (preg_match('#^/a/b\'b/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar2')), array ()); - } - - } - - } - - elseif (0 === strpos($pathinfo, '/multi')) { - // helloWorld - if (0 === strpos($pathinfo, '/multi/hello') && preg_match('#^/multi/hello(?:/(?P[^/]++))?$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'helloWorld')), array ( 'who' => 'World!',)); - } - - // hey - if ('/multi/hey/' === $pathinfo) { - return array('_route' => 'hey'); - } - - // overridden2 - if ('/multi/new' === $pathinfo) { - return array('_route' => 'overridden2'); - } - - } - - // foo3 - if (preg_match('#^/(?P<_locale>[^/]++)/b/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo3')), array ()); - } - - // bar3 - if (preg_match('#^/(?P<_locale>[^/]++)/b/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar3')), array ()); - } - - if (0 === strpos($pathinfo, '/aba')) { - // ababa - if ('/ababa' === $pathinfo) { - return array('_route' => 'ababa'); - } - - // foo4 - if (preg_match('#^/aba/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo4')), array ()); - } - - } - - $host = $context->getHost(); - - if (preg_match('#^a\\.example\\.com$#sDi', $host, $hostMatches)) { - // route1 - if ('/route1' === $pathinfo) { - return array('_route' => 'route1'); - } - - // route2 - if ('/c2/route2' === $pathinfo) { - return array('_route' => 'route2'); - } - - } - - if (preg_match('#^b\\.example\\.com$#sDi', $host, $hostMatches)) { - // route3 - if ('/c2/route3' === $pathinfo) { - return array('_route' => 'route3'); - } - - } - - if (preg_match('#^a\\.example\\.com$#sDi', $host, $hostMatches)) { - // route4 - if ('/route4' === $pathinfo) { - return array('_route' => 'route4'); - } - - } - - if (preg_match('#^c\\.example\\.com$#sDi', $host, $hostMatches)) { - // route5 - if ('/route5' === $pathinfo) { - return array('_route' => 'route5'); - } - - } - - // route6 - if ('/route6' === $pathinfo) { - return array('_route' => 'route6'); - } - - if (preg_match('#^(?P[^\\.]++)\\.example\\.com$#sDi', $host, $hostMatches)) { - if (0 === strpos($pathinfo, '/route1')) { - // route11 - if ('/route11' === $pathinfo) { - return $this->mergeDefaults(array_replace($hostMatches, array('_route' => 'route11')), array ()); - } - - // route12 - if ('/route12' === $pathinfo) { - return $this->mergeDefaults(array_replace($hostMatches, array('_route' => 'route12')), array ( 'var1' => 'val',)); - } - - // route13 - if (0 === strpos($pathinfo, '/route13') && preg_match('#^/route13/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($hostMatches, $matches, array('_route' => 'route13')), array ()); - } - - // route14 - if (0 === strpos($pathinfo, '/route14') && preg_match('#^/route14/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($hostMatches, $matches, array('_route' => 'route14')), array ( 'var1' => 'val',)); - } - - } - - } - - if (preg_match('#^c\\.example\\.com$#sDi', $host, $hostMatches)) { - // route15 - if (0 === strpos($pathinfo, '/route15') && preg_match('#^/route15/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'route15')), array ()); - } - - } - - // route16 - if (0 === strpos($pathinfo, '/route16') && preg_match('#^/route16/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'route16')), array ( 'var1' => 'val',)); - } - - // route17 - if ('/route17' === $pathinfo) { - return array('_route' => 'route17'); - } - - // a - if ('/a/a...' === $pathinfo) { - return array('_route' => 'a'); - } - - if (0 === strpos($pathinfo, '/a/b')) { - // b - if (preg_match('#^/a/b/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'b')), array ()); - } - - // c - if (0 === strpos($pathinfo, '/a/b/c') && preg_match('#^/a/b/c/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'c')), array ()); - } - - } - - throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException(); + throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException(); } } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.php index 7f27184d8a..e15535a78e 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.php @@ -21,361 +21,253 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec $pathinfo = rawurldecode($rawPathinfo); $trimmedPathinfo = rtrim($pathinfo, '/'); $context = $this->context; - $request = $this->request ?: $this->createRequest($pathinfo); $requestMethod = $canonicalMethod = $context->getMethod(); + $host = strtolower($context->getHost()); if ('HEAD' === $requestMethod) { $canonicalMethod = 'GET'; } - if (0 === strpos($pathinfo, '/foo')) { - // foo - if (preg_match('#^/foo/(?Pbaz|symfony)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo')), array ( 'def' => 'test',)); - } + switch ($trimmedPathinfo) { + default: + $routes = array( + '/test/baz' => array(array('_route' => 'baz'), null, null, null), + '/test/baz.html' => array(array('_route' => 'baz2'), null, null, null), + '/test/baz3' => array(array('_route' => 'baz3'), null, null, null, true), + '/foofoo' => array(array('_route' => 'foofoo', 'def' => 'test'), null, null, null), + '/spa ce' => array(array('_route' => 'space'), null, null, null), + '/multi/new' => array(array('_route' => 'overridden2'), null, null, null), + '/multi/hey' => array(array('_route' => 'hey'), null, null, null, true), + '/ababa' => array(array('_route' => 'ababa'), null, null, null), + '/route1' => array(array('_route' => 'route1'), 'a.example.com', null, null), + '/c2/route2' => array(array('_route' => 'route2'), 'a.example.com', null, null), + '/route4' => array(array('_route' => 'route4'), 'a.example.com', null, null), + '/c2/route3' => array(array('_route' => 'route3'), 'b.example.com', null, null), + '/route5' => array(array('_route' => 'route5'), 'c.example.com', null, null), + '/route6' => array(array('_route' => 'route6'), null, null, null), + '/route11' => array(array('_route' => 'route11'), '#^(?P[^\\.]++)\\.example\\.com$#sDi', null, null), + '/route12' => array(array('_route' => 'route12', 'var1' => 'val'), '#^(?P[^\\.]++)\\.example\\.com$#sDi', null, null), + '/route17' => array(array('_route' => 'route17'), null, null, null), + '/secure' => array(array('_route' => 'secure'), null, null, array('https' => 0)), + '/nonsecure' => array(array('_route' => 'nonsecure'), null, null, array('http' => 0)), + ); - // foofoo - if ('/foofoo' === $pathinfo) { - return array ( 'def' => 'test', '_route' => 'foofoo',); - } + if (!isset($routes[$trimmedPathinfo])) { + break; + } + list($ret, $requiredHost, $requiredMethods, $requiredSchemes) = $routes[$trimmedPathinfo]; - } - - elseif (0 === strpos($pathinfo, '/bar')) { - // bar - if (preg_match('#^/bar/(?P[^/]++)$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'bar')), array ()); - if ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_bar; + 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); + } } - return $ret; - } - not_bar: - - // barhead - if (0 === strpos($pathinfo, '/barhead') && preg_match('#^/barhead/(?P[^/]++)$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'barhead')), array ()); - if ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_barhead; + if (empty($routes[$trimmedPathinfo][4]) || '/' === $pathinfo[-1]) { + // no-op + } elseif ('GET' !== $canonicalMethod) { + $allow['GET'] = 'GET'; + break; + } else { + return array_replace($ret, $this->redirect($rawPathinfo.'/', $ret['_route'])); } - return $ret; - } - not_barhead: - - } - - elseif (0 === strpos($pathinfo, '/test')) { - if (0 === strpos($pathinfo, '/test/baz')) { - // baz - if ('/test/baz' === $pathinfo) { - return array('_route' => 'baz'); - } - - // baz2 - if ('/test/baz.html' === $pathinfo) { - return array('_route' => 'baz2'); - } - - // baz3 - if ('/test/baz3' === $trimmedPathinfo) { - $ret = array('_route' => 'baz3'); - if ('/' === substr($pathinfo, -1)) { - // no-op - } elseif ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_baz3; - } else { - return array_replace($ret, $this->redirect($rawPathinfo.'/', 'baz3')); + if ($requiredSchemes && !isset($requiredSchemes[$context->getScheme()])) { + if ('GET' !== $canonicalMethod) { + $allow['GET'] = 'GET'; + break; } - return $ret; + return array_replace($ret, $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes))); } - not_baz3: - } - - // baz4 - if (preg_match('#^/test/(?P[^/]++)/?$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'baz4')), array ()); - if ('/' === substr($pathinfo, -1)) { - // no-op - } elseif ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_baz4; - } else { - return array_replace($ret, $this->redirect($rawPathinfo.'/', 'baz4')); + if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) { + $allow += $requiredMethods; + break; } return $ret; - } - not_baz4: + } - // baz5 - if (preg_match('#^/test/(?P[^/]++)/$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'baz5')), array ()); - if ('POST' !== $canonicalMethod) { - $allow[] = 'POST'; - goto not_baz5; + $matchedPathinfo = $host.$pathinfo; + $regexList = array( + 0 => '{^(?' + .'|[^/]*+(?' + .'|/foo/(baz|symfony)(*:34)' + .'|/bar(?' + .'|/([^/]++)(*:57)' + .'|head/([^/]++)(*:77)' + .')' + .'|/test/([^/]++)(?' + .'|/?(*:104)' + .')' + .'|/([\']+)(*:120)' + .'|/a(?' + .'|/b\'b/([^/]++)(?' + .'|(*:149)' + .'|(*:157)' + .')' + .'|/(.*)(*:171)' + .'|/b\'b/([^/]++)(?' + .'|(*:195)' + .'|(*:203)' + .')' + .')' + .'|/multi/hello(?:/([^/]++))?(*:239)' + .'|/([^/]++)/b/([^/]++)(*:267)' + .'|/([^/]++)/b/([^/]++)(*:295)' + .'|/aba/([^/]++)(*:316)' + .')|(?i:([^\\.]++)\\.example\\.com)(?' + .'|/route1(?' + .'|3/([^/]++)(*:376)' + .'|4/([^/]++)(*:394)' + .')' + .')|(?i:c\\.example\\.com)(?' + .'|/route15/([^/]++)(*:444)' + .')|[^/]*+(?' + .'|/route16/([^/]++)(*:479)' + .'|/a(?' + .'|/a\\.\\.\\.(*:500)' + .'|/b(?' + .'|/([^/]++)(*:522)' + .'|/c/([^/]++)(*:541)' + .')' + .')' + .')' + .')$}sD', + ); + + foreach ($regexList as $offset => $regex) { + while (preg_match($regex, $matchedPathinfo, $matches)) { + switch ($m = (int) $matches['MARK']) { + case 104: + $matches = array('foo' => $matches[1] ?? null); + + // baz4 + $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'baz4')), array()); + if ('/' === $pathinfo[-1]) { + // no-op + } elseif ('GET' !== $canonicalMethod) { + $allow['GET'] = 'GET'; + goto not_baz4; + } else { + return array_replace($ret, $this->redirect($rawPathinfo.'/', 'baz4')); + } + + return $ret; + not_baz4: + + // baz5 + if ('/' === $pathinfo[-1]) { + $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'baz5')), array()); + if (!isset(($a = array('POST' => 0))[$requestMethod])) { + $allow += $a; + goto not_baz5; + } + + return $ret; + } + not_baz5: + + // baz.baz6 + if ('/' === $pathinfo[-1]) { + $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'baz.baz6')), array()); + if (!isset(($a = array('PUT' => 0))[$requestMethod])) { + $allow += $a; + goto not_bazbaz6; + } + + return $ret; + } + not_bazbaz6: + + break; + case 149: + $matches = array('foo' => $matches[1] ?? null); + + // foo1 + $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'foo1')), array()); + if (!isset(($a = array('PUT' => 0))[$requestMethod])) { + $allow += $a; + goto not_foo1; + } + + return $ret; + not_foo1: + + break; + case 195: + $matches = array('foo1' => $matches[1] ?? null); + + // foo2 + return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo2')), array()); + + break; + case 267: + $matches = array('_locale' => $matches[1] ?? null, 'foo' => $matches[2] ?? null); + + // foo3 + return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo3')), array()); + + break; + default: + $routes = array( + 34 => array(array('_route' => 'foo', 'def' => 'test'), array('bar'), null, null), + 57 => array(array('_route' => 'bar'), array('foo'), array('GET' => 0, 'HEAD' => 1), null), + 77 => array(array('_route' => 'barhead'), array('foo'), array('GET' => 0), null), + 120 => array(array('_route' => 'quoter'), array('quoter'), null, null), + 157 => array(array('_route' => 'bar1'), array('bar'), null, null), + 171 => array(array('_route' => 'overridden'), array('var'), null, null), + 203 => array(array('_route' => 'bar2'), array('bar1'), null, null), + 239 => array(array('_route' => 'helloWorld', 'who' => 'World!'), array('who'), null, null), + 295 => array(array('_route' => 'bar3'), array('_locale', 'bar'), null, null), + 316 => array(array('_route' => 'foo4'), array('foo'), null, null), + 376 => array(array('_route' => 'route13'), array('var1', 'name'), null, null), + 394 => array(array('_route' => 'route14', 'var1' => 'val'), array('var1', 'name'), null, null), + 444 => array(array('_route' => 'route15'), array('name'), null, null), + 479 => array(array('_route' => 'route16', 'var1' => 'val'), array('name'), null, null), + 500 => array(array('_route' => 'a'), array(), null, null), + 522 => array(array('_route' => 'b'), array('var'), null, null), + 541 => array(array('_route' => 'c'), array('var'), null, null), + ); + + list($ret, $vars, $requiredMethods, $requiredSchemes) = $routes[$m]; + + foreach ($vars as $i => $v) { + if (isset($matches[1 + $i])) { + $ret[$v] = $matches[1 + $i]; + } + } + + if ($requiredSchemes && !isset($requiredSchemes[$context->getScheme()])) { + if ('GET' !== $canonicalMethod) { + $allow['GET'] = 'GET'; + break; + } + + return array_replace($ret, $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes))); + } + + if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) { + $allow += $requiredMethods; + break; + } + + return $ret; } - return $ret; - } - not_baz5: - - // baz.baz6 - if (preg_match('#^/test/(?P[^/]++)/$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'baz.baz6')), array ()); - if ('PUT' !== $canonicalMethod) { - $allow[] = 'PUT'; - goto not_bazbaz6; + if (541 === $m) { + break; } - - return $ret; + $regex = substr_replace($regex, 'F', $m - $offset, 1 + strlen($m)); + $offset += strlen($m); } - not_bazbaz6: - } - // quoter - if (preg_match('#^/(?P[\']+)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'quoter')), array ()); - } - - // space - if ('/spa ce' === $pathinfo) { - return array('_route' => 'space'); - } - - if (0 === strpos($pathinfo, '/a')) { - if (0 === strpos($pathinfo, '/a/b\'b')) { - // foo1 - if (preg_match('#^/a/b\'b/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo1')), array ()); - } - - // bar1 - if (preg_match('#^/a/b\'b/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar1')), array ()); - } - - } - - // overridden - if (preg_match('#^/a/(?P.*)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'overridden')), array ()); - } - - if (0 === strpos($pathinfo, '/a/b\'b')) { - // foo2 - if (preg_match('#^/a/b\'b/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo2')), array ()); - } - - // bar2 - if (preg_match('#^/a/b\'b/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar2')), array ()); - } - - } - - } - - elseif (0 === strpos($pathinfo, '/multi')) { - // helloWorld - if (0 === strpos($pathinfo, '/multi/hello') && preg_match('#^/multi/hello(?:/(?P[^/]++))?$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'helloWorld')), array ( 'who' => 'World!',)); - } - - // hey - if ('/multi/hey' === $trimmedPathinfo) { - $ret = array('_route' => 'hey'); - if ('/' === substr($pathinfo, -1)) { - // no-op - } elseif ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_hey; - } else { - return array_replace($ret, $this->redirect($rawPathinfo.'/', 'hey')); - } - - return $ret; - } - not_hey: - - // overridden2 - if ('/multi/new' === $pathinfo) { - return array('_route' => 'overridden2'); - } - - } - - // foo3 - if (preg_match('#^/(?P<_locale>[^/]++)/b/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo3')), array ()); - } - - // bar3 - if (preg_match('#^/(?P<_locale>[^/]++)/b/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar3')), array ()); - } - - if (0 === strpos($pathinfo, '/aba')) { - // ababa - if ('/ababa' === $pathinfo) { - return array('_route' => 'ababa'); - } - - // foo4 - if (preg_match('#^/aba/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo4')), array ()); - } - - } - - $host = $context->getHost(); - - if (preg_match('#^a\\.example\\.com$#sDi', $host, $hostMatches)) { - // route1 - if ('/route1' === $pathinfo) { - return array('_route' => 'route1'); - } - - // route2 - if ('/c2/route2' === $pathinfo) { - return array('_route' => 'route2'); - } - - } - - if (preg_match('#^b\\.example\\.com$#sDi', $host, $hostMatches)) { - // route3 - if ('/c2/route3' === $pathinfo) { - return array('_route' => 'route3'); - } - - } - - if (preg_match('#^a\\.example\\.com$#sDi', $host, $hostMatches)) { - // route4 - if ('/route4' === $pathinfo) { - return array('_route' => 'route4'); - } - - } - - if (preg_match('#^c\\.example\\.com$#sDi', $host, $hostMatches)) { - // route5 - if ('/route5' === $pathinfo) { - return array('_route' => 'route5'); - } - - } - - // route6 - if ('/route6' === $pathinfo) { - return array('_route' => 'route6'); - } - - if (preg_match('#^(?P[^\\.]++)\\.example\\.com$#sDi', $host, $hostMatches)) { - if (0 === strpos($pathinfo, '/route1')) { - // route11 - if ('/route11' === $pathinfo) { - return $this->mergeDefaults(array_replace($hostMatches, array('_route' => 'route11')), array ()); - } - - // route12 - if ('/route12' === $pathinfo) { - return $this->mergeDefaults(array_replace($hostMatches, array('_route' => 'route12')), array ( 'var1' => 'val',)); - } - - // route13 - if (0 === strpos($pathinfo, '/route13') && preg_match('#^/route13/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($hostMatches, $matches, array('_route' => 'route13')), array ()); - } - - // route14 - if (0 === strpos($pathinfo, '/route14') && preg_match('#^/route14/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($hostMatches, $matches, array('_route' => 'route14')), array ( 'var1' => 'val',)); - } - - } - - } - - if (preg_match('#^c\\.example\\.com$#sDi', $host, $hostMatches)) { - // route15 - if (0 === strpos($pathinfo, '/route15') && preg_match('#^/route15/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'route15')), array ()); - } - - } - - // route16 - if (0 === strpos($pathinfo, '/route16') && preg_match('#^/route16/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'route16')), array ( 'var1' => 'val',)); - } - - // route17 - if ('/route17' === $pathinfo) { - return array('_route' => 'route17'); - } - - // a - if ('/a/a...' === $pathinfo) { - return array('_route' => 'a'); - } - - if (0 === strpos($pathinfo, '/a/b')) { - // b - if (preg_match('#^/a/b/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'b')), array ()); - } - - // c - if (0 === strpos($pathinfo, '/a/b/c') && preg_match('#^/a/b/c/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'c')), array ()); - } - - } - - // secure - if ('/secure' === $pathinfo) { - $ret = array('_route' => 'secure'); - $requiredSchemes = array ( 'https' => 0,); - if (!isset($requiredSchemes[$context->getScheme()])) { - if ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_secure; - } - - return array_replace($ret, $this->redirect($rawPathinfo, 'secure', key($requiredSchemes))); - } - - return $ret; - } - not_secure: - - // nonsecure - if ('/nonsecure' === $pathinfo) { - $ret = array('_route' => 'nonsecure'); - $requiredSchemes = array ( 'http' => 0,); - if (!isset($requiredSchemes[$context->getScheme()])) { - if ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_nonsecure; - } - - return array_replace($ret, $this->redirect($rawPathinfo, 'nonsecure', key($requiredSchemes))); - } - - return $ret; - } - not_nonsecure: - - throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException(); + throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException(); } } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher3.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher3.php index cfa6d131a0..e49293f03c 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher3.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher3.php @@ -21,31 +21,76 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher $pathinfo = rawurldecode($rawPathinfo); $trimmedPathinfo = rtrim($pathinfo, '/'); $context = $this->context; - $request = $this->request ?: $this->createRequest($pathinfo); $requestMethod = $canonicalMethod = $context->getMethod(); if ('HEAD' === $requestMethod) { $canonicalMethod = 'GET'; } - if (0 === strpos($pathinfo, '/rootprefix')) { - // static - if ('/rootprefix/test' === $pathinfo) { - return array('_route' => 'static'); - } + switch ($pathinfo) { + case '/with-condition': + // with-condition + if (($context->getMethod() == "GET")) { + return array('_route' => 'with-condition'); + } + break; + default: + $routes = array( + '/rootprefix/test' => array(array('_route' => 'static'), null, null, null), + ); - // dynamic - if (preg_match('#^/rootprefix/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'dynamic')), array ()); - } + if (!isset($routes[$pathinfo])) { + break; + } + list($ret, $requiredHost, $requiredMethods, $requiredSchemes) = $routes[$pathinfo]; + if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) { + $allow += $requiredMethods; + break; + } + + return $ret; } - // with-condition - if ('/with-condition' === $pathinfo && ($context->getMethod() == "GET")) { - return array('_route' => 'with-condition'); + $matchedPathinfo = $pathinfo; + $regexList = array( + 0 => '{^(?' + .'|/rootprefix/([^/]++)(*:27)' + .')$}sD', + ); + + foreach ($regexList as $offset => $regex) { + while (preg_match($regex, $matchedPathinfo, $matches)) { + switch ($m = (int) $matches['MARK']) { + default: + $routes = array( + 27 => array(array('_route' => 'dynamic'), array('var'), null, null), + ); + + list($ret, $vars, $requiredMethods, $requiredSchemes) = $routes[$m]; + + foreach ($vars as $i => $v) { + if (isset($matches[1 + $i])) { + $ret[$v] = $matches[1 + $i]; + } + } + + if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) { + $allow += $requiredMethods; + break; + } + + return $ret; + } + + if (27 === $m) { + break; + } + $regex = substr_replace($regex, 'F', $m - $offset, 1 + strlen($m)); + $offset += strlen($m); + } } - throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException(); + throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException(); } } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher4.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher4.php index db9741ccab..17a6a7e421 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher4.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher4.php @@ -21,88 +21,54 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher $pathinfo = rawurldecode($rawPathinfo); $trimmedPathinfo = rtrim($pathinfo, '/'); $context = $this->context; - $request = $this->request ?: $this->createRequest($pathinfo); $requestMethod = $canonicalMethod = $context->getMethod(); if ('HEAD' === $requestMethod) { $canonicalMethod = 'GET'; } - // just_head - if ('/just_head' === $pathinfo) { - $ret = array('_route' => 'just_head'); - if ('HEAD' !== $requestMethod) { - $allow[] = 'HEAD'; - goto not_just_head; - } - - return $ret; - } - not_just_head: - - // head_and_get - if ('/head_and_get' === $pathinfo) { - $ret = array('_route' => 'head_and_get'); - if ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_head_and_get; - } - - return $ret; - } - not_head_and_get: - - // get_and_head - if ('/get_and_head' === $pathinfo) { - $ret = array('_route' => 'get_and_head'); - if ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_get_and_head; - } - - return $ret; - } - not_get_and_head: - - // post_and_head - if ('/post_and_head' === $pathinfo) { - $ret = array('_route' => 'post_and_head'); - if (!in_array($requestMethod, array('POST', 'HEAD'))) { - $allow = array_merge($allow, array('POST', 'HEAD')); - goto not_post_and_head; - } - - return $ret; - } - not_post_and_head: - - if (0 === strpos($pathinfo, '/put_and_post')) { - // put_and_post - if ('/put_and_post' === $pathinfo) { + switch ($pathinfo) { + case '/put_and_post': + // put_and_post $ret = array('_route' => 'put_and_post'); - if (!in_array($requestMethod, array('PUT', 'POST'))) { - $allow = array_merge($allow, array('PUT', 'POST')); + if (!isset(($a = array('PUT' => 0, 'POST' => 1))[$requestMethod])) { + $allow += $a; goto not_put_and_post; } return $ret; - } - not_put_and_post: - - // put_and_get_and_head - if ('/put_and_post' === $pathinfo) { + not_put_and_post: + // put_and_get_and_head $ret = array('_route' => 'put_and_get_and_head'); - if (!in_array($canonicalMethod, array('PUT', 'GET'))) { - $allow = array_merge($allow, array('PUT', 'GET')); + if (!isset(($a = array('PUT' => 0, 'GET' => 1, 'HEAD' => 2))[$canonicalMethod])) { + $allow += $a; goto not_put_and_get_and_head; } return $ret; - } - not_put_and_get_and_head: + not_put_and_get_and_head: + break; + default: + $routes = array( + '/just_head' => array(array('_route' => 'just_head'), null, array('HEAD' => 0), null), + '/head_and_get' => array(array('_route' => 'head_and_get'), null, array('HEAD' => 0, 'GET' => 1), null), + '/get_and_head' => array(array('_route' => 'get_and_head'), null, array('GET' => 0, 'HEAD' => 1), null), + '/post_and_head' => array(array('_route' => 'post_and_head'), null, array('POST' => 0, 'HEAD' => 1), null), + ); + if (!isset($routes[$pathinfo])) { + break; + } + list($ret, $requiredHost, $requiredMethods, $requiredSchemes) = $routes[$pathinfo]; + + if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) { + $allow += $requiredMethods; + break; + } + + return $ret; } - throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException(); + throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException(); } } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher5.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher5.php index 3edda1b034..97aba35984 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher5.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher5.php @@ -21,194 +21,110 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec $pathinfo = rawurldecode($rawPathinfo); $trimmedPathinfo = rtrim($pathinfo, '/'); $context = $this->context; - $request = $this->request ?: $this->createRequest($pathinfo); $requestMethod = $canonicalMethod = $context->getMethod(); if ('HEAD' === $requestMethod) { $canonicalMethod = 'GET'; } - if (0 === strpos($pathinfo, '/a')) { - // a_first - if ('/a/11' === $pathinfo) { - return array('_route' => 'a_first'); - } + switch ($trimmedPathinfo) { + default: + $routes = array( + '/a/11' => array(array('_route' => 'a_first'), null, null, null), + '/a/22' => array(array('_route' => 'a_second'), null, null, null), + '/a/333' => array(array('_route' => 'a_third'), null, null, null), + '/a/44' => array(array('_route' => 'a_fourth'), null, null, null, true), + '/a/55' => array(array('_route' => 'a_fifth'), null, null, null, true), + '/a/66' => array(array('_route' => 'a_sixth'), null, null, null, true), + '/nested/group/a' => array(array('_route' => 'nested_a'), null, null, null, true), + '/nested/group/b' => array(array('_route' => 'nested_b'), null, null, null, true), + '/nested/group/c' => array(array('_route' => 'nested_c'), null, null, null, true), + '/slashed/group' => array(array('_route' => 'slashed_a'), null, null, null, true), + '/slashed/group/b' => array(array('_route' => 'slashed_b'), null, null, null, true), + '/slashed/group/c' => array(array('_route' => 'slashed_c'), null, null, null, true), + ); - // a_second - if ('/a/22' === $pathinfo) { - return array('_route' => 'a_second'); - } + if (!isset($routes[$trimmedPathinfo])) { + break; + } + list($ret, $requiredHost, $requiredMethods, $requiredSchemes) = $routes[$trimmedPathinfo]; - // a_third - if ('/a/333' === $pathinfo) { - return array('_route' => 'a_third'); - } + if (empty($routes[$trimmedPathinfo][4]) || '/' === $pathinfo[-1]) { + // no-op + } elseif ('GET' !== $canonicalMethod) { + $allow['GET'] = 'GET'; + break; + } else { + return array_replace($ret, $this->redirect($rawPathinfo.'/', $ret['_route'])); + } + if ($requiredSchemes && !isset($requiredSchemes[$context->getScheme()])) { + if ('GET' !== $canonicalMethod) { + $allow['GET'] = 'GET'; + break; + } + + return array_replace($ret, $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes))); + } + + if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) { + $allow += $requiredMethods; + break; + } + + return $ret; } - // a_wildcard - if (preg_match('#^/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'a_wildcard')), array ()); + $matchedPathinfo = $pathinfo; + $regexList = array( + 0 => '{^(?' + .'|/([^/]++)(*:16)' + .'|/nested/([^/]++)(*:39)' + .')$}sD', + ); + + foreach ($regexList as $offset => $regex) { + while (preg_match($regex, $matchedPathinfo, $matches)) { + switch ($m = (int) $matches['MARK']) { + default: + $routes = array( + 16 => array(array('_route' => 'a_wildcard'), array('param'), null, null), + 39 => array(array('_route' => 'nested_wildcard'), array('param'), null, null), + ); + + list($ret, $vars, $requiredMethods, $requiredSchemes) = $routes[$m]; + + foreach ($vars as $i => $v) { + if (isset($matches[1 + $i])) { + $ret[$v] = $matches[1 + $i]; + } + } + + if ($requiredSchemes && !isset($requiredSchemes[$context->getScheme()])) { + if ('GET' !== $canonicalMethod) { + $allow['GET'] = 'GET'; + break; + } + + return array_replace($ret, $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes))); + } + + if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) { + $allow += $requiredMethods; + break; + } + + return $ret; + } + + if (39 === $m) { + break; + } + $regex = substr_replace($regex, 'F', $m - $offset, 1 + strlen($m)); + $offset += strlen($m); + } } - if (0 === strpos($pathinfo, '/a')) { - // a_fourth - if ('/a/44' === $trimmedPathinfo) { - $ret = array('_route' => 'a_fourth'); - if ('/' === substr($pathinfo, -1)) { - // no-op - } elseif ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_a_fourth; - } else { - return array_replace($ret, $this->redirect($rawPathinfo.'/', 'a_fourth')); - } - - return $ret; - } - not_a_fourth: - - // a_fifth - if ('/a/55' === $trimmedPathinfo) { - $ret = array('_route' => 'a_fifth'); - if ('/' === substr($pathinfo, -1)) { - // no-op - } elseif ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_a_fifth; - } else { - return array_replace($ret, $this->redirect($rawPathinfo.'/', 'a_fifth')); - } - - return $ret; - } - not_a_fifth: - - // a_sixth - if ('/a/66' === $trimmedPathinfo) { - $ret = array('_route' => 'a_sixth'); - if ('/' === substr($pathinfo, -1)) { - // no-op - } elseif ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_a_sixth; - } else { - return array_replace($ret, $this->redirect($rawPathinfo.'/', 'a_sixth')); - } - - return $ret; - } - not_a_sixth: - - } - - // nested_wildcard - if (0 === strpos($pathinfo, '/nested') && preg_match('#^/nested/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'nested_wildcard')), array ()); - } - - if (0 === strpos($pathinfo, '/nested/group')) { - // nested_a - if ('/nested/group/a' === $trimmedPathinfo) { - $ret = array('_route' => 'nested_a'); - if ('/' === substr($pathinfo, -1)) { - // no-op - } elseif ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_nested_a; - } else { - return array_replace($ret, $this->redirect($rawPathinfo.'/', 'nested_a')); - } - - return $ret; - } - not_nested_a: - - // nested_b - if ('/nested/group/b' === $trimmedPathinfo) { - $ret = array('_route' => 'nested_b'); - if ('/' === substr($pathinfo, -1)) { - // no-op - } elseif ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_nested_b; - } else { - return array_replace($ret, $this->redirect($rawPathinfo.'/', 'nested_b')); - } - - return $ret; - } - not_nested_b: - - // nested_c - if ('/nested/group/c' === $trimmedPathinfo) { - $ret = array('_route' => 'nested_c'); - if ('/' === substr($pathinfo, -1)) { - // no-op - } elseif ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_nested_c; - } else { - return array_replace($ret, $this->redirect($rawPathinfo.'/', 'nested_c')); - } - - return $ret; - } - not_nested_c: - - } - - elseif (0 === strpos($pathinfo, '/slashed/group')) { - // slashed_a - if ('/slashed/group' === $trimmedPathinfo) { - $ret = array('_route' => 'slashed_a'); - if ('/' === substr($pathinfo, -1)) { - // no-op - } elseif ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_slashed_a; - } else { - return array_replace($ret, $this->redirect($rawPathinfo.'/', 'slashed_a')); - } - - return $ret; - } - not_slashed_a: - - // slashed_b - if ('/slashed/group/b' === $trimmedPathinfo) { - $ret = array('_route' => 'slashed_b'); - if ('/' === substr($pathinfo, -1)) { - // no-op - } elseif ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_slashed_b; - } else { - return array_replace($ret, $this->redirect($rawPathinfo.'/', 'slashed_b')); - } - - return $ret; - } - not_slashed_b: - - // slashed_c - if ('/slashed/group/c' === $trimmedPathinfo) { - $ret = array('_route' => 'slashed_c'); - if ('/' === substr($pathinfo, -1)) { - // no-op - } elseif ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_slashed_c; - } else { - return array_replace($ret, $this->redirect($rawPathinfo.'/', 'slashed_c')); - } - - return $ret; - } - not_slashed_c: - - } - - throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException(); + throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException(); } } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher6.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher6.php index 483b63f96f..27804d575d 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher6.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher6.php @@ -21,189 +21,95 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher $pathinfo = rawurldecode($rawPathinfo); $trimmedPathinfo = rtrim($pathinfo, '/'); $context = $this->context; - $request = $this->request ?: $this->createRequest($pathinfo); $requestMethod = $canonicalMethod = $context->getMethod(); if ('HEAD' === $requestMethod) { $canonicalMethod = 'GET'; } - if (0 === strpos($pathinfo, '/trailing/simple')) { - // simple_trailing_slash_no_methods - if ('/trailing/simple/no-methods/' === $pathinfo) { - return array('_route' => 'simple_trailing_slash_no_methods'); - } + switch ($pathinfo) { + default: + $routes = array( + '/trailing/simple/no-methods/' => array(array('_route' => 'simple_trailing_slash_no_methods'), null, null, null), + '/trailing/simple/get-method/' => array(array('_route' => 'simple_trailing_slash_GET_method'), null, array('GET' => 0), null), + '/trailing/simple/head-method/' => array(array('_route' => 'simple_trailing_slash_HEAD_method'), null, array('HEAD' => 0), null), + '/trailing/simple/post-method/' => array(array('_route' => 'simple_trailing_slash_POST_method'), null, array('POST' => 0), null), + '/not-trailing/simple/no-methods' => array(array('_route' => 'simple_not_trailing_slash_no_methods'), null, null, null), + '/not-trailing/simple/get-method' => array(array('_route' => 'simple_not_trailing_slash_GET_method'), null, array('GET' => 0), null), + '/not-trailing/simple/head-method' => array(array('_route' => 'simple_not_trailing_slash_HEAD_method'), null, array('HEAD' => 0), null), + '/not-trailing/simple/post-method' => array(array('_route' => 'simple_not_trailing_slash_POST_method'), null, array('POST' => 0), null), + ); - // simple_trailing_slash_GET_method - if ('/trailing/simple/get-method/' === $pathinfo) { - $ret = array('_route' => 'simple_trailing_slash_GET_method'); - if ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_simple_trailing_slash_GET_method; + if (!isset($routes[$pathinfo])) { + break; + } + list($ret, $requiredHost, $requiredMethods, $requiredSchemes) = $routes[$pathinfo]; + + if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) { + $allow += $requiredMethods; + break; } return $ret; - } - not_simple_trailing_slash_GET_method: - - // simple_trailing_slash_HEAD_method - if ('/trailing/simple/head-method/' === $pathinfo) { - $ret = array('_route' => 'simple_trailing_slash_HEAD_method'); - if ('HEAD' !== $requestMethod) { - $allow[] = 'HEAD'; - goto not_simple_trailing_slash_HEAD_method; - } - - return $ret; - } - not_simple_trailing_slash_HEAD_method: - - // simple_trailing_slash_POST_method - if ('/trailing/simple/post-method/' === $pathinfo) { - $ret = array('_route' => 'simple_trailing_slash_POST_method'); - if ('POST' !== $canonicalMethod) { - $allow[] = 'POST'; - goto not_simple_trailing_slash_POST_method; - } - - return $ret; - } - not_simple_trailing_slash_POST_method: - } - elseif (0 === strpos($pathinfo, '/trailing/regex')) { - // regex_trailing_slash_no_methods - if (0 === strpos($pathinfo, '/trailing/regex/no-methods') && preg_match('#^/trailing/regex/no\\-methods/(?P[^/]++)/$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_trailing_slash_no_methods')), array ()); - } + $matchedPathinfo = $pathinfo; + $regexList = array( + 0 => '{^(?' + .'|/trailing/regex(?' + .'|/no\\-methods/([^/]++)/(*:47)' + .'|/get\\-method/([^/]++)/(*:76)' + .'|/head\\-method/([^/]++)/(*:106)' + .'|/post\\-method/([^/]++)/(*:137)' + .')' + .'|/not\\-trailing/regex(?' + .'|/no\\-methods/([^/]++)(*:190)' + .'|/get\\-method/([^/]++)(*:219)' + .'|/head\\-method/([^/]++)(*:249)' + .'|/post\\-method/([^/]++)(*:279)' + .')' + .')$}sD', + ); - // regex_trailing_slash_GET_method - if (0 === strpos($pathinfo, '/trailing/regex/get-method') && preg_match('#^/trailing/regex/get\\-method/(?P[^/]++)/$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_trailing_slash_GET_method')), array ()); - if ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_regex_trailing_slash_GET_method; + foreach ($regexList as $offset => $regex) { + while (preg_match($regex, $matchedPathinfo, $matches)) { + switch ($m = (int) $matches['MARK']) { + default: + $routes = array( + 47 => array(array('_route' => 'regex_trailing_slash_no_methods'), array('param'), null, null), + 76 => array(array('_route' => 'regex_trailing_slash_GET_method'), array('param'), array('GET' => 0), null), + 106 => array(array('_route' => 'regex_trailing_slash_HEAD_method'), array('param'), array('HEAD' => 0), null), + 137 => array(array('_route' => 'regex_trailing_slash_POST_method'), array('param'), array('POST' => 0), null), + 190 => array(array('_route' => 'regex_not_trailing_slash_no_methods'), array('param'), null, null), + 219 => array(array('_route' => 'regex_not_trailing_slash_GET_method'), array('param'), array('GET' => 0), null), + 249 => array(array('_route' => 'regex_not_trailing_slash_HEAD_method'), array('param'), array('HEAD' => 0), null), + 279 => array(array('_route' => 'regex_not_trailing_slash_POST_method'), array('param'), array('POST' => 0), null), + ); + + list($ret, $vars, $requiredMethods, $requiredSchemes) = $routes[$m]; + + foreach ($vars as $i => $v) { + if (isset($matches[1 + $i])) { + $ret[$v] = $matches[1 + $i]; + } + } + + if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) { + $allow += $requiredMethods; + break; + } + + return $ret; } - return $ret; - } - not_regex_trailing_slash_GET_method: - - // regex_trailing_slash_HEAD_method - if (0 === strpos($pathinfo, '/trailing/regex/head-method') && preg_match('#^/trailing/regex/head\\-method/(?P[^/]++)/$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_trailing_slash_HEAD_method')), array ()); - if ('HEAD' !== $requestMethod) { - $allow[] = 'HEAD'; - goto not_regex_trailing_slash_HEAD_method; + if (279 === $m) { + break; } - - return $ret; + $regex = substr_replace($regex, 'F', $m - $offset, 1 + strlen($m)); + $offset += strlen($m); } - not_regex_trailing_slash_HEAD_method: - - // regex_trailing_slash_POST_method - if (0 === strpos($pathinfo, '/trailing/regex/post-method') && preg_match('#^/trailing/regex/post\\-method/(?P[^/]++)/$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_trailing_slash_POST_method')), array ()); - if ('POST' !== $canonicalMethod) { - $allow[] = 'POST'; - goto not_regex_trailing_slash_POST_method; - } - - return $ret; - } - not_regex_trailing_slash_POST_method: - } - elseif (0 === strpos($pathinfo, '/not-trailing/simple')) { - // simple_not_trailing_slash_no_methods - if ('/not-trailing/simple/no-methods' === $pathinfo) { - return array('_route' => 'simple_not_trailing_slash_no_methods'); - } - - // simple_not_trailing_slash_GET_method - if ('/not-trailing/simple/get-method' === $pathinfo) { - $ret = array('_route' => 'simple_not_trailing_slash_GET_method'); - if ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_simple_not_trailing_slash_GET_method; - } - - return $ret; - } - not_simple_not_trailing_slash_GET_method: - - // simple_not_trailing_slash_HEAD_method - if ('/not-trailing/simple/head-method' === $pathinfo) { - $ret = array('_route' => 'simple_not_trailing_slash_HEAD_method'); - if ('HEAD' !== $requestMethod) { - $allow[] = 'HEAD'; - goto not_simple_not_trailing_slash_HEAD_method; - } - - return $ret; - } - not_simple_not_trailing_slash_HEAD_method: - - // simple_not_trailing_slash_POST_method - if ('/not-trailing/simple/post-method' === $pathinfo) { - $ret = array('_route' => 'simple_not_trailing_slash_POST_method'); - if ('POST' !== $canonicalMethod) { - $allow[] = 'POST'; - goto not_simple_not_trailing_slash_POST_method; - } - - return $ret; - } - not_simple_not_trailing_slash_POST_method: - - } - - elseif (0 === strpos($pathinfo, '/not-trailing/regex')) { - // regex_not_trailing_slash_no_methods - if (0 === strpos($pathinfo, '/not-trailing/regex/no-methods') && preg_match('#^/not\\-trailing/regex/no\\-methods/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_not_trailing_slash_no_methods')), array ()); - } - - // regex_not_trailing_slash_GET_method - if (0 === strpos($pathinfo, '/not-trailing/regex/get-method') && preg_match('#^/not\\-trailing/regex/get\\-method/(?P[^/]++)$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_not_trailing_slash_GET_method')), array ()); - if ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_regex_not_trailing_slash_GET_method; - } - - return $ret; - } - not_regex_not_trailing_slash_GET_method: - - // regex_not_trailing_slash_HEAD_method - if (0 === strpos($pathinfo, '/not-trailing/regex/head-method') && preg_match('#^/not\\-trailing/regex/head\\-method/(?P[^/]++)$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_not_trailing_slash_HEAD_method')), array ()); - if ('HEAD' !== $requestMethod) { - $allow[] = 'HEAD'; - goto not_regex_not_trailing_slash_HEAD_method; - } - - return $ret; - } - not_regex_not_trailing_slash_HEAD_method: - - // regex_not_trailing_slash_POST_method - if (0 === strpos($pathinfo, '/not-trailing/regex/post-method') && preg_match('#^/not\\-trailing/regex/post\\-method/(?P[^/]++)$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_not_trailing_slash_POST_method')), array ()); - if ('POST' !== $canonicalMethod) { - $allow[] = 'POST'; - goto not_regex_not_trailing_slash_POST_method; - } - - return $ret; - } - not_regex_not_trailing_slash_POST_method: - - } - - throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException(); + throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException(); } } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher7.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher7.php index c744f5dacd..6046e17232 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher7.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher7.php @@ -21,229 +21,131 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec $pathinfo = rawurldecode($rawPathinfo); $trimmedPathinfo = rtrim($pathinfo, '/'); $context = $this->context; - $request = $this->request ?: $this->createRequest($pathinfo); $requestMethod = $canonicalMethod = $context->getMethod(); if ('HEAD' === $requestMethod) { $canonicalMethod = 'GET'; } - if (0 === strpos($pathinfo, '/trailing/simple')) { - // simple_trailing_slash_no_methods - if ('/trailing/simple/no-methods' === $trimmedPathinfo) { - $ret = array('_route' => 'simple_trailing_slash_no_methods'); - if ('/' === substr($pathinfo, -1)) { + switch ($trimmedPathinfo) { + default: + $routes = array( + '/trailing/simple/no-methods' => array(array('_route' => 'simple_trailing_slash_no_methods'), null, null, null, true), + '/trailing/simple/get-method' => array(array('_route' => 'simple_trailing_slash_GET_method'), null, array('GET' => 0), null, true), + '/trailing/simple/head-method' => array(array('_route' => 'simple_trailing_slash_HEAD_method'), null, array('HEAD' => 0), null, true), + '/trailing/simple/post-method' => array(array('_route' => 'simple_trailing_slash_POST_method'), null, array('POST' => 0), null, true), + '/not-trailing/simple/no-methods' => array(array('_route' => 'simple_not_trailing_slash_no_methods'), null, null, null), + '/not-trailing/simple/get-method' => array(array('_route' => 'simple_not_trailing_slash_GET_method'), null, array('GET' => 0), null), + '/not-trailing/simple/head-method' => array(array('_route' => 'simple_not_trailing_slash_HEAD_method'), null, array('HEAD' => 0), null), + '/not-trailing/simple/post-method' => array(array('_route' => 'simple_not_trailing_slash_POST_method'), null, array('POST' => 0), null), + ); + + if (!isset($routes[$trimmedPathinfo])) { + break; + } + list($ret, $requiredHost, $requiredMethods, $requiredSchemes) = $routes[$trimmedPathinfo]; + + if (empty($routes[$trimmedPathinfo][4]) || '/' === $pathinfo[-1]) { // no-op } elseif ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_simple_trailing_slash_no_methods; + $allow['GET'] = 'GET'; + break; } else { - return array_replace($ret, $this->redirect($rawPathinfo.'/', 'simple_trailing_slash_no_methods')); + return array_replace($ret, $this->redirect($rawPathinfo.'/', $ret['_route'])); + } + + if ($requiredSchemes && !isset($requiredSchemes[$context->getScheme()])) { + if ('GET' !== $canonicalMethod) { + $allow['GET'] = 'GET'; + break; + } + + return array_replace($ret, $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes))); + } + + if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) { + $allow += $requiredMethods; + break; } return $ret; - } - not_simple_trailing_slash_no_methods: - - // simple_trailing_slash_GET_method - if ('/trailing/simple/get-method' === $trimmedPathinfo) { - $ret = array('_route' => 'simple_trailing_slash_GET_method'); - if ('/' === substr($pathinfo, -1)) { - // no-op - } elseif ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_simple_trailing_slash_GET_method; - } else { - return array_replace($ret, $this->redirect($rawPathinfo.'/', 'simple_trailing_slash_GET_method')); - } - - if ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_simple_trailing_slash_GET_method; - } - - return $ret; - } - not_simple_trailing_slash_GET_method: - - // simple_trailing_slash_HEAD_method - if ('/trailing/simple/head-method/' === $pathinfo) { - $ret = array('_route' => 'simple_trailing_slash_HEAD_method'); - if ('HEAD' !== $requestMethod) { - $allow[] = 'HEAD'; - goto not_simple_trailing_slash_HEAD_method; - } - - return $ret; - } - not_simple_trailing_slash_HEAD_method: - - // simple_trailing_slash_POST_method - if ('/trailing/simple/post-method/' === $pathinfo) { - $ret = array('_route' => 'simple_trailing_slash_POST_method'); - if ('POST' !== $canonicalMethod) { - $allow[] = 'POST'; - goto not_simple_trailing_slash_POST_method; - } - - return $ret; - } - not_simple_trailing_slash_POST_method: - } - elseif (0 === strpos($pathinfo, '/trailing/regex')) { - // regex_trailing_slash_no_methods - if (0 === strpos($pathinfo, '/trailing/regex/no-methods') && preg_match('#^/trailing/regex/no\\-methods/(?P[^/]++)/?$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_trailing_slash_no_methods')), array ()); - if ('/' === substr($pathinfo, -1)) { - // no-op - } elseif ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_regex_trailing_slash_no_methods; - } else { - return array_replace($ret, $this->redirect($rawPathinfo.'/', 'regex_trailing_slash_no_methods')); + $matchedPathinfo = $pathinfo; + $regexList = array( + 0 => '{^(?' + .'|/trailing/regex(?' + .'|/no\\-methods/([^/]++)/?(*:48)' + .'|/get\\-method/([^/]++)/?(*:78)' + .'|/head\\-method/([^/]++)/(*:108)' + .'|/post\\-method/([^/]++)/(*:139)' + .')' + .'|/not\\-trailing/regex(?' + .'|/no\\-methods/([^/]++)(*:192)' + .'|/get\\-method/([^/]++)(*:221)' + .'|/head\\-method/([^/]++)(*:251)' + .'|/post\\-method/([^/]++)(*:281)' + .')' + .')$}sD', + ); + + foreach ($regexList as $offset => $regex) { + while (preg_match($regex, $matchedPathinfo, $matches)) { + switch ($m = (int) $matches['MARK']) { + default: + $routes = array( + 48 => array(array('_route' => 'regex_trailing_slash_no_methods'), array('param'), null, null, true), + 78 => array(array('_route' => 'regex_trailing_slash_GET_method'), array('param'), array('GET' => 0), null, true), + 108 => array(array('_route' => 'regex_trailing_slash_HEAD_method'), array('param'), array('HEAD' => 0), null), + 139 => array(array('_route' => 'regex_trailing_slash_POST_method'), array('param'), array('POST' => 0), null), + 192 => array(array('_route' => 'regex_not_trailing_slash_no_methods'), array('param'), null, null), + 221 => array(array('_route' => 'regex_not_trailing_slash_GET_method'), array('param'), array('GET' => 0), null), + 251 => array(array('_route' => 'regex_not_trailing_slash_HEAD_method'), array('param'), array('HEAD' => 0), null), + 281 => array(array('_route' => 'regex_not_trailing_slash_POST_method'), array('param'), array('POST' => 0), null), + ); + + list($ret, $vars, $requiredMethods, $requiredSchemes) = $routes[$m]; + + foreach ($vars as $i => $v) { + if (isset($matches[1 + $i])) { + $ret[$v] = $matches[1 + $i]; + } + } + + if (empty($routes[$m][4]) || '/' === $pathinfo[-1]) { + // no-op + } elseif ('GET' !== $canonicalMethod) { + $allow['GET'] = 'GET'; + break; + } else { + return array_replace($ret, $this->redirect($rawPathinfo.'/', $ret['_route'])); + } + + if ($requiredSchemes && !isset($requiredSchemes[$context->getScheme()])) { + if ('GET' !== $canonicalMethod) { + $allow['GET'] = 'GET'; + break; + } + + return array_replace($ret, $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes))); + } + + if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) { + $allow += $requiredMethods; + break; + } + + return $ret; } - return $ret; + if (281 === $m) { + break; + } + $regex = substr_replace($regex, 'F', $m - $offset, 1 + strlen($m)); + $offset += strlen($m); } - not_regex_trailing_slash_no_methods: - - // regex_trailing_slash_GET_method - if (0 === strpos($pathinfo, '/trailing/regex/get-method') && preg_match('#^/trailing/regex/get\\-method/(?P[^/]++)/?$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_trailing_slash_GET_method')), array ()); - if ('/' === substr($pathinfo, -1)) { - // no-op - } elseif ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_regex_trailing_slash_GET_method; - } else { - return array_replace($ret, $this->redirect($rawPathinfo.'/', 'regex_trailing_slash_GET_method')); - } - - if ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_regex_trailing_slash_GET_method; - } - - return $ret; - } - not_regex_trailing_slash_GET_method: - - // regex_trailing_slash_HEAD_method - if (0 === strpos($pathinfo, '/trailing/regex/head-method') && preg_match('#^/trailing/regex/head\\-method/(?P[^/]++)/$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_trailing_slash_HEAD_method')), array ()); - if ('HEAD' !== $requestMethod) { - $allow[] = 'HEAD'; - goto not_regex_trailing_slash_HEAD_method; - } - - return $ret; - } - not_regex_trailing_slash_HEAD_method: - - // regex_trailing_slash_POST_method - if (0 === strpos($pathinfo, '/trailing/regex/post-method') && preg_match('#^/trailing/regex/post\\-method/(?P[^/]++)/$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_trailing_slash_POST_method')), array ()); - if ('POST' !== $canonicalMethod) { - $allow[] = 'POST'; - goto not_regex_trailing_slash_POST_method; - } - - return $ret; - } - not_regex_trailing_slash_POST_method: - } - elseif (0 === strpos($pathinfo, '/not-trailing/simple')) { - // simple_not_trailing_slash_no_methods - if ('/not-trailing/simple/no-methods' === $pathinfo) { - return array('_route' => 'simple_not_trailing_slash_no_methods'); - } - - // simple_not_trailing_slash_GET_method - if ('/not-trailing/simple/get-method' === $pathinfo) { - $ret = array('_route' => 'simple_not_trailing_slash_GET_method'); - if ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_simple_not_trailing_slash_GET_method; - } - - return $ret; - } - not_simple_not_trailing_slash_GET_method: - - // simple_not_trailing_slash_HEAD_method - if ('/not-trailing/simple/head-method' === $pathinfo) { - $ret = array('_route' => 'simple_not_trailing_slash_HEAD_method'); - if ('HEAD' !== $requestMethod) { - $allow[] = 'HEAD'; - goto not_simple_not_trailing_slash_HEAD_method; - } - - return $ret; - } - not_simple_not_trailing_slash_HEAD_method: - - // simple_not_trailing_slash_POST_method - if ('/not-trailing/simple/post-method' === $pathinfo) { - $ret = array('_route' => 'simple_not_trailing_slash_POST_method'); - if ('POST' !== $canonicalMethod) { - $allow[] = 'POST'; - goto not_simple_not_trailing_slash_POST_method; - } - - return $ret; - } - not_simple_not_trailing_slash_POST_method: - - } - - elseif (0 === strpos($pathinfo, '/not-trailing/regex')) { - // regex_not_trailing_slash_no_methods - if (0 === strpos($pathinfo, '/not-trailing/regex/no-methods') && preg_match('#^/not\\-trailing/regex/no\\-methods/(?P[^/]++)$#sD', $pathinfo, $matches)) { - return $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_not_trailing_slash_no_methods')), array ()); - } - - // regex_not_trailing_slash_GET_method - if (0 === strpos($pathinfo, '/not-trailing/regex/get-method') && preg_match('#^/not\\-trailing/regex/get\\-method/(?P[^/]++)$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_not_trailing_slash_GET_method')), array ()); - if ('GET' !== $canonicalMethod) { - $allow[] = 'GET'; - goto not_regex_not_trailing_slash_GET_method; - } - - return $ret; - } - not_regex_not_trailing_slash_GET_method: - - // regex_not_trailing_slash_HEAD_method - if (0 === strpos($pathinfo, '/not-trailing/regex/head-method') && preg_match('#^/not\\-trailing/regex/head\\-method/(?P[^/]++)$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_not_trailing_slash_HEAD_method')), array ()); - if ('HEAD' !== $requestMethod) { - $allow[] = 'HEAD'; - goto not_regex_not_trailing_slash_HEAD_method; - } - - return $ret; - } - not_regex_not_trailing_slash_HEAD_method: - - // regex_not_trailing_slash_POST_method - if (0 === strpos($pathinfo, '/not-trailing/regex/post-method') && preg_match('#^/not\\-trailing/regex/post\\-method/(?P[^/]++)$#sD', $pathinfo, $matches)) { - $ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'regex_not_trailing_slash_POST_method')), array ()); - if ('POST' !== $canonicalMethod) { - $allow[] = 'POST'; - goto not_regex_not_trailing_slash_POST_method; - } - - return $ret; - } - not_regex_not_trailing_slash_POST_method: - - } - - throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException(); + throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException(); } } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher8.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher8.php new file mode 100644 index 0000000000..df7bfb029f --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher8.php @@ -0,0 +1,79 @@ +context = $context; + } + + public function match($rawPathinfo) + { + $allow = array(); + $pathinfo = rawurldecode($rawPathinfo); + $trimmedPathinfo = rtrim($pathinfo, '/'); + $context = $this->context; + $requestMethod = $canonicalMethod = $context->getMethod(); + + if ('HEAD' === $requestMethod) { + $canonicalMethod = 'GET'; + } + + $matchedPathinfo = $pathinfo; + $regexList = array( + 0 => '{^(?' + .'|/(a)(*:11)' + .')$}sD', + 11 => '{^(?' + .'|/(.)(*:26)' + .')$}sDu', + 26 => '{^(?' + .'|/(.)(*:41)' + .')$}sD', + ); + + foreach ($regexList as $offset => $regex) { + while (preg_match($regex, $matchedPathinfo, $matches)) { + switch ($m = (int) $matches['MARK']) { + default: + $routes = array( + 11 => array(array('_route' => 'a'), array('a'), null, null), + 26 => array(array('_route' => 'b'), array('a'), null, null), + 41 => array(array('_route' => 'c'), array('a'), null, null), + ); + + list($ret, $vars, $requiredMethods, $requiredSchemes) = $routes[$m]; + + foreach ($vars as $i => $v) { + if (isset($matches[1 + $i])) { + $ret[$v] = $matches[1 + $i]; + } + } + + if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) { + $allow += $requiredMethods; + break; + } + + return $ret; + } + + if (41 === $m) { + break; + } + $regex = substr_replace($regex, 'F', $m - $offset, 1 + strlen($m)); + $offset += strlen($m); + } + } + + throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException(); + } +} diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher9.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher9.php new file mode 100644 index 0000000000..e1af79de23 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher9.php @@ -0,0 +1,50 @@ +context = $context; + } + + public function match($rawPathinfo) + { + $allow = array(); + $pathinfo = rawurldecode($rawPathinfo); + $trimmedPathinfo = rtrim($pathinfo, '/'); + $context = $this->context; + $requestMethod = $canonicalMethod = $context->getMethod(); + $host = strtolower($context->getHost()); + + if ('HEAD' === $requestMethod) { + $canonicalMethod = 'GET'; + } + + switch ($pathinfo) { + case '/': + // a + if (preg_match('#^(?P[^\\.]++)\\.e\\.c\\.b\\.a$#sDi', $host, $hostMatches)) { + return $this->mergeDefaults(array_replace($hostMatches, array('_route' => 'a')), array()); + } + // c + if (preg_match('#^(?P[^\\.]++)\\.e\\.c\\.b\\.a$#sDi', $host, $hostMatches)) { + return $this->mergeDefaults(array_replace($hostMatches, array('_route' => 'c')), array()); + } + // b + if ('d.c.b.a' === $host) { + return array('_route' => 'b'); + } + break; + } + + throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException(); + } +} diff --git a/src/Symfony/Component/Routing/Tests/Matcher/Dumper/DumperCollectionTest.php b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/DumperCollectionTest.php deleted file mode 100644 index 823efdb840..0000000000 --- a/src/Symfony/Component/Routing/Tests/Matcher/Dumper/DumperCollectionTest.php +++ /dev/null @@ -1,34 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Routing\Tests\Matcher\Dumper; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\Routing\Matcher\Dumper\DumperCollection; - -class DumperCollectionTest extends TestCase -{ - public function testGetRoot() - { - $a = new DumperCollection(); - - $b = new DumperCollection(); - $a->add($b); - - $c = new DumperCollection(); - $b->add($c); - - $d = new DumperCollection(); - $c->add($d); - - $this->assertSame($a, $c->getRoot()); - } -} diff --git a/src/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php index f29a6d6a30..09441e0b9b 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php @@ -181,7 +181,7 @@ class PhpMatcherDumperTest extends TestCase // prefixes $collection1 = new RouteCollection(); $collection1->add('overridden', new Route('/overridden1')); - $collection1->add('foo1', new Route('/{foo}')); + $collection1->add('foo1', (new Route('/{foo}'))->setMethods('PUT')); $collection1->add('bar1', new Route('/{bar}')); $collection1->addPrefix('/b\'b'); $collection2 = new RouteCollection(); @@ -399,6 +399,7 @@ class PhpMatcherDumperTest extends TestCase $groupOptimisedCollection->add('slashed_b', new Route('/slashed/group/b/')); $groupOptimisedCollection->add('slashed_c', new Route('/slashed/group/c/')); + /* test case 6 & 7 */ $trailingSlashCollection = new RouteCollection(); $trailingSlashCollection->add('simple_trailing_slash_no_methods', new Route('/trailing/simple/no-methods/', array(), array(), array(), '', array(), array())); $trailingSlashCollection->add('simple_trailing_slash_GET_method', new Route('/trailing/simple/get-method/', array(), array(), array(), '', array(), array('GET'))); @@ -418,6 +419,18 @@ class PhpMatcherDumperTest extends TestCase $trailingSlashCollection->add('regex_not_trailing_slash_HEAD_method', new Route('/not-trailing/regex/head-method/{param}', array(), array(), array(), '', array(), array('HEAD'))); $trailingSlashCollection->add('regex_not_trailing_slash_POST_method', new Route('/not-trailing/regex/post-method/{param}', array(), array(), array(), '', array(), array('POST'))); + /* test case 8 */ + $unicodeCollection = new RouteCollection(); + $unicodeCollection->add('a', new Route('/{a}', array(), array('a' => 'a'), array('utf8' => false))); + $unicodeCollection->add('b', new Route('/{a}', array(), array('a' => '.'), array('utf8' => true))); + $unicodeCollection->add('c', new Route('/{a}', array(), array('a' => '.'), array('utf8' => false))); + + /* test case 9 */ + $hostTreeCollection = new RouteCollection(); + $hostTreeCollection->add('a', (new Route('/'))->setHost('{d}.e.c.b.a')); + $hostTreeCollection->add('b', (new Route('/'))->setHost('d.c.b.a')); + $hostTreeCollection->add('c', (new Route('/'))->setHost('{e}.e.c.b.a')); + return array( array(new RouteCollection(), 'url_matcher0.php', array()), array($collection, 'url_matcher1.php', array()), @@ -427,6 +440,8 @@ class PhpMatcherDumperTest extends TestCase array($groupOptimisedCollection, 'url_matcher5.php', array('base_class' => 'Symfony\Component\Routing\Tests\Fixtures\RedirectableUrlMatcher')), array($trailingSlashCollection, 'url_matcher6.php', array()), array($trailingSlashCollection, 'url_matcher7.php', array('base_class' => 'Symfony\Component\Routing\Tests\Fixtures\RedirectableUrlMatcher')), + array($unicodeCollection, 'url_matcher8.php', array()), + array($hostTreeCollection, 'url_matcher9.php', array()), ); } diff --git a/src/Symfony/Component/Routing/Tests/Matcher/Dumper/StaticPrefixCollectionTest.php b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/StaticPrefixCollectionTest.php index 37419e7743..ea3ff6ff4d 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/Dumper/StaticPrefixCollectionTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/StaticPrefixCollectionTest.php @@ -18,10 +18,9 @@ class StaticPrefixCollectionTest extends TestCase foreach ($routes as $route) { list($path, $name) = $route; $staticPrefix = (new Route($path))->compile()->getStaticPrefix(); - $collection->addRoute($staticPrefix, $name); + $collection->addRoute($staticPrefix, array($name)); } - $collection->optimizeGroups(); $dumped = $this->dumpCollection($collection); $this->assertEquals($expected, $dumped); } @@ -36,21 +35,22 @@ class StaticPrefixCollectionTest extends TestCase array('/leading/segment/', 'leading_segment'), ), << array( + 'Nested - small group' => array( array( array('/', 'root'), array('/prefix/segment/aa', 'prefix_segment'), array('/prefix/segment/bb', 'leading_segment'), ), << prefix_segment +-> leading_segment EOF ), 'Nested - contains item at intersection' => array( @@ -60,10 +60,10 @@ EOF array('/prefix/segment/bb', 'leading_segment'), ), << /prefix/segment prefix_segment --> /prefix/segment/bb leading_segment +-> prefix_segment +-> leading_segment EOF ), 'Simple one level nesting' => array( @@ -74,11 +74,11 @@ EOF array('/group/other/', 'other_segment'), ), << /group/segment nested_segment --> /group/thing some_segment --> /group/other other_segment +-> nested_segment +-> some_segment +-> other_segment EOF ), 'Retain matching order with groups' => array( @@ -93,14 +93,14 @@ EOF ), << /group/aa aa --> /group/bb bb --> /group/cc cc -/ root +-> aa +-> bb +-> cc +root /group --> /group/dd dd --> /group/ee ee --> /group/ff ff +-> dd +-> ee +-> ff EOF ), 'Retain complex matching order with groups at base' => array( @@ -109,28 +109,30 @@ EOF array('/prefixed/group/aa/', 'aa'), array('/prefixed/group/bb/', 'bb'), array('/prefixed/group/cc/', 'cc'), - array('/prefixed/', 'root'), + array('/prefixed/(.*)', 'root'), array('/prefixed/group/dd/', 'dd'), array('/prefixed/group/ee/', 'ee'), + array('/prefixed/', 'parent'), array('/prefixed/group/ff/', 'ff'), array('/aaa/222/', 'second_aaa'), array('/aaa/333/', 'third_aaa'), ), << /aaa/111 first_aaa --> /aaa/222 second_aaa --> /aaa/333 third_aaa +-> first_aaa +-> second_aaa +-> third_aaa /prefixed -> /prefixed/group --> -> /prefixed/group/aa aa --> -> /prefixed/group/bb bb --> -> /prefixed/group/cc cc --> /prefixed root +-> -> aa +-> -> bb +-> -> cc +-> root -> /prefixed/group --> -> /prefixed/group/dd dd --> -> /prefixed/group/ee ee --> -> /prefixed/group/ff ff +-> -> dd +-> -> ee +-> -> ff +-> parent EOF ), @@ -145,13 +147,13 @@ EOF ), << /aaa-111 a1 --> /aaa-222 a2 --> /aaa-333 a3 +-> a1 +-> a2 +-> a3 /group- --> /group-aa g1 --> /group-bb g2 --> /group-cc g3 +-> g1 +-> g2 +-> g3 EOF ), ); @@ -161,7 +163,7 @@ EOF { $lines = array(); - foreach ($collection->getItems() as $item) { + foreach ($collection->getRoutes() as $item) { if ($item instanceof StaticPrefixCollection) { $lines[] = $prefix.$item->getPrefix(); $lines[] = $this->dumpCollection($item, $prefix.'-> '); diff --git a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php index a4ed8b6b24..90dc820313 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php @@ -475,6 +475,31 @@ class UrlMatcherTest extends TestCase $this->assertEquals(array('_route' => 'buz'), $matcher->match('/prefix/buz')); } + public function testSiblingRoutes() + { + $coll = new RouteCollection(); + $coll->add('a', (new Route('/a{a}'))->setMethods('POST')); + $coll->add('b', (new Route('/a{a}'))->setMethods('PUT')); + $coll->add('c', new Route('/a{a}')); + $coll->add('d', (new Route('/b{a}'))->setCondition('false')); + $coll->add('e', (new Route('/{b}{a}'))->setCondition('false')); + $coll->add('f', (new Route('/{b}{a}'))->setRequirements(array('b' => 'b'))); + + $matcher = $this->getUrlMatcher($coll); + $this->assertEquals(array('_route' => 'c', 'a' => 'a'), $matcher->match('/aa')); + $this->assertEquals(array('_route' => 'f', 'b' => 'b', 'a' => 'a'), $matcher->match('/ba')); + } + + public function testUnicodeRoute() + { + $coll = new RouteCollection(); + $coll->add('a', new Route('/{a}', array(), array('a' => '.'), array('utf8' => false))); + $coll->add('b', new Route('/{a}', array(), array('a' => '.'), array('utf8' => true))); + + $matcher = $this->getUrlMatcher($coll); + $this->assertEquals(array('_route' => 'b', 'a' => 'é'), $matcher->match('/é')); + } + protected function getUrlMatcher(RouteCollection $routes, RequestContext $context = null) { return new UrlMatcher($routes, $context ?: new RequestContext());