[Routing] Optimised dumped matcher

This commit is contained in:
Frank de Jonge 2017-03-08 11:03:51 +01:00 committed by Fabien Potencier
parent d236af6757
commit 449b6912dc
12 changed files with 751 additions and 390 deletions

View File

@ -1,107 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Matcher\Dumper;
/**
* Prefix tree of routes preserving routes order.
*
* @author Arnaud Le Blanc <arnaud.lb@gmail.com>
*
* @internal
*/
class DumperPrefixCollection extends DumperCollection
{
/**
* @var string
*/
private $prefix = '';
/**
* Returns the prefix.
*
* @return string The prefix
*/
public function getPrefix()
{
return $this->prefix;
}
/**
* Sets the prefix.
*
* @param string $prefix The prefix
*/
public function setPrefix($prefix)
{
$this->prefix = $prefix;
}
/**
* Adds a route in the tree.
*
* @param DumperRoute $route The route
*
* @return self
*
* @throws \LogicException
*/
public function addPrefixRoute(DumperRoute $route)
{
$prefix = $route->getRoute()->compile()->getStaticPrefix();
for ($collection = $this; null !== $collection; $collection = $collection->getParent()) {
// Same prefix, add to current leave
if ($collection->prefix === $prefix) {
$collection->add($route);
return $collection;
}
// Prefix starts with route's prefix
if ('' === $collection->prefix || 0 === strpos($prefix, $collection->prefix)) {
$child = new self();
$child->setPrefix(substr($prefix, 0, strlen($collection->prefix) + 1));
$collection->add($child);
return $child->addPrefixRoute($route);
}
}
// Reached only if the root has a non empty prefix
throw new \LogicException('The collection root must not have a prefix');
}
/**
* Merges nodes whose prefix ends with a slash.
*
* Children of a node whose prefix ends with a slash are moved to the parent node
*/
public function mergeSlashNodes()
{
$children = array();
foreach ($this as $child) {
if ($child instanceof self) {
$child->mergeSlashNodes();
if ('/' === substr($child->prefix, -1)) {
$children = array_merge($children, $child->all());
} else {
$children[] = $child;
}
} else {
$children[] = $child;
}
}
$this->setAll($children);
}
}

View File

