From 944a98086ea240270e131e93034416fa17d96df2 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 25 Apr 2011 17:39:10 +0200 Subject: [PATCH] [Routing] optimized PHP route dumper --- .../Matcher/Dumper/PhpMatcherDumper.php | 235 ++++++++++-------- .../Component/Routing/Matcher/UrlMatcher.php | 34 ++- .../Component/Routing/RouteCollection.php | 57 ++++- 3 files changed, 209 insertions(+), 117 deletions(-) diff --git a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php index b4243e4229..6b1bae069e 100644 --- a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php +++ b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Routing\Matcher\Dumper; use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; /** * PhpMatcherDumper creates a PHP class able to match URLs for a given set of routes. @@ -53,107 +54,7 @@ class PhpMatcherDumper extends MatcherDumper private function addMatcher($supportsRedirections) { - $code = array(); - - foreach ($this->getRoutes()->all() as $name => $route) { - $compiledRoute = $route->compile(); - $conditions = array(); - $hasTrailingSlash = false; - $matches = false; - if (!count($compiledRoute->getVariables()) && false !== preg_match('#^(.)\^(?P.*?)\$\1#', str_replace(array("\n", ' '), '', $compiledRoute->getRegex()), $m)) { - if ($supportsRedirections && substr($m['url'], -1) === '/') { - $conditions[] = sprintf("rtrim(\$pathinfo, '/') === '%s'", rtrim(str_replace('\\', '', $m['url']), '/')); - $hasTrailingSlash = true; - } else { - $conditions[] = sprintf("\$pathinfo === '%s'", str_replace('\\', '', $m['url'])); - } - } else { - if ($compiledRoute->getStaticPrefix()) { - $conditions[] = sprintf("0 === strpos(\$pathinfo, '%s')", $compiledRoute->getStaticPrefix()); - } - - $regex = str_replace(array("\n", ' '), '', $compiledRoute->getRegex()); - if ($supportsRedirections && $pos = strpos($regex, '/$')) { - $regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2); - $hasTrailingSlash = true; - } - $conditions[] = sprintf("preg_match('%s', \$pathinfo, \$matches)", $regex); - - $matches = true; - } - - $conditions = implode(' && ', $conditions); - - $gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name); - - $code[] = <<getRequirement('_method')) { - $methods = array_map('strtolower', explode('|', $req)); - if (1 === count($methods)) { - $code[] = <<context->getMethod() != '$methods[0]') { - \$allow[] = '$methods[0]'; - goto $gotoname; - } -EOF; - } else { - $methods = implode('\', \'', $methods); - $code[] = <<context->getMethod(), array('$methods'))) { - \$allow = array_merge(\$allow, array('$methods')); - goto $gotoname; - } -EOF; - } - } - - if ($hasTrailingSlash) { - $code[] = sprintf(<<redirect(\$pathinfo.'/', '%s'); - } -EOF - , $name); - } - - if ($scheme = $route->getRequirement('_scheme')) { - if (!$supportsRedirections) { - throw new \LogicException('The "_scheme" requirement is only supported for route dumper that implements RedirectableUrlMatcherInterface.'); - } - - $code[] = sprintf(<<context->getScheme() !== '$scheme') { - return \$this->redirect(\$pathinfo, '%s', '$scheme'); - } -EOF - , $name); - } - - // optimize parameters array - if (true === $matches && $compiledRoute->getDefaults()) { - $code[] = sprintf(" return array_merge(\$this->mergeDefaults(\$matches, %s), array('_route' => '%s'));" - , str_replace("\n", '', var_export($compiledRoute->getDefaults(), true)), $name); - } elseif (true === $matches) { - $code[] = sprintf(" \$matches['_route'] = '%s';\n return \$matches;", $name); - } elseif ($compiledRoute->getDefaults()) { - $code[] = sprintf(' return %s;', str_replace("\n", '', var_export(array_merge($compiledRoute->getDefaults(), array('_route' => $name)), true))); - } else { - $code[] = sprintf(" return array('_route' => '%s');", $name); - } - $code[] = " }"; - - if ($req) { - $code[] = " $gotoname:"; - } - - $code[] = ''; - } - - $code = implode("\n", $code); + $code = implode("\n", $this->compileRoutes($this->getRoutes(), $supportsRedirections)); return << $route) { + if ($route instanceof RouteCollection) { + $indent = ''; + if (count($route->all()) > 1 && $prefix = $route->getPrefix()) { + $code[] = sprintf(" if (0 === strpos(\$pathinfo, '%s')) {", $prefix); + $indent = ' '; + } + + foreach ($this->compileRoutes($route, $supportsRedirections) as $line) { + foreach (explode("\n", $line) as $l) { + $code[] = $indent.$l; + } + } + + if ($indent) { + $code[] = " }\n"; + } + } else { + foreach ($this->compileRoute($route, $name, $supportsRedirections) as $line) { + $code[] = $line; + } + } + } + + return $code; + } + + private function compileRoute(Route $route, $name, $supportsRedirections) + { + $compiledRoute = $route->compile(); + $conditions = array(); + $hasTrailingSlash = false; + $matches = false; + if (!count($compiledRoute->getVariables()) && false !== preg_match('#^(.)\^(?P.*?)\$\1#', str_replace(array("\n", ' '), '', $compiledRoute->getRegex()), $m)) { + if ($supportsRedirections && substr($m['url'], -1) === '/') { + $conditions[] = sprintf("rtrim(\$pathinfo, '/') === '%s'", rtrim(str_replace('\\', '', $m['url']), '/')); + $hasTrailingSlash = true; + } else { + $conditions[] = sprintf("\$pathinfo === '%s'", str_replace('\\', '', $m['url'])); + } + } else { + if ($compiledRoute->getStaticPrefix()) { + $conditions[] = sprintf("0 === strpos(\$pathinfo, '%s')", $compiledRoute->getStaticPrefix()); + } + + $regex = str_replace(array("\n", ' '), '', $compiledRoute->getRegex()); + if ($supportsRedirections && $pos = strpos($regex, '/$')) { + $regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2); + $hasTrailingSlash = true; + } + $conditions[] = sprintf("preg_match('%s', \$pathinfo, \$matches)", $regex); + + $matches = true; + } + + $conditions = implode(' && ', $conditions); + + $gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name); + + $code[] = <<getRequirement('_method')) { + $methods = array_map('strtolower', explode('|', $req)); + if (1 === count($methods)) { + $code[] = <<context->getMethod() != '$methods[0]') { + \$allow[] = '$methods[0]'; + goto $gotoname; + } +EOF; + } else { + $methods = implode('\', \'', $methods); + $code[] = <<context->getMethod(), array('$methods'))) { + \$allow = array_merge(\$allow, array('$methods')); + goto $gotoname; + } +EOF; + } + } + + if ($hasTrailingSlash) { + $code[] = sprintf(<<redirect(\$pathinfo.'/', '%s'); + } +EOF + , $name); + } + + if ($scheme = $route->getRequirement('_scheme')) { + if (!$supportsRedirections) { + throw new \LogicException('The "_scheme" requirement is only supported for route dumper that implements RedirectableUrlMatcherInterface.'); + } + + $code[] = sprintf(<<context->getScheme() !== '$scheme') { + return \$this->redirect(\$pathinfo, '%s', '$scheme'); + } +EOF + , $name); + } + + // optimize parameters array + if (true === $matches && $compiledRoute->getDefaults()) { + $code[] = sprintf(" return array_merge(\$this->mergeDefaults(\$matches, %s), array('_route' => '%s'));" + , str_replace("\n", '', var_export($compiledRoute->getDefaults(), true)), $name); + } elseif (true === $matches) { + $code[] = sprintf(" \$matches['_route'] = '%s';", $name); + $code[] = sprintf(" return \$matches;", $name); + } elseif ($compiledRoute->getDefaults()) { + $code[] = sprintf(' return %s;', str_replace("\n", '', var_export(array_merge($compiledRoute->getDefaults(), array('_route' => $name)), true))); + } else { + $code[] = sprintf(" return array('_route' => '%s');", $name); + } + $code[] = " }"; + + if ($req) { + $code[] = " $gotoname:"; + } + + $code[] = ''; + + return $code; + } + private function startClass($class, $baseClass) { return <<allow = array(); + + if ($ret = $this->matchCollection($pathinfo, $this->routes)) { + return $ret; + } + + throw 0 < count($this->allow) + ? new MethodNotAllowedException(array_unique(array_map('strtolower', $this->allow))) + : new NotFoundException(); + } + + protected function matchCollection($pathinfo, RouteCollection $routes) + { + foreach ($routes as $name => $route) { + if ($route instanceof RouteCollection) { + if ($route->getPrefix() !== substr($pathinfo, 0, strlen($route->getPrefix()))) { + continue; + } + + if (!$ret = $this->matchCollection($pathinfo, $route)) { + continue; + } + + return $ret; + } - foreach ($this->routes->all() as $name => $route) { $compiledRoute = $route->compile(); // check the static prefix of the URL first. Only use the more expensive preg_match when it matches @@ -88,16 +111,13 @@ class UrlMatcher implements UrlMatcherInterface // check HTTP method requirement if ($route->getRequirement('_method') && ($req = explode('|', $route->getRequirement('_method'))) && !in_array($this->context->getMethod(), array_map('strtolower', $req))) { - $allow = array_merge($allow, $req); + $this->allow = array_merge($this->allow, $req); + continue; } return array_merge($this->mergeDefaults($matches, $route->getDefaults()), array('_route' => $name)); } - - throw 0 < count($allow) - ? new MethodNotAllowedException(array_unique(array_map('strtolower', $allow))) - : new NotFoundException(); } protected function mergeDefaults($params, $defaults) diff --git a/src/Symfony/Component/Routing/RouteCollection.php b/src/Symfony/Component/Routing/RouteCollection.php index 8ed7d5204c..359c0bfee0 100644 --- a/src/Symfony/Component/Routing/RouteCollection.php +++ b/src/Symfony/Component/Routing/RouteCollection.php @@ -18,10 +18,11 @@ use Symfony\Component\Config\Resource\ResourceInterface; * * @author Fabien Potencier */ -class RouteCollection +class RouteCollection implements \IteratorAggregate { private $routes; private $resources; + private $prefix; /** * Constructor. @@ -30,6 +31,12 @@ class RouteCollection { $this->routes = array(); $this->resources = array(); + $this->prefix = ''; + } + + public function getIterator() + { + return new \ArrayIterator($this->routes); } /** @@ -56,7 +63,16 @@ class RouteCollection */ public function all() { - return $this->routes; + $routes = array(); + foreach ($this->routes as $name => $route) { + if ($route instanceof RouteCollection) { + $routes = array_merge($routes, $route->all()); + } else { + $routes[$name] = $route; + } + } + + return $routes; } /** @@ -68,7 +84,20 @@ class RouteCollection */ public function get($name) { - return isset($this->routes[$name]) ? $this->routes[$name] : null; + // get the latest defined route + foreach (array_reverse($this->routes) as $routes) { + if (!$routes instanceof RouteCollection) { + continue; + } + + if (null !== $route = $routes->get($name)) { + return $route; + } + } + + if (isset($this->routes[$name])) { + return $this->routes[$name]; + }; } /** @@ -81,11 +110,7 @@ class RouteCollection { $collection->addPrefix($prefix); - foreach ($collection->getResources() as $resource) { - $this->addResource($resource); - } - - $this->routes = array_merge($this->routes, $collection->all()); + $this->routes[] = $collection; } /** @@ -99,11 +124,18 @@ class RouteCollection return; } + $this->prefix = $prefix.$this->prefix; + foreach ($this->all() as $route) { $route->setPattern($prefix.$route->getPattern()); } } + public function getPrefix() + { + return $this->prefix; + } + /** * Returns an array of resources loaded to build this collection. * @@ -111,7 +143,14 @@ class RouteCollection */ public function getResources() { - return array_unique($this->resources); + $resources = $this->resources; + foreach ($this as $routes) { + if ($routes instanceof RouteCollection) { + $resources = array_merge($resources, $routes->getResources()); + } + } + + return array_unique($resources); } /**