From 85d11af880e4a6525da66f99c59aed9d0aa89e20 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Sat, 14 Apr 2012 19:18:29 +0200 Subject: [PATCH] [Routing] added hostname matching support to PhpMatcherDumper --- .../Matcher/Dumper/DumperCollection.php | 48 +++++++ .../Matcher/Dumper/PhpMatcherDumper.php | 100 +++++++++++++-- .../Tests/Fixtures/dumper/url_matcher1.php | 117 ++++++++++++++++-- .../Tests/Fixtures/dumper/url_matcher2.php | 117 ++++++++++++++++-- .../Matcher/Dumper/DumperCollectionTest.php | 25 ++++ .../Matcher/Dumper/PhpMatcherDumperTest.php | 57 +++++++++ 6 files changed, 437 insertions(+), 27 deletions(-) create mode 100644 src/Symfony/Component/Routing/Tests/Matcher/Dumper/DumperCollectionTest.php diff --git a/src/Symfony/Component/Routing/Matcher/Dumper/DumperCollection.php b/src/Symfony/Component/Routing/Matcher/Dumper/DumperCollection.php index c75e9a0be8..cab4ad27c0 100644 --- a/src/Symfony/Component/Routing/Matcher/Dumper/DumperCollection.php +++ b/src/Symfony/Component/Routing/Matcher/Dumper/DumperCollection.php @@ -98,4 +98,52 @@ class DumperCollection implements \IteratorAggregate { $this->parent = $parent; } + + /** + * Returns true if the attribute is defined + * + * @param string $name The attribute name + * @return Boolean 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) + { + if ($this->hasAttribute($name)) { + return $this->attributes[$name]; + } else { + return $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/PhpMatcherDumper.php b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php index df678bcec8..f245818998 100644 --- a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php +++ b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php @@ -109,10 +109,37 @@ EOF; */ private function compileRoutes(RouteCollection $routes, $supportsRedirections) { - $collection = $this->flattenRouteCollection($routes); - $tree = $this->buildPrefixTree($collection); + $fetchedHostname = false; - return $this->compilePrefixRoutes($tree, $supportsRedirections); + $routes = $this->flattenRouteCollection($routes); + $groups = $this->groupRoutesByHostnameRegex($routes); + $code = ''; + + foreach ($groups as $collection) { + if (null !== $regex = $collection->getAttribute('hostname_regex')) { + + if (!$fetchedHostname) { + $code .= " \$hostname = \$this->context->getHost();\n\n"; + $fetchedHostname = true; + } + + $code .= sprintf(" if (preg_match(%s, \$hostname, \$hostnameMatches)) {\n", var_export($regex, true)); + } + + $tree = $this->buildPrefixTree($collection); + $groupCode = $this->compilePrefixRoutes($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; + } + } + + return $code; } /** @@ -171,6 +198,7 @@ EOF; $conditions = array(); $hasTrailingSlash = false; $matches = false; + $hostnameMatches = false; $methods = array(); if ($req = $route->getRequirement('_method')) { @@ -183,7 +211,7 @@ EOF; $supportsTrailingSlash = $supportsRedirections && (!$methods || in_array('HEAD', $methods)); - if (!count($compiledRoute->getVariables()) && false !== preg_match('#^(.)\^(?.*?)\$\1#', $compiledRoute->getRegex(), $m)) { + if (!count($compiledRoute->getPathVariables()) && false !== preg_match('#^(.)\^(?.*?)\$\1#', $compiledRoute->getRegex(), $m)) { if ($supportsTrailingSlash && substr($m['url'], -1) === '/') { $conditions[] = sprintf("rtrim(\$pathinfo, '/') === %s", var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true)); $hasTrailingSlash = true; @@ -205,6 +233,10 @@ EOF; $matches = true; } + if ($compiledRoute->getHostnameVariables()) { + $hostnameMatches = true; + } + $conditions = implode(' && ', $conditions); $code .= <<getDefaults()) { - $code .= sprintf(" return array_merge(\$this->mergeDefaults(\$matches, %s), array('_route' => '%s'));\n" - , str_replace("\n", '', var_export($route->getDefaults(), true)), $name); - } elseif (true === $matches) { + if (($matches || $hostnameMatches) && $route->getDefaults()) { + + $vars = array(); + if ($matches) { + $vars[] = '$matches'; + } + if ($hostnameMatches) { + $vars[] = '$hostnameMatches'; + } + $matchesExpr = implode(' + ', $vars); + + $code .= sprintf(" return array_merge(\$this->mergeDefaults(%s, %s), array('_route' => '%s'));\n" + , $matchesExpr, str_replace("\n", '', var_export($route->getDefaults(), true)), $name); + + } elseif ($matches || $hostnameMatches) { + + if (!$matches) { + $code .= " \$matches = \$hostnameMatches;\n"; + } else { + if ($hostnameMatches) { + $code .= " \$matches = \$matches + \$hostnameMatches;\n"; + } + } + $code .= sprintf(" \$matches['_route'] = '%s';\n\n", $name); $code .= " return \$matches;\n"; } elseif ($route->getDefaults()) { @@ -308,6 +360,37 @@ EOF; return $to; } + /** + * Groups consecutive routes having the same hostname regex + * + * The results is a collection of collections of routes having the same + * hostnameRegex + * + * @param DumperCollection $routes Flat collection of DumperRoutes + * + * @return DumperCollection A collection with routes grouped by hostname regex in sub-collections + */ + private function groupRoutesByHostnameRegex(DumperCollection $routes) + { + $groups = new DumperCollection(); + + $currentGroup = new DumperCollection(); + $currentGroup->setAttribute('hostname_regex', null); + $groups->add($currentGroup); + + foreach ($routes as $route) { + $hostnameRegex = $route->getRoute()->compile()->getHostnameRegex(); + if ($currentGroup->getAttribute('hostname_regex') !== $hostnameRegex) { + $currentGroup = new DumperCollection(); + $currentGroup->setAttribute('hostname_regex', $hostnameRegex); + $groups->add($currentGroup); + } + $currentGroup->add($route); + } + + return $groups; + } + /** * Organizes the routes into a prefix tree. * @@ -331,5 +414,4 @@ EOF; return $tree; } - } 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 9fafa7b3e1..8111e41715 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php @@ -206,22 +206,121 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher return $matches; } - if (0 === strpos($pathinfo, '/a')) { - if (0 === strpos($pathinfo, '/aba')) { - // ababa - if ($pathinfo === '/ababa') { - return array('_route' => 'ababa'); - } + if (0 === strpos($pathinfo, '/aba')) { + // ababa + if ($pathinfo === '/ababa') { + return array('_route' => 'ababa'); + } - // foo4 - if (preg_match('#^/aba/(?[^/]++)$#s', $pathinfo, $matches)) { - $matches['_route'] = 'foo4'; + // foo4 + if (preg_match('#^/aba/(?[^/]++)$#s', $pathinfo, $matches)) { + $matches['_route'] = 'foo4'; + + return $matches; + } + + } + + $hostname = $this->context->getHost(); + + if (preg_match('#^a\\.example\\.com$#s', $hostname, $hostnameMatches)) { + // route1 + if ($pathinfo === '/route1') { + return array('_route' => 'route1'); + } + + // route2 + if ($pathinfo === '/c2/route2') { + return array('_route' => 'route2'); + } + + } + + if (preg_match('#^b\\.example\\.com$#s', $hostname, $hostnameMatches)) { + // route3 + if ($pathinfo === '/c2/route3') { + return array('_route' => 'route3'); + } + + } + + if (preg_match('#^a\\.example\\.com$#s', $hostname, $hostnameMatches)) { + // route4 + if ($pathinfo === '/route4') { + return array('_route' => 'route4'); + } + + } + + if (preg_match('#^c\\.example\\.com$#s', $hostname, $hostnameMatches)) { + // route5 + if ($pathinfo === '/route5') { + return array('_route' => 'route5'); + } + + } + + // route6 + if ($pathinfo === '/route6') { + return array('_route' => 'route6'); + } + + if (preg_match('#^(?[^\\.]++)\\.example\\.com$#s', $hostname, $hostnameMatches)) { + if (0 === strpos($pathinfo, '/route1')) { + // route11 + if ($pathinfo === '/route11') { + $matches = $hostnameMatches; + $matches['_route'] = 'route11'; return $matches; } + // route12 + if ($pathinfo === '/route12') { + return array_merge($this->mergeDefaults($hostnameMatches, array ( 'var1' => 'val',)), array('_route' => 'route12')); + } + + // route13 + if (0 === strpos($pathinfo, '/route13') && preg_match('#^/route13/(?[^/]++)$#s', $pathinfo, $matches)) { + $matches = $matches + $hostnameMatches; + $matches['_route'] = 'route13'; + + return $matches; + } + + // route14 + if (0 === strpos($pathinfo, '/route14') && preg_match('#^/route14/(?[^/]++)$#s', $pathinfo, $matches)) { + return array_merge($this->mergeDefaults($matches + $hostnameMatches, array ( 'var1' => 'val',)), array('_route' => 'route14')); + } + } + } + + if (preg_match('#^c\\.example\\.com$#s', $hostname, $hostnameMatches)) { + // route15 + if (0 === strpos($pathinfo, '/route15') && preg_match('#^/route15/(?[^/]++)$#s', $pathinfo, $matches)) { + $matches['_route'] = 'route15'; + + return $matches; + } + + } + + if (0 === strpos($pathinfo, '/route1')) { + // route16 + if (0 === strpos($pathinfo, '/route16') && preg_match('#^/route16/(?[^/]++)$#s', $pathinfo, $matches)) { + return array_merge($this->mergeDefaults($matches, array ( 'var1' => 'val',)), array('_route' => 'route16')); + } + + // route17 + if ($pathinfo === '/route17') { + return array('_route' => 'route17'); + } + + } + + if (0 === strpos($pathinfo, '/a')) { // a if ($pathinfo === '/a/a...') { return array('_route' => 'a'); 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 7dcc8ccb28..16040bc29c 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.php @@ -218,22 +218,121 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec return $matches; } - if (0 === strpos($pathinfo, '/a')) { - if (0 === strpos($pathinfo, '/aba')) { - // ababa - if ($pathinfo === '/ababa') { - return array('_route' => 'ababa'); - } + if (0 === strpos($pathinfo, '/aba')) { + // ababa + if ($pathinfo === '/ababa') { + return array('_route' => 'ababa'); + } - // foo4 - if (preg_match('#^/aba/(?[^/]++)$#s', $pathinfo, $matches)) { - $matches['_route'] = 'foo4'; + // foo4 + if (preg_match('#^/aba/(?[^/]++)$#s', $pathinfo, $matches)) { + $matches['_route'] = 'foo4'; + + return $matches; + } + + } + + $hostname = $this->context->getHost(); + + if (preg_match('#^a\\.example\\.com$#s', $hostname, $hostnameMatches)) { + // route1 + if ($pathinfo === '/route1') { + return array('_route' => 'route1'); + } + + // route2 + if ($pathinfo === '/c2/route2') { + return array('_route' => 'route2'); + } + + } + + if (preg_match('#^b\\.example\\.com$#s', $hostname, $hostnameMatches)) { + // route3 + if ($pathinfo === '/c2/route3') { + return array('_route' => 'route3'); + } + + } + + if (preg_match('#^a\\.example\\.com$#s', $hostname, $hostnameMatches)) { + // route4 + if ($pathinfo === '/route4') { + return array('_route' => 'route4'); + } + + } + + if (preg_match('#^c\\.example\\.com$#s', $hostname, $hostnameMatches)) { + // route5 + if ($pathinfo === '/route5') { + return array('_route' => 'route5'); + } + + } + + // route6 + if ($pathinfo === '/route6') { + return array('_route' => 'route6'); + } + + if (preg_match('#^(?[^\\.]++)\\.example\\.com$#s', $hostname, $hostnameMatches)) { + if (0 === strpos($pathinfo, '/route1')) { + // route11 + if ($pathinfo === '/route11') { + $matches = $hostnameMatches; + $matches['_route'] = 'route11'; return $matches; } + // route12 + if ($pathinfo === '/route12') { + return array_merge($this->mergeDefaults($hostnameMatches, array ( 'var1' => 'val',)), array('_route' => 'route12')); + } + + // route13 + if (0 === strpos($pathinfo, '/route13') && preg_match('#^/route13/(?[^/]++)$#s', $pathinfo, $matches)) { + $matches = $matches + $hostnameMatches; + $matches['_route'] = 'route13'; + + return $matches; + } + + // route14 + if (0 === strpos($pathinfo, '/route14') && preg_match('#^/route14/(?[^/]++)$#s', $pathinfo, $matches)) { + return array_merge($this->mergeDefaults($matches + $hostnameMatches, array ( 'var1' => 'val',)), array('_route' => 'route14')); + } + } + } + + if (preg_match('#^c\\.example\\.com$#s', $hostname, $hostnameMatches)) { + // route15 + if (0 === strpos($pathinfo, '/route15') && preg_match('#^/route15/(?[^/]++)$#s', $pathinfo, $matches)) { + $matches['_route'] = 'route15'; + + return $matches; + } + + } + + if (0 === strpos($pathinfo, '/route1')) { + // route16 + if (0 === strpos($pathinfo, '/route16') && preg_match('#^/route16/(?[^/]++)$#s', $pathinfo, $matches)) { + return array_merge($this->mergeDefaults($matches, array ( 'var1' => 'val',)), array('_route' => 'route16')); + } + + // route17 + if ($pathinfo === '/route17') { + return array('_route' => 'route17'); + } + + } + + if (0 === strpos($pathinfo, '/a')) { // a if ($pathinfo === '/a/a...') { return array('_route' => 'a'); diff --git a/src/Symfony/Component/Routing/Tests/Matcher/Dumper/DumperCollectionTest.php b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/DumperCollectionTest.php new file mode 100644 index 0000000000..7f2d914424 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/DumperCollectionTest.php @@ -0,0 +1,25 @@ +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 ee113851cd..aec0a9148f 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php @@ -14,6 +14,7 @@ namespace Symfony\Component\Routing\Tests\Matcher\Dumper; use Symfony\Component\Routing\Matcher\Dumper\PhpMatcherDumper; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Routing\Matcher\Dumper\DumperCollection; class PhpMatcherDumperTest extends \PHPUnit_Framework_TestCase { @@ -41,6 +42,7 @@ class PhpMatcherDumperTest extends \PHPUnit_Framework_TestCase $dumper = new PhpMatcherDumper($collection); + file_put_contents('/tmp/' . $fixture, $dumper->dump($options)); $this->assertStringEqualsFile($basePath.$fixture, $dumper->dump($options), '->dump() correctly dumps routes as optimized PHP code.'); } @@ -156,6 +158,61 @@ class PhpMatcherDumperTest extends \PHPUnit_Framework_TestCase $collection1->add('foo4', new Route('/{foo}')); $collection->addCollection($collection1, '/aba'); + // prefix and hostname + + $collection1 = new RouteCollection(); + + $route1 = new Route('/route1', array(), array(), array(), 'a.example.com'); + $collection1->add('route1', $route1); + + $collection2 = new RouteCollection(); + + $route2 = new Route('/route2', array(), array(), array(), 'a.example.com'); + $collection2->add('route2', $route2); + + $route3 = new Route('/route3', array(), array(), array(), 'b.example.com'); + $collection2->add('route3', $route3); + + $collection1->addCollection($collection2, '/c2'); + + $route4 = new Route('/route4', array(), array(), array(), 'a.example.com'); + $collection1->add('route4', $route4); + + $route5 = new Route('/route5', array(), array(), array(), 'c.example.com'); + $collection1->add('route5', $route5); + + $route6 = new Route('/route6', array(), array(), array(), null); + $collection1->add('route6', $route6); + + $collection->addCollection($collection1); + + // hostname and variables + + $collection1 = new RouteCollection(); + + $route11 = new Route('/route11', array(), array(), array(), '{var1}.example.com'); + $collection1->add('route11', $route11); + + $route12 = new Route('/route12', array('var1' => 'val'), array(), array(), '{var1}.example.com'); + $collection1->add('route12', $route12); + + $route13 = new Route('/route13/{name}', array(), array(), array(), '{var1}.example.com'); + $collection1->add('route13', $route13); + + $route14 = new Route('/route14/{name}', array('var1' => 'val'), array(), array(), '{var1}.example.com'); + $collection1->add('route14', $route14); + + $route15 = new Route('/route15/{name}', array(), array(), array(), 'c.example.com'); + $collection1->add('route15', $route15); + + $route16 = new Route('/route16/{name}', array('var1' => 'val'), array(), array(), null); + $collection1->add('route16', $route16); + + $route17 = new Route('/route17', array(), array(), array(), null); + $collection1->add('route17', $route17); + + $collection->addCollection($collection1); + // multiple sub-collections with a single route and a prefix each $collection1 = new RouteCollection(); $collection1->add('a', new Route('/a...'));