@ -134,7 +134,6 @@ EOF;
private function compileRoutes(RouteCollection $routes, $supportsRedirections)
{
$fetchedHost = false;
$groups = $this->groupRoutesByHostRegex($routes);
$code = '';
@ -148,8 +147,8 @@ EOF;
$code .= sprintf(" if (preg_match(%s, \$host, \$hostMatches)) {\n", var_export($regex, true));
}
$tree = $this->buildPrefixTree($collection);
$groupCode = $this->compilePrefixRoutes($tree, $supportsRedirections);
$tree = $this->buildStaticPrefixCollection($collection);
$groupCode = $this->compileStaticPrefixRoutes($tree, $supportsRedirections);
if (null !== $regex) {
// apply extra indention at each line (except empty ones)
@ -164,37 +163,51 @@ 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 recursively to match a tree of routes.
* Generates PHP code to match a tree of routes.
*
* @param DumperPrefixCollection $collection A DumperPrefixCollection instance
* @param StaticPrefixCollection $collection A StaticPrefixCollection instance
* @param bool $supportsRedirections Whether redirections are supported by the base class
* @param string $parentPrefix Prefix of the parent collection
* @param string $ifOrElseIf Either "if" or "elseif" to influence chaining.
*
* @return string PHP code
*/
private function compilePrefixRoutes(DumperPrefixCollection $collection, $supportsRedirections, $parentPrefix = '')
private function compileStaticPrefixRoutes(StaticPrefixCollection $collection, $supportsRedirections, $ifOrElseIf = 'if')
{
$code = '';
$prefix = $collection->getPrefix();
$optimizable = 1 < strlen($prefix) && 1 < count($collection->all());
$optimizedPrefix = $parentPrefix;
if ($optimizable) {
$optimizedPrefix = $prefix;
$code .= sprintf(" if (0 === strpos(\$pathinfo, %s)) {\n", var_export($prefix, true));
if (!empty($prefix) && '/' !== $prefix) {
$code .= sprintf(" %s (0 === strpos(\$pathinfo, %s)) {\n", $ifOrElseIf, var_export($prefix, true));
}
foreach ($collection as $route) {
if ($route instanceof DumperCollection) {
$code .= $this->compilePrefixRoutes($route, $supportsRedirections, $optimizedPrefix);
$ifOrElseIf = 'if';
foreach ($collection->getItems() as $route) {
if ($route instanceof StaticPrefixCollection) {
$code .= $this->compileStaticPrefixRoutes($route, $supportsRedirections, $ifOrElseIf);
$ifOrElseIf = 'elseif';
} else {
$code .= $this->compileRoute($route->getRoute(), $route->getName(), $supportsRedirections, $optimizedPrefix)."\n";
$code .= $this->compileRoute($route[1]->getRoute(), $route[1]->getName(), $supportsRedirections, $prefix)."\n";
$ifOrElseIf = 'if';
}
}
if ($optimizable) {
if (!empty($prefix) && '/' !== $prefix) {
$code .= " }\n\n";
// apply extra indention at each line (except empty ones)
$code = preg_replace('/^.{2,}$/m', ' $0', $code);
@ -387,7 +400,6 @@ EOF;
private function groupRoutesByHostRegex(RouteCollection $routes)
{
$groups = new DumperCollection();
$currentGroup = new DumperCollection();
$currentGroup->setAttribute('host_regex', null);
$groups->add($currentGroup);
@ -405,30 +417,6 @@ EOF;
return $groups;
}
/**
* Organizes the routes into a prefix tree.
*
* Routes order is preserved such that traversing the tree will traverse the
* routes in the origin order.
*
* @param DumperCollection $collection A collection of routes
*
* @return DumperPrefixCollection
*/
private function buildPrefixTree(DumperCollection $collection)
{
$tree = new DumperPrefixCollection();
$current = $tree;
foreach ($collection as $route) {
$current = $current->addPrefixRoute($route);
}
$tree->mergeSlashNodes();
return $tree;
}
private function getExpressionLanguage()
{
if (null === $this->expressionLanguage) {

View File

@ -0,0 +1,238 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Matcher\Dumper;
/**
* Prefix tree of routes preserving routes order.
*
* @author Frank de Jonge <info@frankdejonge.nl>
*
* @internal
*/
class StaticPrefixCollection
{
/**
* @var string
*/
private $prefix;
/**
* @var array[]|StaticPrefixCollection[]
*/
private $items = array();
/**
* @var int
*/
private $matchStart = 0;
public function __construct($prefix = '')
{
$this->prefix = $prefix;
}
public function getPrefix()
{
return $this->prefix;
}
/**
* @return mixed[]|StaticPrefixCollection[]
*/
public function getItems()
{
return $this->items;
}
/**
* Adds a route to a group.
*
* @param string $prefix
* @param mixed $route
*/
public function addRoute($prefix, $route)
{
$prefix = '/' === $prefix ? $prefix : rtrim($prefix, '/');
$this->guardAgainstAddingNotAcceptedRoutes($prefix);
if ($this->prefix === $prefix) {
// When a prefix is exactly the same as the base we move up the match start position.
// This is needed because otherwise routes that come afterwards have higher precedence
// than a possible regular expression, which goes against the input order sorting.
$this->items[] = array($prefix, $route);
$this->matchStart = count($this->items);
return;
}
foreach ($this->items as $i => $item) {
if ($i < $this->matchStart) {
continue;
}
if ($item instanceof self && $item->accepts($prefix)) {
$item->addRoute($prefix, $route);
return;
}
$group = $this->groupWithItem($item, $prefix, $route);
if ($group instanceof self) {
$this->items[$i] = $group;
return;
}
}
// No optimised case was found, in this case we simple add the route for possible
// grouping when new routes are added.
$this->items[] = array($prefix, $route);
}
/**
* 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, $prefix, $route)
{
$itemPrefix = $item instanceof self ? $item->prefix : $item[0];
$commonPrefix = $this->detectCommonPrefix($prefix, $itemPrefix);
if (!$commonPrefix) {
return;
}
$child = new self($commonPrefix);
if ($item instanceof self) {
$child->items = array($item);
} else {
$child->addRoute($item[0], $item[1]);
}
$child->addRoute($prefix, $route);
return $child;
}
/**
* Checks whether a prefix can be contained within the group.
*
* @param string $prefix
*
* @return bool Whether a prefix could belong in a given group
*/
private function accepts($prefix)
{
return '' === $this->prefix || strpos($prefix, $this->prefix) === 0;
}
/**
* Detects whether there's a common prefix relative to the group prefix and returns it.
*
* @param string $prefix
* @param string $anotherPrefix
*
* @return false|string A common prefix, longer than the base/group prefix, or false when none available
*/
private function detectCommonPrefix($prefix, $anotherPrefix)
{
$baseLength = strlen($this->prefix);
$commonLength = $baseLength;
$end = min(strlen($prefix), strlen($anotherPrefix));
for ($i = $baseLength; $i <= $end; ++$i) {
if (substr($prefix, 0, $i) !== substr($anotherPrefix, 0, $i)) {
break;
}
$commonLength = $i;
}
$commonPrefix = rtrim(substr($prefix, 0, $commonLength), '/');
if (strlen($commonPrefix) > $baseLength) {
return $commonPrefix;
}
return false;
}
/**
* Optimizes the tree by inlining items from groups with less than 3 items.
*/
public function optimizeGroups()
{
$index = -1;
while (isset($this->items[++$index])) {
$item = $this->items[$index];
if ($item instanceof self) {
$item->optimizeGroups();
// When a group contains only two items there's no reason to optimize because at minimum
// the amount of prefix check is 2. In this case inline the group.
if ($item->shouldBeInlined()) {
array_splice($this->items, $index, 1, $item->items);
// Lower index to pass through the same index again after optimizing.
// The first item of the replacements might be a group needing optimization.
--$index;
}
}
}
}
private function shouldBeInlined()
{
if (count($this->items) >= 3) {
return false;
}
foreach ($this->items as $item) {
if ($item instanceof self) {
return true;
}
}
foreach ($this->items as $item) {
if (is_array($item) && $item[0] === $this->prefix) {
return false;
}
}
return true;
}
/**
* Guards against adding incompatible prefixes in a group.
*
* @param string $prefix
*
* @throws \LogicException When a prefix does not belong in a group.
*/
private function guardAgainstAddingNotAcceptedRoutes($prefix)
{
if (!$this->accepts($prefix)) {
$message = sprintf('Could not add route with prefix %s to collection with prefix %s', $prefix, $this->prefix);
throw new \LogicException($message);
}
}
}

View File

@ -223,13 +223,36 @@ class RouteCompiler implements RouteCompilerInterface
}
return array(
'staticPrefix' => 'text' === $tokens[0][0] ? $tokens[0][1] : '',
'staticPrefix' => self::determineStaticPrefix($route, $tokens),
'regex' => $regexp,
'tokens' => array_reverse($tokens),
'variables' => $variables,
);
}
/**
* Determines the longest static prefix possible for a route.
*
* @param Route $route
* @param array $tokens
*
* @return string The leading static part of a route's path
*/
private static function determineStaticPrefix(Route $route, array $tokens)
{
if ('text' !== $tokens[0][0]) {
return ($route->hasDefault($tokens[0][3]) || '/' === $tokens[0][1]) ? '' : $tokens[0][1];
}
$prefix = $tokens[0][1];
if (isset($tokens[1][1]) && '/' !== $tokens[1][1] && false === $route->hasDefault($tokens[1][3])) {
$prefix .= $tokens[1][1];
}
return $prefix;
}
/**
* Returns the next static character in the Route pattern that will serve as a separator.
*

View File

@ -35,12 +35,20 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
// foo
if (0 === strpos($pathinfo, '/foo') && preg_match('#^/foo/(?P<bar>baz|symfony)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo')), array ( 'def' => 'test',));
if (0 === strpos($pathinfo, '/foo')) {
// foo
if (preg_match('#^/foo/(?P<bar>baz|symfony)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo')), array ( 'def' => 'test',));
}
// foofoo
if ('/foofoo' === $pathinfo) {
return array ( 'def' => 'test', '_route' => 'foofoo',);
}
}
if (0 === strpos($pathinfo, '/bar')) {
elseif (0 === strpos($pathinfo, '/bar')) {
// bar
if (preg_match('#^/bar/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
if ('GET' !== $canonicalMethod) {
@ -65,7 +73,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
if (0 === strpos($pathinfo, '/test')) {
elseif (0 === strpos($pathinfo, '/test')) {
if (0 === strpos($pathinfo, '/test/baz')) {
// baz
if ('/test/baz' === $pathinfo) {
@ -113,11 +121,6 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
// foofoo
if ('/foofoo' === $pathinfo) {
return array ( 'def' => 'test', '_route' => 'foofoo',);
}
// quoter
if (preg_match('#^/(?P<quoter>[\']+)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'quoter')), array ());
@ -162,22 +165,22 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
if (0 === strpos($pathinfo, '/multi')) {
elseif (0 === strpos($pathinfo, '/multi')) {
// helloWorld
if (0 === strpos($pathinfo, '/multi/hello') && preg_match('#^/multi/hello(?:/(?P<who>[^/]++))?$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'helloWorld')), array ( 'who' => 'World!',));
}
// overridden2
if ('/multi/new' === $pathinfo) {
return array('_route' => 'overridden2');
}
// hey
if ('/multi/hey/' === $pathinfo) {
return array('_route' => 'hey');
}
// overridden2
if ('/multi/new' === $pathinfo) {
return array('_route' => 'overridden2');
}
}
// foo3
@ -281,36 +284,30 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
if (0 === strpos($pathinfo, '/route1')) {
// route16
if (0 === strpos($pathinfo, '/route16') && preg_match('#^/route16/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'route16')), array ( 'var1' => 'val',));
}
// route17
if ('/route17' === $pathinfo) {
return array('_route' => 'route17');
}
// route16
if (0 === strpos($pathinfo, '/route16') && preg_match('#^/route16/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'route16')), array ( 'var1' => 'val',));
}
if (0 === strpos($pathinfo, '/a')) {
// a
if ('/a/a...' === $pathinfo) {
return array('_route' => 'a');
// 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<var>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'b')), array ());
}
if (0 === strpos($pathinfo, '/a/b')) {
// b
if (preg_match('#^/a/b/(?P<var>[^/]++)$#s', $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<var>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'c')), array ());
}
// c
if (0 === strpos($pathinfo, '/a/b/c') && preg_match('#^/a/b/c/(?P<var>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'c')), array ());
}
}

View File

@ -35,12 +35,20 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
}
// foo
if (0 === strpos($pathinfo, '/foo') && preg_match('#^/foo/(?P<bar>baz|symfony)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo')), array ( 'def' => 'test',));
if (0 === strpos($pathinfo, '/foo')) {
// foo
if (preg_match('#^/foo/(?P<bar>baz|symfony)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo')), array ( 'def' => 'test',));
}
// foofoo
if ('/foofoo' === $pathinfo) {
return array ( 'def' => 'test', '_route' => 'foofoo',);
}
}
if (0 === strpos($pathinfo, '/bar')) {
elseif (0 === strpos($pathinfo, '/bar')) {
// bar
if (preg_match('#^/bar/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
if ('GET' !== $canonicalMethod) {
@ -65,7 +73,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
}
if (0 === strpos($pathinfo, '/test')) {
elseif (0 === strpos($pathinfo, '/test')) {
if (0 === strpos($pathinfo, '/test/baz')) {
// baz
if ('/test/baz' === $pathinfo) {
@ -121,11 +129,6 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
}
// foofoo
if ('/foofoo' === $pathinfo) {
return array ( 'def' => 'test', '_route' => 'foofoo',);
}
// quoter
if (preg_match('#^/(?P<quoter>[\']+)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'quoter')), array ());
@ -170,17 +173,12 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
}
if (0 === strpos($pathinfo, '/multi')) {
elseif (0 === strpos($pathinfo, '/multi')) {
// helloWorld
if (0 === strpos($pathinfo, '/multi/hello') && preg_match('#^/multi/hello(?:/(?P<who>[^/]++))?$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'helloWorld')), array ( 'who' => 'World!',));
}
// overridden2
if ('/multi/new' === $pathinfo) {
return array('_route' => 'overridden2');
}
// hey
if ('/multi/hey' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
@ -190,6 +188,11 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
return array('_route' => 'hey');
}
// overridden2
if ('/multi/new' === $pathinfo) {
return array('_route' => 'overridden2');
}
}
// foo3
@ -293,36 +296,30 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
}
if (0 === strpos($pathinfo, '/route1')) {
// route16
if (0 === strpos($pathinfo, '/route16') && preg_match('#^/route16/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'route16')), array ( 'var1' => 'val',));
}
// route17
if ('/route17' === $pathinfo) {
return array('_route' => 'route17');
}
// route16
if (0 === strpos($pathinfo, '/route16') && preg_match('#^/route16/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'route16')), array ( 'var1' => 'val',));
}
if (0 === strpos($pathinfo, '/a')) {
// a
if ('/a/a...' === $pathinfo) {
return array('_route' => 'a');
// 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<var>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'b')), array ());
}
if (0 === strpos($pathinfo, '/a/b')) {
// b
if (preg_match('#^/a/b/(?P<var>[^/]++)$#s', $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<var>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'c')), array ());
}
// c
if (0 === strpos($pathinfo, '/a/b/c') && preg_match('#^/a/b/c/(?P<var>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'c')), array ());
}
}

View File

@ -57,42 +57,39 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
not_head_and_get:
if (0 === strpos($pathinfo, '/p')) {
// post_and_head
if ('/post_and_get' === $pathinfo) {
if (!in_array($requestMethod, array('POST', 'HEAD'))) {
$allow = array_merge($allow, array('POST', 'HEAD'));
goto not_post_and_head;
}
return array('_route' => 'post_and_head');
// post_and_head
if ('/post_and_get' === $pathinfo) {
if (!in_array($requestMethod, array('POST', 'HEAD'))) {
$allow = array_merge($allow, array('POST', 'HEAD'));
goto not_post_and_head;
}
not_post_and_head:
if (0 === strpos($pathinfo, '/put_and_post')) {
// put_and_post
if ('/put_and_post' === $pathinfo) {
if (!in_array($requestMethod, array('PUT', 'POST'))) {
$allow = array_merge($allow, array('PUT', 'POST'));
goto not_put_and_post;
}
return array('_route' => 'post_and_head');
}
not_post_and_head:
return array('_route' => 'put_and_post');
if (0 === strpos($pathinfo, '/put_and_post')) {
// put_and_post
if ('/put_and_post' === $pathinfo) {
if (!in_array($requestMethod, array('PUT', 'POST'))) {
$allow = array_merge($allow, array('PUT', 'POST'));
goto not_put_and_post;
}
not_put_and_post:
// put_and_get_and_head
if ('/put_and_post' === $pathinfo) {
if (!in_array($canonicalMethod, array('PUT', 'GET'))) {
$allow = array_merge($allow, array('PUT', 'GET'));
goto not_put_and_get_and_head;
}
return array('_route' => 'put_and_get_and_head');
}
not_put_and_get_and_head:
return array('_route' => 'put_and_post');
}
not_put_and_post:
// put_and_get_and_head
if ('/put_and_post' === $pathinfo) {
if (!in_array($canonicalMethod, array('PUT', 'GET'))) {
$allow = array_merge($allow, array('PUT', 'GET'));
goto not_put_and_get_and_head;
}
return array('_route' => 'put_and_get_and_head');
}
not_put_and_get_and_head:
}

View File

@ -0,0 +1,158 @@
<?php
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RequestContext;
/**
* ProjectUrlMatcher.
*
* This class has been auto-generated
* by the Symfony Routing Component.
*/
class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\RedirectableUrlMatcher
{
/**
* Constructor.
*/
public function __construct(RequestContext $context)
{
$this->context = $context;
}
public function match($pathinfo)
{
$allow = array();
$pathinfo = rawurldecode($pathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$request = $this->request;
$requestMethod = $canonicalMethod = $context->getMethod();
$scheme = $context->getScheme();
if ('HEAD' === $requestMethod) {
$canonicalMethod = 'GET';
}
if (0 === strpos($pathinfo, '/a')) {
// a_first
if ('/a/11' === $pathinfo) {
return array('_route' => 'a_first');
}
// a_second
if ('/a/22' === $pathinfo) {
return array('_route' => 'a_second');
}
// a_third
if ('/a/333' === $pathinfo) {
return array('_route' => 'a_third');
}
}
// a_wildcard
if (preg_match('#^/(?P<param>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'a_wildcard')), array ());
}
if (0 === strpos($pathinfo, '/a')) {
// a_fourth
if ('/a/44' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'a_fourth');
}
return array('_route' => 'a_fourth');
}
// a_fifth
if ('/a/55' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'a_fifth');
}
return array('_route' => 'a_fifth');
}
// a_sixth
if ('/a/66' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'a_sixth');
}
return array('_route' => 'a_sixth');
}
}
// nested_wildcard
if (0 === strpos($pathinfo, '/nested') && preg_match('#^/nested/(?P<param>[^/]++)$#s', $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) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'nested_a');
}
return array('_route' => 'nested_a');
}
// nested_b
if ('/nested/group/b' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'nested_b');
}
return array('_route' => 'nested_b');
}
// nested_c
if ('/nested/group/c' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'nested_c');
}
return array('_route' => 'nested_c');
}
}
elseif (0 === strpos($pathinfo, '/slashed/group')) {
// slashed_a
if ('/slashed/group' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'slashed_a');
}
return array('_route' => 'slashed_a');
}
// slashed_b
if ('/slashed/group/b' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'slashed_b');
}
return array('_route' => 'slashed_b');
}
// slashed_c
if ('/slashed/group/c' === $trimmedPathinfo) {
if (substr($pathinfo, -1) !== '/') {
return $this->redirect($pathinfo.'/', 'slashed_c');
}
return array('_route' => 'slashed_c');
}
}
throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException();
}
}

