From d41db55647dd8afc22783512235657805bd52dd4 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 23 Jan 2018 10:29:33 +0100 Subject: [PATCH] [Routing] dump static arrays instead of classes for both matcher and generator --- .php_cs.dist | 2 + .../FrameworkExtension.php | 9 + .../Resources/config/routing.xml | 8 +- .../Routing/RedirectableStaticUrlMatcher.php | 37 ++ .../Routing/RedirectableUrlMatcher.php | 4 + .../RedirectableStaticUrlMatcherTest.php | 77 ++++ .../Routing/RedirectableUrlMatcherTest.php | 3 + .../Generator/Dumper/PhpGeneratorDumper.php | 4 + .../Dumper/StaticUrlGeneratorDumper.php | 64 +++ .../Routing/Generator/StaticUrlGenerator.php | 39 ++ .../Matcher/Dumper/PhpMatcherDumper.php | 4 + .../Matcher/Dumper/StaticUrlMatcherDumper.php | 296 ++++++++++++ .../Routing/Matcher/StaticUrlMatcher.php | 160 +++++++ src/Symfony/Component/Routing/Router.php | 32 +- .../Fixtures/dumper/static_url_matcher0.php | 8 + .../Fixtures/dumper/static_url_matcher1.php | 136 ++++++ .../Fixtures/dumper/static_url_matcher2.php | 138 ++++++ .../Fixtures/dumper/static_url_matcher3.php | 23 + .../Fixtures/dumper/static_url_matcher4.php | 24 + .../Fixtures/dumper/static_url_matcher5.php | 41 ++ .../Fixtures/dumper/static_url_matcher6.php | 43 ++ .../Fixtures/dumper/static_url_matcher7.php | 43 ++ .../Dumper/PhpGeneratorDumperTest.php | 3 + .../Dumper/StaticUrlGeneratorDumperTest.php | 170 +++++++ .../Matcher/Dumper/PhpMatcherDumperTest.php | 3 + .../Dumper/StaticUrlMatcherDumperTest.php | 427 ++++++++++++++++++ .../Tests/Matcher/StaticUrlMatcherTest.php | 28 ++ .../Routing/Tests/Matcher/UrlMatcherTest.php | 111 +++-- 28 files changed, 1888 insertions(+), 49 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableStaticUrlMatcher.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableStaticUrlMatcherTest.php create mode 100644 src/Symfony/Component/Routing/Generator/Dumper/StaticUrlGeneratorDumper.php create mode 100644 src/Symfony/Component/Routing/Generator/StaticUrlGenerator.php create mode 100644 src/Symfony/Component/Routing/Matcher/Dumper/StaticUrlMatcherDumper.php create mode 100644 src/Symfony/Component/Routing/Matcher/StaticUrlMatcher.php create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher0.php create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher1.php create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher2.php create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher3.php create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher4.php create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher5.php create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher6.php create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher7.php create mode 100644 src/Symfony/Component/Routing/Tests/Generator/Dumper/StaticUrlGeneratorDumperTest.php create mode 100644 src/Symfony/Component/Routing/Tests/Matcher/Dumper/StaticUrlMatcherDumperTest.php create mode 100644 src/Symfony/Component/Routing/Tests/Matcher/StaticUrlMatcherTest.php diff --git a/.php_cs.dist b/.php_cs.dist index c0011033990d8..71834bacaa7ac 100644 --- a/.php_cs.dist +++ b/.php_cs.dist @@ -40,5 +40,7 @@ return PhpCsFixer\Config::create() ->notPath('Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/default.phpt') ->notPath('Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/weak.phpt') ->notPath('Symfony/Component/Debug/Tests/DebugClassLoaderTest.php') + // file autogenerated + ->notPath('Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher3.php') ) ; diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index be81b91003c4d..04fd56d8e8ea4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -63,8 +63,11 @@ use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\Routing\Generator\Dumper\PhpGeneratorDumper; use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader; use Symfony\Component\Routing\Loader\AnnotationFileLoader; +use Symfony\Component\Routing\Matcher\Dumper\PhpMatcherDumper; +use Symfony\Component\Routing\Matcher\StaticUrlMatcher; use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Serializer\Encoder\DecoderInterface; @@ -642,6 +645,12 @@ private function registerRouterConfiguration(array $config, ContainerBuilder $co if (isset($config['type'])) { $argument['resource_type'] = $config['type']; } + if (!class_exists(StaticUrlMatcher::class)) { + $argument['generator_class'] = $argument['generator_base_class']; + $argument['generator_dumper_class'] = PhpGeneratorDumper::class; + $argument['matcher_class'] = $argument['matcher_base_class']; + $argument['matcher_dumper_class'] = PhpMatcherDumper::class; + } $router->replaceArgument(2, $argument); $container->setParameter('request_listener.http_port', $config['http_port']); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml index 5d25c9116f35a..d0a19424e6620 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml @@ -58,13 +58,13 @@ %kernel.cache_dir% %kernel.debug% - Symfony\Component\Routing\Generator\UrlGenerator + Symfony\Component\Routing\Generator\StaticUrlGenerator Symfony\Component\Routing\Generator\UrlGenerator - Symfony\Component\Routing\Generator\Dumper\PhpGeneratorDumper + Symfony\Component\Routing\Generator\Dumper\StaticUrlGeneratorDumper %router.cache_class_prefix%UrlGenerator - Symfony\Bundle\FrameworkBundle\Routing\RedirectableUrlMatcher + Symfony\Bundle\FrameworkBundle\Routing\RedirectableStaticUrlMatcher Symfony\Bundle\FrameworkBundle\Routing\RedirectableUrlMatcher - Symfony\Component\Routing\Matcher\Dumper\PhpMatcherDumper + Symfony\Component\Routing\Matcher\Dumper\StaticUrlMatcherDumper %router.cache_class_prefix%UrlMatcher diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableStaticUrlMatcher.php b/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableStaticUrlMatcher.php new file mode 100644 index 0000000000000..0692ffbb3c64f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableStaticUrlMatcher.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Routing; + +use Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface; +use Symfony\Component\Routing\Matcher\StaticUrlMatcher; + +/** + * @author Fabien Potencier + */ +class RedirectableStaticUrlMatcher extends StaticUrlMatcher implements RedirectableUrlMatcherInterface +{ + /** + * {@inheritdoc} + */ + public function redirect($path, $route, $scheme = null) + { + return array( + '_controller' => 'Symfony\\Bundle\\FrameworkBundle\\Controller\\RedirectController::urlRedirectAction', + 'path' => $path, + 'permanent' => true, + 'scheme' => $scheme, + 'httpPort' => $this->context->getHttpPort(), + 'httpsPort' => $this->context->getHttpsPort(), + '_route' => $route, + ); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableUrlMatcher.php b/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableUrlMatcher.php index 5571c74e81d19..c2a2ffafc08bf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableUrlMatcher.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableUrlMatcher.php @@ -11,10 +11,14 @@ namespace Symfony\Bundle\FrameworkBundle\Routing; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1 and will be removed in 5.0. Use RedirectableStaticUrlMatcher instead.', RedirectableUrlMatcher::class), E_USER_DEPRECATED); + use Symfony\Component\Routing\Matcher\RedirectableUrlMatcher as BaseMatcher; /** * @author Fabien Potencier + * + * @deprecated since Symfony 4.1, to be removed in 5.0. Use RedirectableStaticUrlMatcher instead. */ class RedirectableUrlMatcher extends BaseMatcher { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableStaticUrlMatcherTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableStaticUrlMatcherTest.php new file mode 100644 index 0000000000000..571a801f0d4cc --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableStaticUrlMatcherTest.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Routing; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Routing\Matcher\Dumper\StaticUrlMatcherDumper; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; +use Symfony\Bundle\FrameworkBundle\Routing\RedirectableStaticUrlMatcher; +use Symfony\Component\Routing\RequestContext; + +/** + * @requires function \Symfony\Component\Routing\Matcher\StaticUrlMatcher::match + */ +class RedirectableStaticUrlMatcherTest extends TestCase +{ + public function testRedirectWhenNoSlash() + { + $routes = new RouteCollection(); + $routes->add('foo', new Route('/foo/')); + + $matcher = $this->getMatcher($routes, $context = new RequestContext()); + + $this->assertEquals(array( + '_controller' => 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction', + 'path' => '/foo/', + 'permanent' => true, + 'scheme' => null, + 'httpPort' => $context->getHttpPort(), + 'httpsPort' => $context->getHttpsPort(), + '_route' => 'foo', + ), + $matcher->match('/foo') + ); + } + + public function testSchemeRedirect() + { + $routes = new RouteCollection(); + $routes->add('foo', new Route('/foo', array(), array(), array(), '', array('https'))); + + $matcher = $this->getMatcher($routes, $context = new RequestContext()); + + $this->assertEquals(array( + '_controller' => 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction', + 'path' => '/foo', + 'permanent' => true, + 'scheme' => 'https', + 'httpPort' => $context->getHttpPort(), + 'httpsPort' => $context->getHttpsPort(), + '_route' => 'foo', + ), + $matcher->match('/foo') + ); + } + + private function getMatcher(RouteCollection $routes, RequestContext $context) + { + $dumper = new StaticUrlMatcherDumper($routes); + $path = sys_get_temp_dir().'/php_matcher.'.uniqid('StaticUrlMatcher').'.php'; + + file_put_contents($path, $dumper->dump()); + $matcher = new RedirectableStaticUrlMatcher(require $path, $context); + unlink($path); + + return $matcher; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableUrlMatcherTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableUrlMatcherTest.php index 438ca2538df8e..c84e3b734e6f0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableUrlMatcherTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableUrlMatcherTest.php @@ -17,6 +17,9 @@ use Symfony\Bundle\FrameworkBundle\Routing\RedirectableUrlMatcher; use Symfony\Component\Routing\RequestContext; +/** + * @group legacy + */ class RedirectableUrlMatcherTest extends TestCase { public function testRedirectWhenNoSlash() diff --git a/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php b/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php index 60bdf1da3522c..91eb89e55cdb7 100644 --- a/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php +++ b/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php @@ -11,11 +11,15 @@ namespace Symfony\Component\Routing\Generator\Dumper; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1 and will be removed in 5.0. Use StaticUrlGeneratorDumper instead.', PhpGeneratorDumper::class), E_USER_DEPRECATED); + /** * PhpGeneratorDumper creates a PHP class able to generate URLs for a given set of routes. * * @author Fabien Potencier * @author Tobias Schultze + * + * @deprecated since Symfony 4.1, to be removed in 5.0. Use StaticUrlGeneratorDumper instead. */ class PhpGeneratorDumper extends GeneratorDumper { diff --git a/src/Symfony/Component/Routing/Generator/Dumper/StaticUrlGeneratorDumper.php b/src/Symfony/Component/Routing/Generator/Dumper/StaticUrlGeneratorDumper.php new file mode 100644 index 0000000000000..44c6f6ebb1ed9 --- /dev/null +++ b/src/Symfony/Component/Routing/Generator/Dumper/StaticUrlGeneratorDumper.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Generator\Dumper; + +use Symfony\Component\Routing\Matcher\Dumper\StaticUrlMatcherDumper; + +/** + * StaticUrlGeneratorDumper creates a PHP array to be used with StaticUrlGenerator. + * + * @author Fabien Potencier + * @author Tobias Schultze + * @author Nicolas Grekas + */ +class StaticUrlGeneratorDumper extends GeneratorDumper +{ + /** + * {@inheritdoc} + */ + public function dump(array $options = array()) + { + return <<generateDeclaredRoutes()} +); + +EOF; + } + + /** + * Generates PHP code representing an array of defined routes + * together with the routes properties (e.g. requirements). + */ + private function generateDeclaredRoutes(): string + { + $routes = ''; + foreach ($this->getRoutes()->all() as $name => $route) { + $compiledRoute = $route->compile(); + + $properties = array(); + $properties[] = $compiledRoute->getVariables(); + $properties[] = $route->getDefaults(); + $properties[] = $route->getRequirements(); + $properties[] = $compiledRoute->getTokens(); + $properties[] = $compiledRoute->getHostTokens(); + $properties[] = $route->getSchemes(); + + $routes .= sprintf("\n '%s' => %s,", $name, StaticUrlMatcherDumper::export($properties)); + } + + return $routes; + } +} diff --git a/src/Symfony/Component/Routing/Generator/StaticUrlGenerator.php b/src/Symfony/Component/Routing/Generator/StaticUrlGenerator.php new file mode 100644 index 0000000000000..7367898da8838 --- /dev/null +++ b/src/Symfony/Component/Routing/Generator/StaticUrlGenerator.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Generator; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Symfony\Component\Routing\RequestContext; + +class StaticUrlGenerator extends UrlGenerator +{ + private $dumpedRoutes = array(); + + public function __construct(array $dumpedRoutes, RequestContext $context, LoggerInterface $logger = null) + { + $this->dumpedRoutes = $dumpedRoutes; + $this->context = $context; + $this->logger = $logger; + } + + public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH) + { + if (!isset($this->dumpedRoutes[$name])) { + throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $name)); + } + + list($variables, $defaults, $requirements, $tokens, $hostTokens, $requiredSchemes) = $this->dumpedRoutes[$name]; + + return $this->doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, $requiredSchemes); + } +} diff --git a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php index acb9eddb34de8..cb89ae5eaa6a5 100644 --- a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php +++ b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Routing\Matcher\Dumper; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1 and will be removed in 5.0. Use StaticUrlMatcherDumper instead.', PhpMatcherDumper::class), E_USER_DEPRECATED); + use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; @@ -22,6 +24,8 @@ * @author Fabien Potencier * @author Tobias Schultze * @author Arnaud Le Blanc + * + * @deprecated since Symfony 4.1, to be removed in 5.0. Use StaticUrlMatcherDumper instead. */ class PhpMatcherDumper extends MatcherDumper { diff --git a/src/Symfony/Component/Routing/Matcher/Dumper/StaticUrlMatcherDumper.php b/src/Symfony/Component/Routing/Matcher/Dumper/StaticUrlMatcherDumper.php new file mode 100644 index 0000000000000..1dfd08645c086 --- /dev/null +++ b/src/Symfony/Component/Routing/Matcher/Dumper/StaticUrlMatcherDumper.php @@ -0,0 +1,296 @@ + + * + * 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; +use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; + +/** + * StaticUrlMatcherDumper creates a PHP array to be used with StaticUrlMatcher. + * + * @author Fabien Potencier + * @author Tobias Schultze + * @author Arnaud Le Blanc + * @author Nicolas Grekas + */ +class StaticUrlMatcherDumper extends MatcherDumper +{ + private $expressionLanguage; + private $expressions = array(); + private $expressionsIndex = array(); + + /** + * @var ExpressionFunctionProviderInterface[] + */ + private $expressionLanguageProviders = array(); + + /** + * {@inheritdoc} + */ + public function dump(array $options = array()) + { + list($routes, $expressions) = $this->compileRoutes($this->getRoutes()); + + return <<exportRoutes($routes)}, + {$this->exportExpressions($expressions)}, +); + +EOF; + } + + public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider) + { + $this->expressionLanguageProviders[] = $provider; + } + + private function compileRoutes(RouteCollection $routes): array + { + try { + $groups = $this->groupRoutesByHostRegex($routes); + $dumpedRoutes = array(); + + foreach ($groups as $collection) { + $tree = $this->buildStaticPrefixCollection($collection); + if (!$tree->getItems()) { + continue; + } + $dumpedRoutes[] = array( + $collection->getAttribute('host_regex'), + $this->compileStaticPrefixRoutes($tree), + ); + } + + return array($dumpedRoutes, $this->expressions); + } finally { + $this->expressions = $this->expressionsIndex = array(); + } + } + + private function buildStaticPrefixCollection(DumperCollection $collection): StaticPrefixCollection + { + $prefixCollection = new StaticPrefixCollection(); + + foreach ($collection as $dumperRoute) { + $prefix = $dumperRoute->getRoute()->compile()->getStaticPrefix(); + $prefixCollection->addRoute($prefix, $dumperRoute); + } + + $prefixCollection->optimizeGroups(); + + return $prefixCollection; + } + + private function compileStaticPrefixRoutes(StaticPrefixCollection $collection): array + { + $prefix = $collection->getPrefix(); + $dumpedRoutes = array( + !empty($prefix) && '/' !== $prefix ? $prefix : null, + ); + + foreach ($collection->getItems() as $route) { + if (!$route instanceof StaticPrefixCollection) { + $dumpedRoutes[] = $this->compileRoute($route[1]->getRoute(), $route[1]->getName(), $prefix); + } elseif ($route->getItems()) { + $dumpedRoutes[] = $this->compileStaticPrefixRoutes($route); + } + } + + return $dumpedRoutes; + } + + /** + * Compiles a single Route to PHP code used to match it against the path info. + */ + private function compileRoute(Route $route, string $name, string $parentPrefix = null): array + { + $dumpedRoute = array(); + $compiledRoute = $route->compile(); + $conditions = array(); + $methods = array_values($route->getMethods()); + $schemes = array_values($route->getSchemes()); + + $supportsTrailingSlash = !$methods || in_array('HEAD', $methods) || in_array('GET', $methods); + $regex = $compiledRoute->getRegex(); + + if (!count($compiledRoute->getPathVariables()) && false !== preg_match('#^(.)\^(?P.*?)\$\1#'.('u' === $regex[-1] ? 'u' : ''), $regex, $m)) { + if ($supportsTrailingSlash && '/' === $m['url'][-1]) { + $conditions[] = 'trim'; + $conditions[] = rtrim(str_replace('\\', '', $m['url']), '/'); + } else { + $conditions[] = 'path'; + $conditions[] = str_replace('\\', '', $m['url']); + } + } else { + if ($start = $compiledRoute->getStaticPrefix() && $compiledRoute->getStaticPrefix() !== $parentPrefix) { + $conditions[] = 'start'; + } else { + $conditions[] = 'match'; + } + + $conditions[] = ($supportsTrailingSlash && $pos = strpos($regex, '/$')) ? substr_replace($regex, '(?P<>/?)$', $pos, 2) : $regex; + + if ($start) { + $conditions[] = $compiledRoute->getStaticPrefix(); + } + } + + if ($route->getCondition()) { + $expression = $this->getExpressionLanguage()->compile($route->getCondition(), array('context', 'request')); + + if (isset($this->expressionsIndex[$expression])) { + $expr = $this->expressionsIndex[$expression]; + } else { + $this->expressionsIndex[$expression] = $expr = count($this->expressionsIndex); + $this->expressions[] = $expression; + } + } else { + $expr = null; + } + + $dumpedRoute[] = $conditions; + $dumpedRoute[] = $expr; + $dumpedRoute[] = array_combine($methods, $methods) ?: null; + $dumpedRoute[] = array_combine($schemes, $schemes) ?: null; + $dumpedRoute[] = array_replace($route->getDefaults(), array('_route' => $name)); + + return $dumpedRoute; + } + + /** + * Groups consecutive routes having the same host regex. + * + * The result is a collection of collections of routes having the same host regex. + */ + private function groupRoutesByHostRegex(RouteCollection $routes): DumperCollection + { + $groups = new DumperCollection(); + $currentGroup = new DumperCollection(); + $currentGroup->setAttribute('host_regex', null); + $groups->add($currentGroup); + + foreach ($routes as $name => $route) { + $hostRegex = $route->compile()->getHostRegex(); + if ($currentGroup->getAttribute('host_regex') !== $hostRegex) { + $currentGroup = new DumperCollection(); + $currentGroup->setAttribute('host_regex', $hostRegex); + $groups->add($currentGroup); + } + $currentGroup->add(new DumperRoute($name, $route)); + } + + return $groups; + } + + private function getExpressionLanguage(): ExpressionLanguage + { + if (null === $this->expressionLanguage) { + if (!class_exists(ExpressionLanguage::class)) { + throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); + } + $this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders); + } + + return $this->expressionLanguage; + } + + private function exportRoutes(array $routes): string + { + if (!$routes) { + return 'array()'; + } + + $code = ''; + foreach ($routes as list($hostRegex, $routeCollection)) { + $code .= <<export($hostRegex)}, + {$this->exportCollection($routeCollection)}, + ), + +EOF; + } + + return "array(\n$code )"; + } + + private function exportCollection(array $routes, int $indent = 1): string + { + $pad = ' '.str_repeat(' ', $indent); + $code = "array(\n$pad{$this->export($routes[0])},\n"; + for ($i = 1; $i < \count($routes); ++$i) { + if (!\is_array($routes[$i][0])) { + $code .= $pad.$this->exportCollection($routes[$i], 1 + $indent).",\n"; + } else { + $code .= $pad.$this->export($routes[$i]).",\n"; + } + } + + return $code.substr_replace($pad, ')', -4); + } + + private function exportExpressions(array $expressions): string + { + if (!$expressions) { + return 'array()'; + } + + $code = ''; + foreach ($expressions as $expression) { + $code .= " function (\$context, \$request) { return $expression; },\n"; + } + + return "array(\n$code )"; + } + + /** + * @internal + */ + public static function export($value): string + { + if (null === $value) { + return 'null'; + } + if (!\is_array($value)) { + return var_export($value, true); + } + if (!$value) { + return 'array()'; + } + + $i = 0; + $export = 'array('; + + foreach ($value as $k => $v) { + if ($i === $k) { + ++$i; + } else { + $export .= var_export($k, true).' => '; + + 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/StaticUrlMatcher.php b/src/Symfony/Component/Routing/Matcher/StaticUrlMatcher.php new file mode 100644 index 0000000000000..3a81ed0e3dd34 --- /dev/null +++ b/src/Symfony/Component/Routing/Matcher/StaticUrlMatcher.php @@ -0,0 +1,160 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher; + +use Symfony\Component\Routing\Exception\MethodNotAllowedException; +use Symfony\Component\Routing\Exception\NoConfigurationException; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Routing\RequestContext; + +/** + * StaticUrlMatcher matches URL based on a set of rules dumped by StaticUrlMatcherDumper. + * + * @author Nicolas Grekas + */ +class StaticUrlMatcher extends UrlMatcher +{ + private $dumpedRoutes = array(); + + public function __construct(array $dumpedRoutes, RequestContext $context) + { + $this->dumpedRoutes = $dumpedRoutes; + $this->context = $context; + } + + /** + * {@inheritdoc} + */ + public function match($rawPathinfo) + { + $allow = array(); + $pathinfo = rawurldecode($rawPathinfo); + $trimmedPathinfo = rtrim($pathinfo, '/'); + $context = $this->context; + $request = $this->request ?: $this->createRequest($pathinfo); + $requestMethod = $context->getMethod(); + $host = $context->getHost(); + $canRedirect = $this instanceof RedirectableUrlMatcherInterface; + + foreach ($this->dumpedRoutes[0] as list($hostRegex, $routes)) { + if (null === $hostRegex) { + $hostMatches = array(); + } elseif (!preg_match($hostRegex, $host, $hostMatches)) { + continue; + } + + $i = 1; + $count = \count($routes); + $nextRoutes = array(); + + while (true) { + for (; $i < $count; ++$i) { + if (!\is_array($conditions = $routes[$i][0])) { + // nested collection + + if (null !== $conditions && 0 !== strpos($pathinfo, $conditions)) { + continue; + } + + if ($i < $count - 1) { + $nextRoutes[] = array(1 + $i, $routes); + } + + $routes = $routes[$i]; + $count = \count($routes); + $i = 0; + } else { + // single route + + list(, $expr, $methods, $schemes, $defaults) = $routes[$i]; + $matches = array(); + + switch ($conditions[0]) { + case 'trim': + if ($canRedirect) { + if ($conditions[1] !== $trimmedPathinfo) { + continue 2; + } + $redirectTrailingSlash = '/' !== $pathinfo[-1]; + break; + } + $conditions[1] .= '/'; + // no break + case 'path': + if ($conditions[1] !== $pathinfo) { + continue 2; + } + $redirectTrailingSlash = false; + break; + case 'start': + if (0 !== strpos($pathinfo, $conditions[2])) { + continue 2; + } + // no break + case 'match': + if (!preg_match($conditions[1], $pathinfo, $matches)) { + continue 2; + } + if ($redirectTrailingSlash = isset($matches[''])) { + $redirectTrailingSlash = $canRedirect && '/' !== $matches['']; + unset($matches['']); + } + break; + } + + if (null !== $expr && !$this->dumpedRoutes[1][$expr]($context, $request)) { + continue; + } + + if ($methods && !isset($methods[$requestMethod]) && ('HEAD' !== $requestMethod || !isset($methods['GET']))) { + $allow += $methods; + continue; + } + + if ($matches || $hostMatches) { + $ret = $this->mergeDefaults(array_replace($hostMatches, $matches), $defaults); + $ret['_route'] = $defaults['_route']; + } else { + $ret = $defaults; + } + + if ($redirectTrailingSlash) { + return array_replace($ret, $this->redirect($rawPathinfo.'/', $defaults['_route'])); + } + + if ($schemes && !isset($schemes[$context->getScheme()])) { + if ($canRedirect) { + return array_replace($ret, $this->redirect($rawPathinfo, $defaults['_route'], key($schemes))); + } + continue; + } + + return $ret; + } + } + + if (!$nextRoutes) { + break; + } + + list($i, $routes) = array_pop($nextRoutes); + $count = \count($routes); + } + } + + if (!$this->dumpedRoutes[0] && '/' === $pathinfo) { + throw new NoConfigurationException(); + } + + throw 0 < count($allow) ? new MethodNotAllowedException(array_values($allow)) : new ResourceNotFoundException(); + } +} diff --git a/src/Symfony/Component/Routing/Router.php b/src/Symfony/Component/Routing/Router.php index 0de921853f875..8e78c3b5ac94e 100644 --- a/src/Symfony/Component/Routing/Router.php +++ b/src/Symfony/Component/Routing/Router.php @@ -17,9 +17,11 @@ use Symfony\Component\Config\ConfigCacheFactory; use Psr\Log\LoggerInterface; use Symfony\Component\Routing\Generator\ConfigurableRequirementsInterface; +use Symfony\Component\Routing\Generator\StaticUrlGenerator; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\Dumper\GeneratorDumperInterface; use Symfony\Component\Routing\Matcher\RequestMatcherInterface; +use Symfony\Component\Routing\Matcher\StaticUrlMatcher; use Symfony\Component\Routing\Matcher\UrlMatcherInterface; use Symfony\Component\Routing\Matcher\Dumper\MatcherDumperInterface; use Symfony\Component\HttpFoundation\Request; @@ -83,6 +85,8 @@ class Router implements RouterInterface, RequestMatcherInterface */ private $expressionLanguageProviders = array(); + private static $dumpCache = array(); + /** * @param LoaderInterface $loader A LoaderInterface instance * @param mixed $resource The main resource to load @@ -273,8 +277,10 @@ public function getMatcher() return $this->matcher; } + $useStatic = is_subclass_of($this->options['matcher_class'], StaticUrlMatcher::class); + if (null === $this->options['cache_dir'] || null === $this->options['matcher_cache_class']) { - $this->matcher = new $this->options['matcher_class']($this->getRouteCollection(), $this->context); + $this->matcher = new $this->options[$useStatic ? 'matcher_base_class' : 'matcher_class']($this->getRouteCollection(), $this->context); if (method_exists($this->matcher, 'addExpressionLanguageProvider')) { foreach ($this->expressionLanguageProviders as $provider) { $this->matcher->addExpressionLanguageProvider($provider); @@ -302,6 +308,14 @@ function (ConfigCacheInterface $cache) { } ); + if ($useStatic) { + if (!isset(self::$dumpCache[$path = $cache->getPath()])) { + self::$dumpCache[$path] = require $path; + } + + return $this->matcher = new $this->options['matcher_class'](self::$dumpCache[$path], $this->context); + } + require_once $cache->getPath(); return $this->matcher = new $this->options['matcher_cache_class']($this->context); @@ -318,8 +332,10 @@ public function getGenerator() return $this->generator; } + $useStatic = is_a($this->options['generator_class'], StaticUrlGenerator::class, true); + if (null === $this->options['cache_dir'] || null === $this->options['generator_cache_class']) { - $this->generator = new $this->options['generator_class']($this->getRouteCollection(), $this->context, $this->logger); + $this->generator = new $this->options[$useStatic ? 'generator_base_class' : 'generator_class']($this->getRouteCollection(), $this->context, $this->logger); } else { $cache = $this->getConfigCacheFactory()->cache($this->options['cache_dir'].'/'.$this->options['generator_cache_class'].'.php', function (ConfigCacheInterface $cache) { @@ -334,9 +350,17 @@ function (ConfigCacheInterface $cache) { } ); - require_once $cache->getPath(); + if ($useStatic) { + if (!isset(self::$dumpCache[$path = $cache->getPath()])) { + self::$dumpCache[$path] = require $path; + } - $this->generator = new $this->options['generator_cache_class']($this->context, $this->logger); + $this->generator = new $this->options['generator_class'](self::$dumpCache[$path], $this->context, $this->logger); + } else { + require_once $cache->getPath(); + + $this->generator = new $this->options['generator_cache_class']($this->context, $this->logger); + } } if ($this->generator instanceof ConfigurableRequirementsInterface) { diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher0.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher0.php new file mode 100644 index 0000000000000..f81ee0847a9e8 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher0.php @@ -0,0 +1,8 @@ +baz|symfony)$#s'), null, null, null, array('def' => 'test', '_route' => 'foo')), + array(array('path', '/foofoo'), null, null, null, array('def' => 'test', '_route' => 'foofoo')), + ), + array( + '/bar', + array(array('match', '#^/bar/(?P[^/]++)$#s'), null, array('GET' => 'GET', 'HEAD' => 'HEAD'), null, array('_route' => 'bar')), + array(array('start', '#^/barhead/(?P[^/]++)$#s', '/barhead'), null, array('GET' => 'GET'), null, array('_route' => 'barhead')), + ), + array( + '/test', + array( + '/test/baz', + array(array('path', '/test/baz'), null, null, null, array('_route' => 'baz')), + array(array('path', '/test/baz.html'), null, null, null, array('_route' => 'baz2')), + array(array('trim', '/test/baz3'), null, null, null, array('_route' => 'baz3')), + ), + array(array('match', '#^/test/(?P[^/]++)(?P<>/?)$#s'), null, null, null, array('_route' => 'baz4')), + array(array('match', '#^/test/(?P[^/]++)/$#s'), null, array('POST' => 'POST'), null, array('_route' => 'baz5')), + array(array('match', '#^/test/(?P[^/]++)/$#s'), null, array('PUT' => 'PUT'), null, array('_route' => 'baz.baz6')), + ), + array(array('match', '#^/(?P[\']+)$#s'), null, null, null, array('_route' => 'quoter')), + array(array('path', '/spa ce'), null, null, null, array('_route' => 'space')), + array( + '/a', + array( + '/a/b\'b', + array(array('match', '#^/a/b\'b/(?P[^/]++)$#s'), null, null, null, array('_route' => 'foo1')), + array(array('match', '#^/a/b\'b/(?P[^/]++)$#s'), null, null, null, array('_route' => 'bar1')), + ), + array(array('match', '#^/a/(?P.*)$#s'), null, null, null, array('_route' => 'overridden')), + array( + '/a/b\'b', + array(array('match', '#^/a/b\'b/(?P[^/]++)$#s'), null, null, null, array('_route' => 'foo2')), + array(array('match', '#^/a/b\'b/(?P[^/]++)$#s'), null, null, null, array('_route' => 'bar2')), + ), + ), + array( + '/multi', + array(array('start', '#^/multi/hello(?:/(?P[^/]++))?$#s', '/multi/hello'), null, null, null, array('who' => 'World!', '_route' => 'helloWorld')), + array(array('trim', '/multi/hey'), null, null, null, array('_route' => 'hey')), + array(array('path', '/multi/new'), null, null, null, array('_route' => 'overridden2')), + ), + array(array('match', '#^/(?P<_locale>[^/]++)/b/(?P[^/]++)$#s'), null, null, null, array('_route' => 'foo3')), + array(array('match', '#^/(?P<_locale>[^/]++)/b/(?P[^/]++)$#s'), null, null, null, array('_route' => 'bar3')), + array( + '/aba', + array(array('path', '/ababa'), null, null, null, array('_route' => 'ababa')), + array(array('match', '#^/aba/(?P[^/]++)$#s'), null, null, null, array('_route' => 'foo4')), + ), + ), + ), + array( + '#^a\\.example\\.com$#si', + array( + null, + array(array('path', '/route1'), null, null, null, array('_route' => 'route1')), + array(array('path', '/c2/route2'), null, null, null, array('_route' => 'route2')), + ), + ), + array( + '#^b\\.example\\.com$#si', + array( + null, + array(array('path', '/c2/route3'), null, null, null, array('_route' => 'route3')), + ), + ), + array( + '#^a\\.example\\.com$#si', + array( + null, + array(array('path', '/route4'), null, null, null, array('_route' => 'route4')), + ), + ), + array( + '#^c\\.example\\.com$#si', + array( + null, + array(array('path', '/route5'), null, null, null, array('_route' => 'route5')), + ), + ), + array( + null, + array( + null, + array(array('path', '/route6'), null, null, null, array('_route' => 'route6')), + ), + ), + array( + '#^(?P[^\\.]++)\\.example\\.com$#si', + array( + null, + array( + '/route1', + array(array('path', '/route11'), null, null, null, array('_route' => 'route11')), + array(array('path', '/route12'), null, null, null, array('var1' => 'val', '_route' => 'route12')), + array(array('start', '#^/route13/(?P[^/]++)$#s', '/route13'), null, null, null, array('_route' => 'route13')), + array(array('start', '#^/route14/(?P[^/]++)$#s', '/route14'), null, null, null, array('var1' => 'val', '_route' => 'route14')), + ), + ), + ), + array( + '#^c\\.example\\.com$#si', + array( + null, + array(array('start', '#^/route15/(?P[^/]++)$#s', '/route15'), null, null, null, array('_route' => 'route15')), + ), + ), + array( + null, + array( + null, + array(array('start', '#^/route16/(?P[^/]++)$#s', '/route16'), null, null, null, array('var1' => 'val', '_route' => 'route16')), + array(array('path', '/route17'), null, null, null, array('_route' => 'route17')), + array(array('path', '/a/a...'), null, null, null, array('_route' => 'a')), + array( + '/a/b', + array(array('match', '#^/a/b/(?P[^/]++)$#s'), null, null, null, array('_route' => 'b')), + array(array('start', '#^/a/b/c/(?P[^/]++)$#s', '/a/b/c'), null, null, null, array('_route' => 'c')), + ), + ), + ), + ), + array(), +); diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher2.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher2.php new file mode 100644 index 0000000000000..a80ba85c7ecb6 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher2.php @@ -0,0 +1,138 @@ +baz|symfony)$#s'), null, null, null, array('def' => 'test', '_route' => 'foo')), + array(array('path', '/foofoo'), null, null, null, array('def' => 'test', '_route' => 'foofoo')), + ), + array( + '/bar', + array(array('match', '#^/bar/(?P[^/]++)$#s'), null, array('GET' => 'GET', 'HEAD' => 'HEAD'), null, array('_route' => 'bar')), + array(array('start', '#^/barhead/(?P[^/]++)$#s', '/barhead'), null, array('GET' => 'GET'), null, array('_route' => 'barhead')), + ), + array( + '/test', + array( + '/test/baz', + array(array('path', '/test/baz'), null, null, null, array('_route' => 'baz')), + array(array('path', '/test/baz.html'), null, null, null, array('_route' => 'baz2')), + array(array('trim', '/test/baz3'), null, null, null, array('_route' => 'baz3')), + ), + array(array('match', '#^/test/(?P[^/]++)(?P<>/?)$#s'), null, null, null, array('_route' => 'baz4')), + array(array('match', '#^/test/(?P[^/]++)/$#s'), null, array('POST' => 'POST'), null, array('_route' => 'baz5')), + array(array('match', '#^/test/(?P[^/]++)/$#s'), null, array('PUT' => 'PUT'), null, array('_route' => 'baz.baz6')), + ), + array(array('match', '#^/(?P[\']+)$#s'), null, null, null, array('_route' => 'quoter')), + array(array('path', '/spa ce'), null, null, null, array('_route' => 'space')), + array( + '/a', + array( + '/a/b\'b', + array(array('match', '#^/a/b\'b/(?P[^/]++)$#s'), null, null, null, array('_route' => 'foo1')), + array(array('match', '#^/a/b\'b/(?P[^/]++)$#s'), null, null, null, array('_route' => 'bar1')), + ), + array(array('match', '#^/a/(?P.*)$#s'), null, null, null, array('_route' => 'overridden')), + array( + '/a/b\'b', + array(array('match', '#^/a/b\'b/(?P[^/]++)$#s'), null, null, null, array('_route' => 'foo2')), + array(array('match', '#^/a/b\'b/(?P[^/]++)$#s'), null, null, null, array('_route' => 'bar2')), + ), + ), + array( + '/multi', + array(array('start', '#^/multi/hello(?:/(?P[^/]++))?$#s', '/multi/hello'), null, null, null, array('who' => 'World!', '_route' => 'helloWorld')), + array(array('trim', '/multi/hey'), null, null, null, array('_route' => 'hey')), + array(array('path', '/multi/new'), null, null, null, array('_route' => 'overridden2')), + ), + array(array('match', '#^/(?P<_locale>[^/]++)/b/(?P[^/]++)$#s'), null, null, null, array('_route' => 'foo3')), + array(array('match', '#^/(?P<_locale>[^/]++)/b/(?P[^/]++)$#s'), null, null, null, array('_route' => 'bar3')), + array( + '/aba', + array(array('path', '/ababa'), null, null, null, array('_route' => 'ababa')), + array(array('match', '#^/aba/(?P[^/]++)$#s'), null, null, null, array('_route' => 'foo4')), + ), + ), + ), + array( + '#^a\\.example\\.com$#si', + array( + null, + array(array('path', '/route1'), null, null, null, array('_route' => 'route1')), + array(array('path', '/c2/route2'), null, null, null, array('_route' => 'route2')), + ), + ), + array( + '#^b\\.example\\.com$#si', + array( + null, + array(array('path', '/c2/route3'), null, null, null, array('_route' => 'route3')), + ), + ), + array( + '#^a\\.example\\.com$#si', + array( + null, + array(array('path', '/route4'), null, null, null, array('_route' => 'route4')), + ), + ), + array( + '#^c\\.example\\.com$#si', + array( + null, + array(array('path', '/route5'), null, null, null, array('_route' => 'route5')), + ), + ), + array( + null, + array( + null, + array(array('path', '/route6'), null, null, null, array('_route' => 'route6')), + ), + ), + array( + '#^(?P[^\\.]++)\\.example\\.com$#si', + array( + null, + array( + '/route1', + array(array('path', '/route11'), null, null, null, array('_route' => 'route11')), + array(array('path', '/route12'), null, null, null, array('var1' => 'val', '_route' => 'route12')), + array(array('start', '#^/route13/(?P[^/]++)$#s', '/route13'), null, null, null, array('_route' => 'route13')), + array(array('start', '#^/route14/(?P[^/]++)$#s', '/route14'), null, null, null, array('var1' => 'val', '_route' => 'route14')), + ), + ), + ), + array( + '#^c\\.example\\.com$#si', + array( + null, + array(array('start', '#^/route15/(?P[^/]++)$#s', '/route15'), null, null, null, array('_route' => 'route15')), + ), + ), + array( + null, + array( + null, + array(array('start', '#^/route16/(?P[^/]++)$#s', '/route16'), null, null, null, array('var1' => 'val', '_route' => 'route16')), + array(array('path', '/route17'), null, null, null, array('_route' => 'route17')), + array(array('path', '/a/a...'), null, null, null, array('_route' => 'a')), + array( + '/a/b', + array(array('match', '#^/a/b/(?P[^/]++)$#s'), null, null, null, array('_route' => 'b')), + array(array('start', '#^/a/b/c/(?P[^/]++)$#s', '/a/b/c'), null, null, null, array('_route' => 'c')), + ), + array(array('path', '/secure'), null, null, array('https' => 'https'), array('_route' => 'secure')), + array(array('path', '/nonsecure'), null, null, array('http' => 'http'), array('_route' => 'nonsecure')), + ), + ), + ), + array(), +); diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher3.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher3.php new file mode 100644 index 0000000000000..37e658f87d439 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher3.php @@ -0,0 +1,23 @@ + 'static')), + array(array('match', '#^/rootprefix/(?P[^/]++)$#s'), null, null, null, array('_route' => 'dynamic')), + ), + array(array('path', '/with-condition'), 0, null, null, array('_route' => 'with-condition')), + ), + ), + ), + array( + function ($context, $request) { return ($context->getMethod() == "GET"); }, + ), +); diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher4.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher4.php new file mode 100644 index 0000000000000..37cb69f0df45e --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher4.php @@ -0,0 +1,24 @@ + 'HEAD'), null, array('_route' => 'just_head')), + array(array('path', '/head_and_get'), null, array('HEAD' => 'HEAD', 'GET' => 'GET'), null, array('_route' => 'head_and_get')), + array(array('path', '/get_and_head'), null, array('GET' => 'GET', 'HEAD' => 'HEAD'), null, array('_route' => 'get_and_head')), + array(array('path', '/post_and_get'), null, array('POST' => 'POST', 'HEAD' => 'HEAD'), null, array('_route' => 'post_and_head')), + array( + '/put_and_post', + array(array('path', '/put_and_post'), null, array('PUT' => 'PUT', 'POST' => 'POST'), null, array('_route' => 'put_and_post')), + array(array('path', '/put_and_post'), null, array('PUT' => 'PUT', 'GET' => 'GET', 'HEAD' => 'HEAD'), null, array('_route' => 'put_and_get_and_head')), + ), + ), + ), + ), + array(), +); diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher5.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher5.php new file mode 100644 index 0000000000000..8bf38e631faa4 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher5.php @@ -0,0 +1,41 @@ + 'a_first')), + array(array('path', '/a/22'), null, null, null, array('_route' => 'a_second')), + array(array('path', '/a/333'), null, null, null, array('_route' => 'a_third')), + ), + array(array('match', '#^/(?P[^/]++)$#s'), null, null, null, array('_route' => 'a_wildcard')), + array( + '/a', + array(array('trim', '/a/44'), null, null, null, array('_route' => 'a_fourth')), + array(array('trim', '/a/55'), null, null, null, array('_route' => 'a_fifth')), + array(array('trim', '/a/66'), null, null, null, array('_route' => 'a_sixth')), + ), + array(array('start', '#^/nested/(?P[^/]++)$#s', '/nested'), null, null, null, array('_route' => 'nested_wildcard')), + array( + '/nested/group', + array(array('trim', '/nested/group/a'), null, null, null, array('_route' => 'nested_a')), + array(array('trim', '/nested/group/b'), null, null, null, array('_route' => 'nested_b')), + array(array('trim', '/nested/group/c'), null, null, null, array('_route' => 'nested_c')), + ), + array( + '/slashed/group', + array(array('trim', '/slashed/group'), null, null, null, array('_route' => 'slashed_a')), + array(array('trim', '/slashed/group/b'), null, null, null, array('_route' => 'slashed_b')), + array(array('trim', '/slashed/group/c'), null, null, null, array('_route' => 'slashed_c')), + ), + ), + ), + ), + array(), +); diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher6.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher6.php new file mode 100644 index 0000000000000..51dcf4a8cd644 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher6.php @@ -0,0 +1,43 @@ + 'simple_trailing_slash_no_methods')), + array(array('trim', '/trailing/simple/get-method'), null, array('GET' => 'GET'), null, array('_route' => 'simple_trailing_slash_GET_method')), + array(array('trim', '/trailing/simple/head-method'), null, array('HEAD' => 'HEAD'), null, array('_route' => 'simple_trailing_slash_HEAD_method')), + array(array('path', '/trailing/simple/post-method/'), null, array('POST' => 'POST'), null, array('_route' => 'simple_trailing_slash_POST_method')), + ), + array( + '/trailing/regex', + array(array('start', '#^/trailing/regex/no\\-methods/(?P[^/]++)(?P<>/?)$#s', '/trailing/regex/no-methods'), null, null, null, array('_route' => 'regex_trailing_slash_no_methods')), + array(array('start', '#^/trailing/regex/get\\-method/(?P[^/]++)(?P<>/?)$#s', '/trailing/regex/get-method'), null, array('GET' => 'GET'), null, array('_route' => 'regex_trailing_slash_GET_method')), + array(array('start', '#^/trailing/regex/head\\-method/(?P[^/]++)(?P<>/?)$#s', '/trailing/regex/head-method'), null, array('HEAD' => 'HEAD'), null, array('_route' => 'regex_trailing_slash_HEAD_method')), + array(array('start', '#^/trailing/regex/post\\-method/(?P[^/]++)/$#s', '/trailing/regex/post-method'), null, array('POST' => 'POST'), null, array('_route' => 'regex_trailing_slash_POST_method')), + ), + array( + '/not-trailing/simple', + array(array('path', '/not-trailing/simple/no-methods'), null, null, null, array('_route' => 'simple_not_trailing_slash_no_methods')), + array(array('path', '/not-trailing/simple/get-method'), null, array('GET' => 'GET'), null, array('_route' => 'simple_not_trailing_slash_GET_method')), + array(array('path', '/not-trailing/simple/head-method'), null, array('HEAD' => 'HEAD'), null, array('_route' => 'simple_not_trailing_slash_HEAD_method')), + array(array('path', '/not-trailing/simple/post-method'), null, array('POST' => 'POST'), null, array('_route' => 'simple_not_trailing_slash_POST_method')), + ), + array( + '/not-trailing/regex', + array(array('start', '#^/not\\-trailing/regex/no\\-methods/(?P[^/]++)$#s', '/not-trailing/regex/no-methods'), null, null, null, array('_route' => 'regex_not_trailing_slash_no_methods')), + array(array('start', '#^/not\\-trailing/regex/get\\-method/(?P[^/]++)$#s', '/not-trailing/regex/get-method'), null, array('GET' => 'GET'), null, array('_route' => 'regex_not_trailing_slash_GET_method')), + array(array('start', '#^/not\\-trailing/regex/head\\-method/(?P[^/]++)$#s', '/not-trailing/regex/head-method'), null, array('HEAD' => 'HEAD'), null, array('_route' => 'regex_not_trailing_slash_HEAD_method')), + array(array('start', '#^/not\\-trailing/regex/post\\-method/(?P[^/]++)$#s', '/not-trailing/regex/post-method'), null, array('POST' => 'POST'), null, array('_route' => 'regex_not_trailing_slash_POST_method')), + ), + ), + ), + ), + array(), +); diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher7.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher7.php new file mode 100644 index 0000000000000..51dcf4a8cd644 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/static_url_matcher7.php @@ -0,0 +1,43 @@ + 'simple_trailing_slash_no_methods')), + array(array('trim', '/trailing/simple/get-method'), null, array('GET' => 'GET'), null, array('_route' => 'simple_trailing_slash_GET_method')), + array(array('trim', '/trailing/simple/head-method'), null, array('HEAD' => 'HEAD'), null, array('_route' => 'simple_trailing_slash_HEAD_method')), + array(array('path', '/trailing/simple/post-method/'), null, array('POST' => 'POST'), null, array('_route' => 'simple_trailing_slash_POST_method')), + ), + array( + '/trailing/regex', + array(array('start', '#^/trailing/regex/no\\-methods/(?P[^/]++)(?P<>/?)$#s', '/trailing/regex/no-methods'), null, null, null, array('_route' => 'regex_trailing_slash_no_methods')), + array(array('start', '#^/trailing/regex/get\\-method/(?P[^/]++)(?P<>/?)$#s', '/trailing/regex/get-method'), null, array('GET' => 'GET'), null, array('_route' => 'regex_trailing_slash_GET_method')), + array(array('start', '#^/trailing/regex/head\\-method/(?P[^/]++)(?P<>/?)$#s', '/trailing/regex/head-method'), null, array('HEAD' => 'HEAD'), null, array('_route' => 'regex_trailing_slash_HEAD_method')), + array(array('start', '#^/trailing/regex/post\\-method/(?P[^/]++)/$#s', '/trailing/regex/post-method'), null, array('POST' => 'POST'), null, array('_route' => 'regex_trailing_slash_POST_method')), + ), + array( + '/not-trailing/simple', + array(array('path', '/not-trailing/simple/no-methods'), null, null, null, array('_route' => 'simple_not_trailing_slash_no_methods')), + array(array('path', '/not-trailing/simple/get-method'), null, array('GET' => 'GET'), null, array('_route' => 'simple_not_trailing_slash_GET_method')), + array(array('path', '/not-trailing/simple/head-method'), null, array('HEAD' => 'HEAD'), null, array('_route' => 'simple_not_trailing_slash_HEAD_method')), + array(array('path', '/not-trailing/simple/post-method'), null, array('POST' => 'POST'), null, array('_route' => 'simple_not_trailing_slash_POST_method')), + ), + array( + '/not-trailing/regex', + array(array('start', '#^/not\\-trailing/regex/no\\-methods/(?P[^/]++)$#s', '/not-trailing/regex/no-methods'), null, null, null, array('_route' => 'regex_not_trailing_slash_no_methods')), + array(array('start', '#^/not\\-trailing/regex/get\\-method/(?P[^/]++)$#s', '/not-trailing/regex/get-method'), null, array('GET' => 'GET'), null, array('_route' => 'regex_not_trailing_slash_GET_method')), + array(array('start', '#^/not\\-trailing/regex/head\\-method/(?P[^/]++)$#s', '/not-trailing/regex/head-method'), null, array('HEAD' => 'HEAD'), null, array('_route' => 'regex_not_trailing_slash_HEAD_method')), + array(array('start', '#^/not\\-trailing/regex/post\\-method/(?P[^/]++)$#s', '/not-trailing/regex/post-method'), null, array('POST' => 'POST'), null, array('_route' => 'regex_not_trailing_slash_POST_method')), + ), + ), + ), + ), + array(), +); diff --git a/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php b/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php index 4b2e5b196dc06..89a6b0f7f68e4 100644 --- a/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php +++ b/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php @@ -18,6 +18,9 @@ use Symfony\Component\Routing\Generator\Dumper\PhpGeneratorDumper; use Symfony\Component\Routing\RequestContext; +/** + * @group legacy + */ class PhpGeneratorDumperTest extends TestCase { /** diff --git a/src/Symfony/Component/Routing/Tests/Generator/Dumper/StaticUrlGeneratorDumperTest.php b/src/Symfony/Component/Routing/Tests/Generator/Dumper/StaticUrlGeneratorDumperTest.php new file mode 100644 index 0000000000000..04d379279ffa6 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Generator/Dumper/StaticUrlGeneratorDumperTest.php @@ -0,0 +1,170 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Tests\Generator\Dumper; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\Generator\Dumper\StaticUrlGeneratorDumper; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\Generator\StaticUrlGenerator; + +class StaticUrlGeneratorDumperTest extends TestCase +{ + /** + * @var RouteCollection + */ + private $routeCollection; + + /** + * @var StaticUrlGeneratorDumper + */ + private $generatorDumper; + + /** + * @var string + */ + private $testTmpFilepath; + + /** + * @var string + */ + private $largeTestTmpFilepath; + + protected function setUp() + { + parent::setUp(); + + $this->routeCollection = new RouteCollection(); + $this->generatorDumper = new StaticUrlGeneratorDumper($this->routeCollection); + $this->testTmpFilepath = sys_get_temp_dir().'/php_generator.'.$this->getName().'.php'; + $this->largeTestTmpFilepath = sys_get_temp_dir().'/php_generator.'.$this->getName().'.large.php'; + @unlink($this->testTmpFilepath); + @unlink($this->largeTestTmpFilepath); + } + + protected function tearDown() + { + parent::tearDown(); + + @unlink($this->testTmpFilepath); + + $this->routeCollection = null; + $this->generatorDumper = null; + $this->testTmpFilepath = null; + } + + public function testDumpWithRoutes() + { + $this->routeCollection->add('Test', new Route('/testing/{foo}')); + $this->routeCollection->add('Test2', new Route('/testing2')); + + file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump()); + + $projectUrlGenerator = new StaticUrlGenerator(require $this->testTmpFilepath, new RequestContext('/app.php')); + + $absoluteUrlWithParameter = $projectUrlGenerator->generate('Test', array('foo' => 'bar'), UrlGeneratorInterface::ABSOLUTE_URL); + $absoluteUrlWithoutParameter = $projectUrlGenerator->generate('Test2', array(), UrlGeneratorInterface::ABSOLUTE_URL); + $relativeUrlWithParameter = $projectUrlGenerator->generate('Test', array('foo' => 'bar'), UrlGeneratorInterface::ABSOLUTE_PATH); + $relativeUrlWithoutParameter = $projectUrlGenerator->generate('Test2', array(), UrlGeneratorInterface::ABSOLUTE_PATH); + + $this->assertEquals('http://localhost/app.php/testing/bar', $absoluteUrlWithParameter); + $this->assertEquals('http://localhost/app.php/testing2', $absoluteUrlWithoutParameter); + $this->assertEquals('/app.php/testing/bar', $relativeUrlWithParameter); + $this->assertEquals('/app.php/testing2', $relativeUrlWithoutParameter); + } + + public function testDumpWithTooManyRoutes() + { + $this->routeCollection->add('Test', new Route('/testing/{foo}')); + for ($i = 0; $i < 32769; ++$i) { + $this->routeCollection->add('route_'.$i, new Route('/route_'.$i)); + } + $this->routeCollection->add('Test2', new Route('/testing2')); + + file_put_contents($this->largeTestTmpFilepath, $this->generatorDumper->dump()); + $this->routeCollection = $this->generatorDumper = null; + + $projectUrlGenerator = new StaticUrlGenerator(require $this->largeTestTmpFilepath, new RequestContext('/app.php')); + + $absoluteUrlWithParameter = $projectUrlGenerator->generate('Test', array('foo' => 'bar'), UrlGeneratorInterface::ABSOLUTE_URL); + $absoluteUrlWithoutParameter = $projectUrlGenerator->generate('Test2', array(), UrlGeneratorInterface::ABSOLUTE_URL); + $relativeUrlWithParameter = $projectUrlGenerator->generate('Test', array('foo' => 'bar'), UrlGeneratorInterface::ABSOLUTE_PATH); + $relativeUrlWithoutParameter = $projectUrlGenerator->generate('Test2', array(), UrlGeneratorInterface::ABSOLUTE_PATH); + + $this->assertEquals('http://localhost/app.php/testing/bar', $absoluteUrlWithParameter); + $this->assertEquals('http://localhost/app.php/testing2', $absoluteUrlWithoutParameter); + $this->assertEquals('/app.php/testing/bar', $relativeUrlWithParameter); + $this->assertEquals('/app.php/testing2', $relativeUrlWithoutParameter); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testDumpWithoutRoutes() + { + file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump()); + + $projectUrlGenerator = new StaticUrlGenerator(require $this->testTmpFilepath, new RequestContext('/app.php')); + + $projectUrlGenerator->generate('Test', array()); + } + + /** + * @expectedException \Symfony\Component\Routing\Exception\RouteNotFoundException + */ + public function testGenerateNonExistingRoute() + { + $this->routeCollection->add('Test', new Route('/test')); + + file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump()); + + $projectUrlGenerator = new StaticUrlGenerator(require $this->testTmpFilepath, new RequestContext()); + $url = $projectUrlGenerator->generate('NonExisting', array()); + } + + public function testDumpForRouteWithDefaults() + { + $this->routeCollection->add('Test', new Route('/testing/{foo}', array('foo' => 'bar'))); + + file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump()); + + $projectUrlGenerator = new StaticUrlGenerator(require $this->testTmpFilepath, new RequestContext()); + $url = $projectUrlGenerator->generate('Test', array()); + + $this->assertEquals('/testing', $url); + } + + public function testDumpWithSchemeRequirement() + { + $this->routeCollection->add('Test1', new Route('/testing', array(), array(), array(), '', array('ftp', 'https'))); + + file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump()); + + $projectUrlGenerator = new StaticUrlGenerator(require $this->testTmpFilepath, new RequestContext('/app.php')); + + $absoluteUrl = $projectUrlGenerator->generate('Test1', array(), UrlGeneratorInterface::ABSOLUTE_URL); + $relativeUrl = $projectUrlGenerator->generate('Test1', array(), UrlGeneratorInterface::ABSOLUTE_PATH); + + $this->assertEquals('ftp://localhost/app.php/testing', $absoluteUrl); + $this->assertEquals('ftp://localhost/app.php/testing', $relativeUrl); + + $projectUrlGenerator = new StaticUrlGenerator(require $this->testTmpFilepath, new RequestContext('/app.php', 'GET', 'localhost', 'https')); + + $absoluteUrl = $projectUrlGenerator->generate('Test1', array(), UrlGeneratorInterface::ABSOLUTE_URL); + $relativeUrl = $projectUrlGenerator->generate('Test1', array(), UrlGeneratorInterface::ABSOLUTE_PATH); + + $this->assertEquals('https://localhost/app.php/testing', $absoluteUrl); + $this->assertEquals('/app.php/testing', $relativeUrl); + } +} diff --git a/src/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php index e4c18c47b144f..46660c9138a56 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php @@ -19,6 +19,9 @@ use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; +/** + * @group legacy + */ class PhpMatcherDumperTest extends TestCase { /** diff --git a/src/Symfony/Component/Routing/Tests/Matcher/Dumper/StaticUrlMatcherDumperTest.php b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/StaticUrlMatcherDumperTest.php new file mode 100644 index 0000000000000..f876c6a874bb1 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/StaticUrlMatcherDumperTest.php @@ -0,0 +1,427 @@ + + * + * 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\StaticUrlMatcherDumper; +use Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface; +use Symfony\Component\Routing\Matcher\StaticUrlMatcher; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +class StaticUrlMatcherDumperTest extends TestCase +{ + /** + * @var string + */ + private $dumpPath; + + protected function setUp() + { + parent::setUp(); + + $this->dumpPath = sys_get_temp_dir().DIRECTORY_SEPARATOR.'php_matcher.'.uniqid('StaticUrlMatcher').'.php'; + } + + protected function tearDown() + { + parent::tearDown(); + + @unlink($this->dumpPath); + } + + public function testRedirectPreservesUrlEncoding() + { + $collection = new RouteCollection(); + $collection->add('foo', new Route('/foo:bar/')); + + $matcher = $this->generateDumpedMatcher($collection); + $matcher->expects($this->once())->method('redirect')->with('/foo%3Abar/', 'foo')->willReturn(array()); + + $matcher->match('/foo%3Abar'); + } + + /** + * @dataProvider getRouteCollections + */ + public function testDump(RouteCollection $collection, $fixture) + { + $basePath = __DIR__.'/../../Fixtures/dumper/'; + + $dumper = new StaticUrlMatcherDumper($collection); + $this->assertStringEqualsFile($basePath.$fixture, $dumper->dump()); + } + + public function getRouteCollections() + { + /* test case 1 */ + + $collection = new RouteCollection(); + + $collection->add('overridden', new Route('/overridden')); + + // defaults and requirements + $collection->add('foo', new Route( + '/foo/{bar}', + array('def' => 'test'), + array('bar' => 'baz|symfony') + )); + // method requirement + $collection->add('bar', new Route( + '/bar/{foo}', + array(), + array(), + array(), + '', + array(), + array('GET', 'head') + )); + // GET method requirement automatically adds HEAD as valid + $collection->add('barhead', new Route( + '/barhead/{foo}', + array(), + array(), + array(), + '', + array(), + array('GET') + )); + // simple + $collection->add('baz', new Route( + '/test/baz' + )); + // simple with extension + $collection->add('baz2', new Route( + '/test/baz.html' + )); + // trailing slash + $collection->add('baz3', new Route( + '/test/baz3/' + )); + // trailing slash with variable + $collection->add('baz4', new Route( + '/test/{foo}/' + )); + // trailing slash and method + $collection->add('baz5', new Route( + '/test/{foo}/', + array(), + array(), + array(), + '', + array(), + array('post') + )); + // complex name + $collection->add('baz.baz6', new Route( + '/test/{foo}/', + array(), + array(), + array(), + '', + array(), + array('put') + )); + // defaults without variable + $collection->add('foofoo', new Route( + '/foofoo', + array('def' => 'test') + )); + // pattern with quotes + $collection->add('quoter', new Route( + '/{quoter}', + array(), + array('quoter' => '[\']+') + )); + // space in pattern + $collection->add('space', new Route( + '/spa ce' + )); + + // prefixes + $collection1 = new RouteCollection(); + $collection1->add('overridden', new Route('/overridden1')); + $collection1->add('foo1', new Route('/{foo}')); + $collection1->add('bar1', new Route('/{bar}')); + $collection1->addPrefix('/b\'b'); + $collection2 = new RouteCollection(); + $collection2->addCollection($collection1); + $collection2->add('overridden', new Route('/{var}', array(), array('var' => '.*'))); + $collection1 = new RouteCollection(); + $collection1->add('foo2', new Route('/{foo1}')); + $collection1->add('bar2', new Route('/{bar1}')); + $collection1->addPrefix('/b\'b'); + $collection2->addCollection($collection1); + $collection2->addPrefix('/a'); + $collection->addCollection($collection2); + + // overridden through addCollection() and multiple sub-collections with no own prefix + $collection1 = new RouteCollection(); + $collection1->add('overridden2', new Route('/old')); + $collection1->add('helloWorld', new Route('/hello/{who}', array('who' => 'World!'))); + $collection2 = new RouteCollection(); + $collection3 = new RouteCollection(); + $collection3->add('overridden2', new Route('/new')); + $collection3->add('hey', new Route('/hey/')); + $collection2->addCollection($collection3); + $collection1->addCollection($collection2); + $collection1->addPrefix('/multi'); + $collection->addCollection($collection1); + + // "dynamic" prefix + $collection1 = new RouteCollection(); + $collection1->add('foo3', new Route('/{foo}')); + $collection1->add('bar3', new Route('/{bar}')); + $collection1->addPrefix('/b'); + $collection1->addPrefix('{_locale}'); + $collection->addCollection($collection1); + + // route between collections + $collection->add('ababa', new Route('/ababa')); + + // collection with static prefix but only one route + $collection1 = new RouteCollection(); + $collection1->add('foo4', new Route('/{foo}')); + $collection1->addPrefix('/aba'); + $collection->addCollection($collection1); + + // prefix and host + + $collection1 = new RouteCollection(); + + $route1 = new Route('/route1', array(), array(), array(), 'a.example.com'); + $collection1->add('route1', $route1); + + $route2 = new Route('/c2/route2', array(), array(), array(), 'a.example.com'); + $collection1->add('route2', $route2); + + $route3 = new Route('/c2/route3', array(), array(), array(), 'b.example.com'); + $collection1->add('route3', $route3); + + $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); + + // host 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...')); + $collection2 = new RouteCollection(); + $collection2->add('b', new Route('/{var}')); + $collection3 = new RouteCollection(); + $collection3->add('c', new Route('/{var}')); + $collection3->addPrefix('/c'); + $collection2->addCollection($collection3); + $collection2->addPrefix('/b'); + $collection1->addCollection($collection2); + $collection1->addPrefix('/a'); + $collection->addCollection($collection1); + + /* test case 2 */ + + $redirectCollection = clone $collection; + + // force HTTPS redirection + $redirectCollection->add('secure', new Route( + '/secure', + array(), + array(), + array(), + '', + array('https') + )); + + // force HTTP redirection + $redirectCollection->add('nonsecure', new Route( + '/nonsecure', + array(), + array(), + array(), + '', + array('http') + )); + + /* test case 3 */ + + $rootprefixCollection = new RouteCollection(); + $rootprefixCollection->add('static', new Route('/test')); + $rootprefixCollection->add('dynamic', new Route('/{var}')); + $rootprefixCollection->addPrefix('rootprefix'); + $route = new Route('/with-condition'); + $route->setCondition('context.getMethod() == "GET"'); + $rootprefixCollection->add('with-condition', $route); + + /* test case 4 */ + $headMatchCasesCollection = new RouteCollection(); + $headMatchCasesCollection->add('just_head', new Route( + '/just_head', + array(), + array(), + array(), + '', + array(), + array('HEAD') + )); + $headMatchCasesCollection->add('head_and_get', new Route( + '/head_and_get', + array(), + array(), + array(), + '', + array(), + array('HEAD', 'GET') + )); + $headMatchCasesCollection->add('get_and_head', new Route( + '/get_and_head', + array(), + array(), + array(), + '', + array(), + array('GET', 'HEAD') + )); + $headMatchCasesCollection->add('post_and_head', new Route( + '/post_and_get', + array(), + array(), + array(), + '', + array(), + array('POST', 'HEAD') + )); + $headMatchCasesCollection->add('put_and_post', new Route( + '/put_and_post', + array(), + array(), + array(), + '', + array(), + array('PUT', 'POST') + )); + $headMatchCasesCollection->add('put_and_get_and_head', new Route( + '/put_and_post', + array(), + array(), + array(), + '', + array(), + 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/')); + + $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'))); + $trailingSlashCollection->add('simple_trailing_slash_HEAD_method', new Route('/trailing/simple/head-method/', array(), array(), array(), '', array(), array('HEAD'))); + $trailingSlashCollection->add('simple_trailing_slash_POST_method', new Route('/trailing/simple/post-method/', array(), array(), array(), '', array(), array('POST'))); + $trailingSlashCollection->add('regex_trailing_slash_no_methods', new Route('/trailing/regex/no-methods/{param}/', array(), array(), array(), '', array(), array())); + $trailingSlashCollection->add('regex_trailing_slash_GET_method', new Route('/trailing/regex/get-method/{param}/', array(), array(), array(), '', array(), array('GET'))); + $trailingSlashCollection->add('regex_trailing_slash_HEAD_method', new Route('/trailing/regex/head-method/{param}/', array(), array(), array(), '', array(), array('HEAD'))); + $trailingSlashCollection->add('regex_trailing_slash_POST_method', new Route('/trailing/regex/post-method/{param}/', array(), array(), array(), '', array(), array('POST'))); + + $trailingSlashCollection->add('simple_not_trailing_slash_no_methods', new Route('/not-trailing/simple/no-methods', array(), array(), array(), '', array(), array())); + $trailingSlashCollection->add('simple_not_trailing_slash_GET_method', new Route('/not-trailing/simple/get-method', array(), array(), array(), '', array(), array('GET'))); + $trailingSlashCollection->add('simple_not_trailing_slash_HEAD_method', new Route('/not-trailing/simple/head-method', array(), array(), array(), '', array(), array('HEAD'))); + $trailingSlashCollection->add('simple_not_trailing_slash_POST_method', new Route('/not-trailing/simple/post-method', array(), array(), array(), '', array(), array('POST'))); + $trailingSlashCollection->add('regex_not_trailing_slash_no_methods', new Route('/not-trailing/regex/no-methods/{param}', array(), array(), array(), '', array(), array())); + $trailingSlashCollection->add('regex_not_trailing_slash_GET_method', new Route('/not-trailing/regex/get-method/{param}', array(), array(), array(), '', array(), array('GET'))); + $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'))); + + return array( + array(new RouteCollection(), 'static_url_matcher0.php'), + array($collection, 'static_url_matcher1.php'), + array($redirectCollection, 'static_url_matcher2.php'), + array($rootprefixCollection, 'static_url_matcher3.php'), + array($headMatchCasesCollection, 'static_url_matcher4.php'), + array($groupOptimisedCollection, 'static_url_matcher5.php'), + array($trailingSlashCollection, 'static_url_matcher6.php'), + array($trailingSlashCollection, 'static_url_matcher7.php'), + ); + } + + /** + * @param $dumper + */ + private function generateDumpedMatcher(RouteCollection $collection) + { + $dumper = new StaticUrlMatcherDumper($collection); + $code = $dumper->dump(); + + file_put_contents($this->dumpPath, $code); + $dumpedRoutes = require $this->dumpPath; + + return $this->getMockBuilder(TestStaticUrlMatcher::class) + ->setConstructorArgs(array($dumpedRoutes, new RequestContext())) + ->setMethods(array('redirect')) + ->getMock(); + } +} + +class TestStaticUrlMatcher extends StaticUrlMatcher implements RedirectableUrlMatcherInterface +{ + public function redirect($path, $route, $scheme = null) + { + return array(); + } +} diff --git a/src/Symfony/Component/Routing/Tests/Matcher/StaticUrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/StaticUrlMatcherTest.php new file mode 100644 index 0000000000000..0c5d6aab38f7e --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Matcher/StaticUrlMatcherTest.php @@ -0,0 +1,28 @@ + + * + * 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; + +use Symfony\Component\Routing\Matcher\Dumper\StaticUrlMatcherDumper; +use Symfony\Component\Routing\Matcher\StaticUrlMatcher; +use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Routing\RequestContext; + +class StaticUrlMatcherTest extends UrlMatcherTest +{ + protected function getUrlMatcher(RouteCollection $routes, RequestContext $context) + { + $dumper = new StaticUrlMatcherDumper($routes); + $dumpedRoutes = eval('?>'.$dumper->dump()); + + return new StaticUrlMatcher($dumpedRoutes, $context); + } +} diff --git a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php index 8545c2c29d83d..bb3f61864837f 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php @@ -26,7 +26,7 @@ public function testNoMethodSoAllowed() $coll = new RouteCollection(); $coll->add('foo', new Route('/foo')); - $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher = $this->getUrlMatcher($coll, new RequestContext()); $this->assertInternalType('array', $matcher->match('/foo')); } @@ -35,7 +35,7 @@ public function testMethodNotAllowed() $coll = new RouteCollection(); $coll->add('foo', new Route('/foo', array(), array(), array(), '', array(), array('post'))); - $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher = $this->getUrlMatcher($coll, new RequestContext()); try { $matcher->match('/foo'); @@ -50,7 +50,7 @@ public function testHeadAllowedWhenRequirementContainsGet() $coll = new RouteCollection(); $coll->add('foo', new Route('/foo', array(), array(), array(), '', array(), array('get'))); - $matcher = new UrlMatcher($coll, new RequestContext('', 'head')); + $matcher = $this->getUrlMatcher($coll, new RequestContext('', 'head')); $this->assertInternalType('array', $matcher->match('/foo')); } @@ -60,7 +60,7 @@ public function testMethodNotAllowedAggregatesAllowedMethods() $coll->add('foo1', new Route('/foo', array(), array(), array(), '', array(), array('post'))); $coll->add('foo2', new Route('/foo', array(), array(), array(), '', array(), array('put', 'delete'))); - $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher = $this->getUrlMatcher($coll, new RequestContext()); try { $matcher->match('/foo'); @@ -75,7 +75,7 @@ public function testMatch() // test the patterns are matched and parameters are returned $collection = new RouteCollection(); $collection->add('foo', new Route('/foo/{bar}')); - $matcher = new UrlMatcher($collection, new RequestContext()); + $matcher = $this->getUrlMatcher($collection, new RequestContext()); try { $matcher->match('/no-match'); $this->fail(); @@ -86,17 +86,17 @@ public function testMatch() // test that defaults are merged $collection = new RouteCollection(); $collection->add('foo', new Route('/foo/{bar}', array('def' => 'test'))); - $matcher = new UrlMatcher($collection, new RequestContext()); + $matcher = $this->getUrlMatcher($collection, new RequestContext()); $this->assertEquals(array('_route' => 'foo', 'bar' => 'baz', 'def' => 'test'), $matcher->match('/foo/baz')); // test that route "method" is ignored if no method is given in the context $collection = new RouteCollection(); $collection->add('foo', new Route('/foo', array(), array(), array(), '', array(), array('get', 'head'))); - $matcher = new UrlMatcher($collection, new RequestContext()); + $matcher = $this->getUrlMatcher($collection, new RequestContext()); $this->assertInternalType('array', $matcher->match('/foo')); // route does not match with POST method context - $matcher = new UrlMatcher($collection, new RequestContext('', 'post')); + $matcher = $this->getUrlMatcher($collection, new RequestContext('', 'post')); try { $matcher->match('/foo'); $this->fail(); @@ -104,28 +104,28 @@ public function testMatch() } // route does match with GET or HEAD method context - $matcher = new UrlMatcher($collection, new RequestContext()); + $matcher = $this->getUrlMatcher($collection, new RequestContext()); $this->assertInternalType('array', $matcher->match('/foo')); - $matcher = new UrlMatcher($collection, new RequestContext('', 'head')); + $matcher = $this->getUrlMatcher($collection, new RequestContext('', 'head')); $this->assertInternalType('array', $matcher->match('/foo')); // route with an optional variable as the first segment $collection = new RouteCollection(); $collection->add('bar', new Route('/{bar}/foo', array('bar' => 'bar'), array('bar' => 'foo|bar'))); - $matcher = new UrlMatcher($collection, new RequestContext()); + $matcher = $this->getUrlMatcher($collection, new RequestContext()); $this->assertEquals(array('_route' => 'bar', 'bar' => 'bar'), $matcher->match('/bar/foo')); $this->assertEquals(array('_route' => 'bar', 'bar' => 'foo'), $matcher->match('/foo/foo')); $collection = new RouteCollection(); $collection->add('bar', new Route('/{bar}', array('bar' => 'bar'), array('bar' => 'foo|bar'))); - $matcher = new UrlMatcher($collection, new RequestContext()); + $matcher = $this->getUrlMatcher($collection, new RequestContext()); $this->assertEquals(array('_route' => 'bar', 'bar' => 'foo'), $matcher->match('/foo')); $this->assertEquals(array('_route' => 'bar', 'bar' => 'bar'), $matcher->match('/')); // route with only optional variables $collection = new RouteCollection(); $collection->add('bar', new Route('/{foo}/{bar}', array('foo' => 'foo', 'bar' => 'bar'), array())); - $matcher = new UrlMatcher($collection, new RequestContext()); + $matcher = $this->getUrlMatcher($collection, new RequestContext()); $this->assertEquals(array('_route' => 'bar', 'foo' => 'foo', 'bar' => 'bar'), $matcher->match('/')); $this->assertEquals(array('_route' => 'bar', 'foo' => 'a', 'bar' => 'bar'), $matcher->match('/a')); $this->assertEquals(array('_route' => 'bar', 'foo' => 'a', 'bar' => 'b'), $matcher->match('/a/b')); @@ -138,7 +138,7 @@ public function testMatchWithPrefixes() $collection->addPrefix('/b'); $collection->addPrefix('/a'); - $matcher = new UrlMatcher($collection, new RequestContext()); + $matcher = $this->getUrlMatcher($collection, new RequestContext()); $this->assertEquals(array('_route' => 'foo', 'foo' => 'foo'), $matcher->match('/a/b/foo')); } @@ -149,7 +149,7 @@ public function testMatchWithDynamicPrefix() $collection->addPrefix('/b'); $collection->addPrefix('/{_locale}'); - $matcher = new UrlMatcher($collection, new RequestContext()); + $matcher = $this->getUrlMatcher($collection, new RequestContext()); $this->assertEquals(array('_locale' => 'fr', '_route' => 'foo', 'foo' => 'foo'), $matcher->match('/fr/b/foo')); } @@ -158,7 +158,7 @@ public function testMatchSpecialRouteName() $collection = new RouteCollection(); $collection->add('$péß^a|', new Route('/bar')); - $matcher = new UrlMatcher($collection, new RequestContext()); + $matcher = $this->getUrlMatcher($collection, new RequestContext()); $this->assertEquals(array('_route' => '$péß^a|'), $matcher->match('/bar')); } @@ -168,7 +168,7 @@ public function testMatchNonAlpha() $chars = '!"$%éà &\'()*+,./:;<=>@ABCDEFGHIJKLMNOPQRSTUVWXYZ\\[]^_`abcdefghijklmnopqrstuvwxyz{|}~-'; $collection->add('foo', new Route('/{foo}/bar', array(), array('foo' => '['.preg_quote($chars).']+'), array('utf8' => true))); - $matcher = new UrlMatcher($collection, new RequestContext()); + $matcher = $this->getUrlMatcher($collection, new RequestContext()); $this->assertEquals(array('_route' => 'foo', 'foo' => $chars), $matcher->match('/'.rawurlencode($chars).'/bar')); $this->assertEquals(array('_route' => 'foo', 'foo' => $chars), $matcher->match('/'.strtr($chars, array('%' => '%25')).'/bar')); } @@ -178,7 +178,7 @@ public function testMatchWithDotMetacharacterInRequirements() $collection = new RouteCollection(); $collection->add('foo', new Route('/{foo}/bar', array(), array('foo' => '.+'))); - $matcher = new UrlMatcher($collection, new RequestContext()); + $matcher = $this->getUrlMatcher($collection, new RequestContext()); $this->assertEquals(array('_route' => 'foo', 'foo' => "\n"), $matcher->match('/'.urlencode("\n").'/bar'), 'linefeed character is matched'); } @@ -192,7 +192,7 @@ public function testMatchOverriddenRoute() $collection->addCollection($collection1); - $matcher = new UrlMatcher($collection, new RequestContext()); + $matcher = $this->getUrlMatcher($collection, new RequestContext()); $this->assertEquals(array('_route' => 'foo'), $matcher->match('/foo1')); $this->{method_exists($this, $_ = 'expectException') ? $_ : 'setExpectedException'}('Symfony\Component\Routing\Exception\ResourceNotFoundException'); @@ -205,12 +205,12 @@ public function testMatchRegression() $coll->add('foo', new Route('/foo/{foo}')); $coll->add('bar', new Route('/foo/bar/{foo}')); - $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher = $this->getUrlMatcher($coll, new RequestContext()); $this->assertEquals(array('foo' => 'bar', '_route' => 'bar'), $matcher->match('/foo/bar/bar')); $collection = new RouteCollection(); $collection->add('foo', new Route('/{bar}')); - $matcher = new UrlMatcher($collection, new RequestContext()); + $matcher = $this->getUrlMatcher($collection, new RequestContext()); try { $matcher->match('/'); $this->fail(); @@ -223,7 +223,7 @@ public function testDefaultRequirementForOptionalVariables() $coll = new RouteCollection(); $coll->add('test', new Route('/{page}.{_format}', array('page' => 'index', '_format' => 'html'))); - $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher = $this->getUrlMatcher($coll, new RequestContext()); $this->assertEquals(array('page' => 'my-page', '_format' => 'xml', '_route' => 'test'), $matcher->match('/my-page.xml')); } @@ -232,7 +232,7 @@ public function testMatchingIsEager() $coll = new RouteCollection(); $coll->add('test', new Route('/{foo}-{bar}-', array(), array('foo' => '.+', 'bar' => '.+'))); - $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher = $this->getUrlMatcher($coll, new RequestContext()); $this->assertEquals(array('foo' => 'text1-text2-text3', 'bar' => 'text4', '_route' => 'test'), $matcher->match('/text1-text2-text3-text4-')); } @@ -241,7 +241,7 @@ public function testAdjacentVariables() $coll = new RouteCollection(); $coll->add('test', new Route('/{w}{x}{y}{z}.{_format}', array('z' => 'default-z', '_format' => 'html'), array('y' => 'y|Y'))); - $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher = $this->getUrlMatcher($coll, new RequestContext()); // 'w' eagerly matches as much as possible and the other variables match the remaining chars. // This also shows that the variables w-z must all exclude the separating char (the dot '.' in this case) by default requirement. // Otherwise they would also consume '.xml' and _format would never match as it's an optional variable. @@ -260,7 +260,7 @@ public function testOptionalVariableWithNoRealSeparator() { $coll = new RouteCollection(); $coll->add('test', new Route('/get{what}', array('what' => 'All'))); - $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher = $this->getUrlMatcher($coll, new RequestContext()); $this->assertEquals(array('what' => 'All', '_route' => 'test'), $matcher->match('/get')); $this->assertEquals(array('what' => 'Sites', '_route' => 'test'), $matcher->match('/getSites')); @@ -275,7 +275,7 @@ public function testRequiredVariableWithNoRealSeparator() { $coll = new RouteCollection(); $coll->add('test', new Route('/get{what}Suffix')); - $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher = $this->getUrlMatcher($coll, new RequestContext()); $this->assertEquals(array('what' => 'Sites', '_route' => 'test'), $matcher->match('/getSitesSuffix')); } @@ -284,7 +284,7 @@ public function testDefaultRequirementOfVariable() { $coll = new RouteCollection(); $coll->add('test', new Route('/{page}.{_format}')); - $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher = $this->getUrlMatcher($coll, new RequestContext()); $this->assertEquals(array('page' => 'index', '_format' => 'mobile.html', '_route' => 'test'), $matcher->match('/index.mobile.html')); } @@ -296,7 +296,7 @@ public function testDefaultRequirementOfVariableDisallowsSlash() { $coll = new RouteCollection(); $coll->add('test', new Route('/{page}.{_format}')); - $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher = $this->getUrlMatcher($coll, new RequestContext()); $matcher->match('/index.sl/ash'); } @@ -308,7 +308,7 @@ public function testDefaultRequirementOfVariableDisallowsNextSeparator() { $coll = new RouteCollection(); $coll->add('test', new Route('/{page}.{_format}', array(), array('_format' => 'html|xml'))); - $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher = $this->getUrlMatcher($coll, new RequestContext()); $matcher->match('/do.t.html'); } @@ -320,7 +320,7 @@ public function testSchemeRequirement() { $coll = new RouteCollection(); $coll->add('foo', new Route('/foo', array(), array(), array(), '', array('https'))); - $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher = $this->getUrlMatcher($coll, new RequestContext()); $matcher->match('/foo'); } @@ -333,7 +333,7 @@ public function testCondition() $route = new Route('/foo'); $route->setCondition('context.getMethod() == "POST"'); $coll->add('foo', $route); - $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher = $this->getUrlMatcher($coll, new RequestContext()); $matcher->match('/foo'); } @@ -343,7 +343,7 @@ public function testRequestCondition() $route = new Route('/foo/{bar}'); $route->setCondition('request.getBaseUrl() == "/sub/front.php" and request.getPathInfo() == "/foo/bar"'); $coll->add('foo', $route); - $matcher = new UrlMatcher($coll, new RequestContext('/sub/front.php')); + $matcher = $this->getUrlMatcher($coll, new RequestContext('/sub/front.php')); $this->assertEquals(array('bar' => 'bar', '_route' => 'foo'), $matcher->match('/foo/bar')); } @@ -352,7 +352,7 @@ public function testDecodeOnce() $coll = new RouteCollection(); $coll->add('foo', new Route('/foo/{foo}')); - $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher = $this->getUrlMatcher($coll, new RequestContext()); $this->assertEquals(array('foo' => 'bar%23', '_route' => 'foo'), $matcher->match('/foo/bar%2523')); } @@ -368,7 +368,7 @@ public function testCannotRelyOnPrefix() $coll->addCollection($subColl); - $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher = $this->getUrlMatcher($coll, new RequestContext()); $this->assertEquals(array('_route' => 'bar'), $matcher->match('/new')); } @@ -377,7 +377,7 @@ public function testWithHost() $coll = new RouteCollection(); $coll->add('foo', new Route('/foo/{foo}', array(), array(), array(), '{locale}.example.com')); - $matcher = new UrlMatcher($coll, new RequestContext('', 'GET', 'en.example.com')); + $matcher = $this->getUrlMatcher($coll, new RequestContext('', 'GET', 'en.example.com')); $this->assertEquals(array('foo' => 'bar', '_route' => 'foo', 'locale' => 'en'), $matcher->match('/foo/bar')); } @@ -388,10 +388,10 @@ public function testWithHostOnRouteCollection() $coll->add('bar', new Route('/bar/{foo}', array(), array(), array(), '{locale}.example.net')); $coll->setHost('{locale}.example.com'); - $matcher = new UrlMatcher($coll, new RequestContext('', 'GET', 'en.example.com')); + $matcher = $this->getUrlMatcher($coll, new RequestContext('', 'GET', 'en.example.com')); $this->assertEquals(array('foo' => 'bar', '_route' => 'foo', 'locale' => 'en'), $matcher->match('/foo/bar')); - $matcher = new UrlMatcher($coll, new RequestContext('', 'GET', 'en.example.com')); + $matcher = $this->getUrlMatcher($coll, new RequestContext('', 'GET', 'en.example.com')); $this->assertEquals(array('foo' => 'bar', '_route' => 'bar', 'locale' => 'en'), $matcher->match('/bar/bar')); } @@ -403,7 +403,7 @@ public function testWithOutHostHostDoesNotMatch() $coll = new RouteCollection(); $coll->add('foo', new Route('/foo/{foo}', array(), array(), array(), '{locale}.example.com')); - $matcher = new UrlMatcher($coll, new RequestContext('', 'GET', 'example.com')); + $matcher = $this->getUrlMatcher($coll, new RequestContext('', 'GET', 'example.com')); $matcher->match('/foo/bar'); } @@ -415,7 +415,7 @@ public function testPathIsCaseSensitive() $coll = new RouteCollection(); $coll->add('foo', new Route('/locale', array(), array('locale' => 'EN|FR|DE'))); - $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher = $this->getUrlMatcher($coll, new RequestContext()); $matcher->match('/en'); } @@ -424,7 +424,7 @@ public function testHostIsCaseInsensitive() $coll = new RouteCollection(); $coll->add('foo', new Route('/', array(), array('locale' => 'EN|FR|DE'), array(), '{locale}.example.com')); - $matcher = new UrlMatcher($coll, new RequestContext('', 'GET', 'en.example.com')); + $matcher = $this->getUrlMatcher($coll, new RequestContext('', 'GET', 'en.example.com')); $this->assertEquals(array('_route' => 'foo', 'locale' => 'en'), $matcher->match('/')); } @@ -435,7 +435,36 @@ public function testNoConfiguration() { $coll = new RouteCollection(); - $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher = $this->getUrlMatcher($coll, new RequestContext()); $matcher->match('/'); } + + public function testNestedCollections() + { + $coll = new RouteCollection(); + + $subColl = new RouteCollection(); + $subColl->add('a', new Route('/a')); + $subColl->add('b', new Route('/b')); + $subColl->add('c', new Route('/c')); + $subColl->addPrefix('/p'); + $coll->addCollection($subColl); + + $coll->add('baz', new Route('/{baz}')); + + $subColl = new RouteCollection(); + $subColl->add('buz', new Route('/buz')); + $subColl->addPrefix('/prefix'); + $coll->addCollection($subColl); + + $matcher = $this->getUrlMatcher($coll, new RequestContext()); + $this->assertEquals(array('_route' => 'a'), $matcher->match('/p/a')); + $this->assertEquals(array('_route' => 'baz', 'baz' => 'p'), $matcher->match('/p')); + $this->assertEquals(array('_route' => 'buz'), $matcher->match('/prefix/buz')); + } + + protected function getUrlMatcher(RouteCollection $routes, RequestContext $context) + { + return new UrlMatcher($routes, $context); + } }