View File

@ -1,124 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Tests\Matcher\Dumper;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\Matcher\Dumper\DumperPrefixCollection;
use Symfony\Component\Routing\Matcher\Dumper\DumperRoute;
use Symfony\Component\Routing\Matcher\Dumper\DumperCollection;
class DumperPrefixCollectionTest extends TestCase
{
public function testAddPrefixRoute()
{
$coll = new DumperPrefixCollection();
$coll->setPrefix('');
$route = new DumperRoute('bar', new Route('/foo/bar'));
$coll = $coll->addPrefixRoute($route);
$route = new DumperRoute('bar2', new Route('/foo/bar'));
$coll = $coll->addPrefixRoute($route);
$route = new DumperRoute('qux', new Route('/foo/qux'));
$coll = $coll->addPrefixRoute($route);
$route = new DumperRoute('bar3', new Route('/foo/bar'));
$coll = $coll->addPrefixRoute($route);
$route = new DumperRoute('bar4', new Route(''));
$result = $coll->addPrefixRoute($route);
$expect = <<<'EOF'
|-coll /
| |-coll /f
| | |-coll /fo
| | | |-coll /foo
| | | | |-coll /foo/
| | | | | |-coll /foo/b
| | | | | | |-coll /foo/ba
| | | | | | | |-coll /foo/bar
| | | | | | | | |-route bar /foo/bar
| | | | | | | | |-route bar2 /foo/bar
| | | | | |-coll /foo/q
| | | | | | |-coll /foo/qu
| | | | | | | |-coll /foo/qux
| | | | | | | | |-route qux /foo/qux
| | | | | |-coll /foo/b
| | | | | | |-coll /foo/ba
| | | | | | | |-coll /foo/bar
| | | | | | | | |-route bar3 /foo/bar
| |-route bar4 /
EOF;
$this->assertSame($expect, $this->collectionToString($result->getRoot(), ' '));
}
public function testMergeSlashNodes()
{
$coll = new DumperPrefixCollection();
$coll->setPrefix('');
$route = new DumperRoute('bar', new Route('/foo/bar'));
$coll = $coll->addPrefixRoute($route);
$route = new DumperRoute('bar2', new Route('/foo/bar'));
$coll = $coll->addPrefixRoute($route);
$route = new DumperRoute('qux', new Route('/foo/qux'));
$coll = $coll->addPrefixRoute($route);
$route = new DumperRoute('bar3', new Route('/foo/bar'));
$result = $coll->addPrefixRoute($route);
$result->getRoot()->mergeSlashNodes();
$expect = <<<'EOF'
|-coll /f
| |-coll /fo
| | |-coll /foo
| | | |-coll /foo/b
| | | | |-coll /foo/ba
| | | | | |-coll /foo/bar
| | | | | | |-route bar /foo/bar
| | | | | | |-route bar2 /foo/bar
| | | |-coll /foo/q
| | | | |-coll /foo/qu
| | | | | |-coll /foo/qux
| | | | | | |-route qux /foo/qux
| | | |-coll /foo/b
| | | | |-coll /foo/ba
| | | | | |-coll /foo/bar
| | | | | | |-route bar3 /foo/bar
EOF;
$this->assertSame($expect, $this->collectionToString($result->getRoot(), ' '));
}
private function collectionToString(DumperCollection $collection, $prefix)
{
$string = '';
foreach ($collection as $route) {
if ($route instanceof DumperCollection) {
$string .= sprintf("%s|-coll %s\n", $prefix, $route->getPrefix());
$string .= $this->collectionToString($route, $prefix.'| ');
} else {
$string .= sprintf("%s|-route %s %s\n", $prefix, $route->getName(), $route->getRoute()->getPath());
}
}
return $string;
}
}

View File

@ -327,11 +327,30 @@ class PhpMatcherDumperTest extends TestCase
array('PUT', 'GET', 'HEAD')
));
/* test case 5 */
$groupOptimisedCollection = new RouteCollection();
$groupOptimisedCollection->add('a_first', new Route('/a/11'));
$groupOptimisedCollection->add('a_second', new Route('/a/22'));
$groupOptimisedCollection->add('a_third', new Route('/a/333'));
$groupOptimisedCollection->add('a_wildcard', new Route('/{param}'));
$groupOptimisedCollection->add('a_fourth', new Route('/a/44/'));
$groupOptimisedCollection->add('a_fifth', new Route('/a/55/'));
$groupOptimisedCollection->add('a_sixth', new Route('/a/66/'));
$groupOptimisedCollection->add('nested_wildcard', new Route('/nested/{param}'));
$groupOptimisedCollection->add('nested_a', new Route('/nested/group/a/'));
$groupOptimisedCollection->add('nested_b', new Route('/nested/group/b/'));
$groupOptimisedCollection->add('nested_c', new Route('/nested/group/c/'));
$groupOptimisedCollection->add('slashed_a', new Route('/slashed/group/'));
$groupOptimisedCollection->add('slashed_b', new Route('/slashed/group/b/'));
$groupOptimisedCollection->add('slashed_c', new Route('/slashed/group/c/'));
return array(
array($collection, 'url_matcher1.php', array()),
array($redirectCollection, 'url_matcher2.php', array('base_class' => 'Symfony\Component\Routing\Tests\Fixtures\RedirectableUrlMatcher')),
array($rootprefixCollection, 'url_matcher3.php', array()),
array($headMatchCasesCollection, 'url_matcher4.php', array()),
array($groupOptimisedCollection, 'url_matcher5.php', array('base_class' => 'Symfony\Component\Routing\Tests\Fixtures\RedirectableUrlMatcher')),
);
}
}

View File

@ -0,0 +1,175 @@
<?php
namespace Symfony\Component\Routing\Tests\Matcher\Dumper;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Routing\Matcher\Dumper\StaticPrefixCollection;
use Symfony\Component\Routing\Route;
class StaticPrefixCollectionTest extends TestCase
{
/**
* @dataProvider routeProvider
*/
public function testGrouping(array $routes, $expected)
{
$collection = new StaticPrefixCollection('/');
foreach ($routes as $route) {
list($path, $name) = $route;
$staticPrefix = (new Route($path))->compile()->getStaticPrefix();
$collection->addRoute($staticPrefix, $name);
}
$collection->optimizeGroups();
$dumped = $this->dumpCollection($collection);
$this->assertEquals($expected, $dumped);
}
public function routeProvider()
{
return array(
'Simple - not nested' => array(
array(
array('/', 'root'),
array('/prefix/segment/', 'prefix_segment'),
array('/leading/segment/', 'leading_segment'),
),
<<<EOF
/ root
/prefix/segment prefix_segment
/leading/segment leading_segment
EOF
),
'Not nested - group too small' => array(
array(
array('/', 'root'),
array('/prefix/segment/aa', 'prefix_segment'),
array('/prefix/segment/bb', 'leading_segment'),
),
<<<EOF
/ root
/prefix/segment/aa prefix_segment
/prefix/segment/bb leading_segment
EOF
),
'Nested - contains item at intersection' => array(
array(
array('/', 'root'),
array('/prefix/segment/', 'prefix_segment'),
array('/prefix/segment/bb', 'leading_segment'),
),
<<<EOF
/ root
/prefix/segment
-> /prefix/segment prefix_segment
-> /prefix/segment/bb leading_segment
EOF
),
'Simple one level nesting' => array(
array(
array('/', 'root'),
array('/group/segment/', 'nested_segment'),
array('/group/thing/', 'some_segment'),
array('/group/other/', 'other_segment'),
),
<<<EOF
/ root
/group
-> /group/segment nested_segment
-> /group/thing some_segment
-> /group/other other_segment
EOF
),
'Retain matching order with groups' => array(
array(
array('/group/aa/', 'aa'),
array('/group/bb/', 'bb'),
array('/group/cc/', 'cc'),
array('/', 'root'),
array('/group/dd/', 'dd'),
array('/group/ee/', 'ee'),
array('/group/ff/', 'ff'),
),
<<<EOF
/group
-> /group/aa aa
-> /group/bb bb
-> /group/cc cc
/ root
/group
-> /group/dd dd
-> /group/ee ee
-> /group/ff ff
EOF
),
'Retain complex matching order with groups at base' => array(
array(
array('/aaa/111/', 'first_aaa'),
array('/prefixed/group/aa/', 'aa'),
array('/prefixed/group/bb/', 'bb'),
array('/prefixed/group/cc/', 'cc'),
array('/prefixed/', 'root'),
array('/prefixed/group/dd/', 'dd'),
array('/prefixed/group/ee/', 'ee'),
array('/prefixed/group/ff/', 'ff'),
array('/aaa/222/', 'second_aaa'),
array('/aaa/333/', 'third_aaa'),
),
<<<EOF
/aaa
-> /aaa/111 first_aaa
-> /aaa/222 second_aaa
-> /aaa/333 third_aaa
/prefixed
-> /prefixed/group
-> -> /prefixed/group/aa aa
-> -> /prefixed/group/bb bb
-> -> /prefixed/group/cc cc
-> /prefixed root
-> /prefixed/group
-> -> /prefixed/group/dd dd
-> -> /prefixed/group/ee ee
-> -> /prefixed/group/ff ff
EOF
),
'Group regardless of segments' => array(
array(
array('/aaa-111/', 'a1'),
array('/aaa-222/', 'a2'),
array('/aaa-333/', 'a3'),
array('/group-aa/', 'g1'),
array('/group-bb/', 'g2'),
array('/group-cc/', 'g3'),
),
<<<EOF
/aaa-
-> /aaa-111 a1
-> /aaa-222 a2
-> /aaa-333 a3
/group-
-> /group-aa g1
-> /group-bb g2
-> /group-cc g3
EOF
),
);
}
private function dumpCollection(StaticPrefixCollection $collection, $prefix = '')
{
$lines = array();
foreach ($collection->getItems() as $item) {
if ($item instanceof StaticPrefixCollection) {
$lines[] = $prefix.$item->getPrefix();
$lines[] = $this->dumpCollection($item, $prefix.'-> ');
} else {
$lines[] = $prefix.implode(' ', $item);
}
}
return implode("\n", $lines);
}
}

View File

@ -127,7 +127,7 @@ class RouteCompilerTest extends TestCase
array(
'Route with a variable in last position',
array('/foo-{bar}'),
'/foo', '#^/foo\-(?P<bar>[^/]++)$#s', array('bar'), array(
'/foo-', '#^/foo\-(?P<bar>[^/]++)$#s', array('bar'), array(
array('variable', '-', '[^/]++', 'bar'),
array('text', '/foo'),
),