diff --git a/src/Symfony/Component/Routing/Loader/Configurator/RoutingConfigurator.php b/src/Symfony/Component/Routing/Loader/Configurator/RoutingConfigurator.php index 713fcd2e05190..99bb9d7a9be54 100644 --- a/src/Symfony/Component/Routing/Loader/Configurator/RoutingConfigurator.php +++ b/src/Symfony/Component/Routing/Loader/Configurator/RoutingConfigurator.php @@ -51,4 +51,11 @@ final public function collection($name = '') { return new CollectionConfigurator($this->collection, $name); } + + final public function subroutine(string $name, string $pattern): self + { + $this->collection->setSubroutine($name, $pattern); + + return $this; + } } diff --git a/src/Symfony/Component/Routing/Loader/XmlFileLoader.php b/src/Symfony/Component/Routing/Loader/XmlFileLoader.php index 81a4c94ce06fc..cb02ac5072dbc 100644 --- a/src/Symfony/Component/Routing/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Routing/Loader/XmlFileLoader.php @@ -83,6 +83,9 @@ protected function parseNode(RouteCollection $collection, \DOMElement $node, $pa case 'import': $this->parseImport($collection, $node, $path, $file); break; + case 'subroutine': + $collection->setSubroutine($node->getAttribute('id'), $node->getAttribute('pattern')); + break; default: throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "route" or "import".', $node->localName, $path)); } diff --git a/src/Symfony/Component/Routing/Loader/YamlFileLoader.php b/src/Symfony/Component/Routing/Loader/YamlFileLoader.php index 30d66d36113bb..a0a146bfd4ae3 100644 --- a/src/Symfony/Component/Routing/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Routing/Loader/YamlFileLoader.php @@ -78,6 +78,12 @@ public function load($file, $type = null) } foreach ($parsedConfig as $name => $config) { + if ('_subroutines' === $name && \is_array($config) && !isset($config['resource']) && !isset($config['path'])) { + foreach ($config as $name => $pattern) { + $collection->setSubroutine($name, $pattern); + } + continue; + } $this->validate($config, $name, $path); if (isset($config['resource'])) { diff --git a/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd b/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd index dd2477999df24..494ce332830a0 100644 --- a/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd +++ b/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd @@ -21,6 +21,7 @@ + @@ -69,6 +70,11 @@ + + + + + diff --git a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php index aa8f61cc8b78d..7ebd73d13494b 100644 --- a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php +++ b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php @@ -404,6 +404,11 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $matchHo $code .= "\n .')'"; $state->regex .= ')'; } + foreach ($this->getRoutes()->getSubroutines() as $name => $rx) { + $rx = sprintf('(?(DEFINE)(?P<%s>%s))', $name, $rx); + $code .= "\n .'{$rx}'"; + $state->regex .= $rx; + } $rx = ")$}{$modifiers}"; $code .= "\n .'{$rx}',"; $state->regex .= $rx; diff --git a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php index e37cae0361eb8..d3ea89ef1fa34 100644 --- a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php @@ -130,6 +130,11 @@ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterfac */ protected function matchCollection($pathinfo, RouteCollection $routes) { + $subroutines = ''; + foreach ($routes->getSubroutines() as $name => $pattern) { + $subroutines .= sprintf('(?(DEFINE)(?P<%s>%s))', $name, $pattern); + } + foreach ($routes as $name => $route) { $compiledRoute = $route->compile(); @@ -138,13 +143,22 @@ protected function matchCollection($pathinfo, RouteCollection $routes) continue; } - if (!preg_match($compiledRoute->getRegex(), $pathinfo, $matches)) { + $rx = $compiledRoute->getRegex(); + if ('' !== $subroutines) { + $rx = substr_replace($rx, $subroutines, strrpos($rx, '#'), 0); + } + if (!preg_match($rx, $pathinfo, $matches)) { continue; } $hostMatches = array(); - if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) { - continue; + if ($rx = $compiledRoute->getHostRegex()) { + if ('' !== $subroutines) { + $rx = substr_replace($rx, $subroutines, strrpos($rx, '#'), 0); + } + if (!preg_match($rx, $this->context->getHost(), $hostMatches)) { + continue; + } } $status = $this->handleRouteRequirements($pathinfo, $name, $route); diff --git a/src/Symfony/Component/Routing/RouteCollection.php b/src/Symfony/Component/Routing/RouteCollection.php index 84719e2c82fa5..6a3860fa7fde0 100644 --- a/src/Symfony/Component/Routing/RouteCollection.php +++ b/src/Symfony/Component/Routing/RouteCollection.php @@ -35,6 +35,11 @@ class RouteCollection implements \IteratorAggregate, \Countable */ private $resources = array(); + /** + * @var array + */ + private $subroutines = array(); + public function __clone() { foreach ($this->routes as $name => $route) { @@ -129,6 +134,10 @@ public function addCollection(self $collection) foreach ($collection->getResources() as $resource) { $this->addResource($resource); } + + foreach ($collection->getSubroutines() as $name => $pattern) { + $this->setSubroutine($name, $pattern); + } } /** @@ -291,4 +300,23 @@ public function addResource(ResourceInterface $resource) $this->resources[$key] = $resource; } } + + /** + * Sets a subroutine that can be reused in requirements. + */ + public function setSubroutine(string $name, string $pattern) + { + if (\strlen($name) > RouteCompiler::VARIABLE_MAXIMUM_LENGTH) { + throw new \DomainException(sprintf('Subroutine name "%s" cannot be longer than %s characters. Please use a shorter name for pattern "%s".', $name, RouteCompiler::VARIABLE_MAXIMUM_LENGTH, $pattern)); + } + $this->subroutines[$name] = $pattern; + } + + /** + * Returns the defined subroutines. + */ + public function getSubroutines(): array + { + return $this->subroutines; + } } diff --git a/src/Symfony/Component/Routing/RouteCollectionBuilder.php b/src/Symfony/Component/Routing/RouteCollectionBuilder.php index d63c6138f7983..d40592c8cf3c2 100644 --- a/src/Symfony/Component/Routing/RouteCollectionBuilder.php +++ b/src/Symfony/Component/Routing/RouteCollectionBuilder.php @@ -37,6 +37,7 @@ class RouteCollectionBuilder private $schemes; private $methods; private $resources = array(); + private $subroutines = array(); public function __construct(LoaderInterface $loader = null) { @@ -76,6 +77,10 @@ public function import($resource, $prefix = '/', $type = null) foreach ($collection->getResources() as $resource) { $builder->addResource($resource); } + + foreach ($collection->getSubroutines() as $name => $pattern) { + $builder->setSubroutine($name, $pattern); + } } // mount into this builder @@ -262,6 +267,22 @@ private function addResource(ResourceInterface $resource): RouteCollectionBuilde return $this; } + /** + * Sets a subroutine that can be reused in requirements. + */ + public function setSubroutine(string $name, string $pattern) + { + $this->subroutines[$name] = $pattern; + } + + /** + * Returns the defined subroutines. + */ + public function getSubroutines() + { + return $this->subroutines; + } + /** * Creates the final RouteCollection and returns it. * @@ -321,6 +342,10 @@ public function build() $routeCollection->addResource($resource); } + foreach ($this->subroutines as $name => $pattern) { + $routeCollection->setSubroutine($name, $pattern); + } + return $routeCollection; } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/controller/routing.xml b/src/Symfony/Component/Routing/Tests/Fixtures/controller/routing.xml index 6420138a65072..674b010b64bad 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/controller/routing.xml +++ b/src/Symfony/Component/Routing/Tests/Fixtures/controller/routing.xml @@ -4,6 +4,8 @@ xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd"> + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/controller/routing.yml b/src/Symfony/Component/Routing/Tests/Fixtures/controller/routing.yml index cb71ec3b75b79..c4f41c6c8637f 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/controller/routing.yml +++ b/src/Symfony/Component/Routing/Tests/Fixtures/controller/routing.yml @@ -1,3 +1,6 @@ +_subroutines: + number: \d + app_homepage: path: / controller: AppBundle:Homepage:show diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/php_dsl.php b/src/Symfony/Component/Routing/Tests/Fixtures/php_dsl.php index 0b8fa0a9eb339..b017ebda2a6da 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/php_dsl.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/php_dsl.php @@ -3,6 +3,8 @@ namespace Symfony\Component\Routing\Loader\Configurator; return function (RoutingConfigurator $routes) { + $routes->subroutine('number', '\d'); + $routes ->collection() ->add('foo', '/foo') diff --git a/src/Symfony/Component/Routing/Tests/Loader/PhpFileLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/PhpFileLoaderTest.php index c0a72b6f81cbe..1d5ffb858296b 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/PhpFileLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/PhpFileLoaderTest.php @@ -113,6 +113,7 @@ public function testRoutingConfigurator() ->setMethods(array('GET')) ->setDefaults(array('id' => 0)) ); + $expectedCollection->setSubroutine('number', '\d'); $expectedCollection->addResource(new FileResource(realpath(__DIR__.'/../Fixtures/php_dsl_sub.php'))); $expectedCollection->addResource(new FileResource(realpath(__DIR__.'/../Fixtures/php_dsl.php'))); diff --git a/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php index 0b2e1a9d79340..c1f00a33fc222 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php @@ -411,4 +411,12 @@ public function testImportRouteWithNamePrefix() $this->assertNotNull($routeCollection->get('api_app_blog')); $this->assertEquals('/api/blog', $routeCollection->get('api_app_blog')->getPath()); } + + public function testSubroutine() + { + $loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/controller'))); + $routeCollection = $loader->load('routing.xml'); + + $this->assertSame(array('number' => '\d'), $routeCollection->getSubroutines()); + } } diff --git a/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php index 3bcfe1b5b6453..97bb3feb28408 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php @@ -275,4 +275,12 @@ public function testImportingWithControllerDefault() $this->assertEquals('DefaultController::defaultAction', $routes->get('home.nl')->getDefault('_controller')); $this->assertEquals('DefaultController::defaultAction', $routes->get('not_localized')->getDefault('_controller')); } + + public function testSubroutine() + { + $loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/controller'))); + $routeCollection = $loader->load('routing.yml'); + + $this->assertSame(array('number' => '\d'), $routeCollection->getSubroutines()); + } } diff --git a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php index cf7ded2551cf2..eb9312b6049eb 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php @@ -596,6 +596,16 @@ public function testRequirementWithCapturingGroup() $this->assertEquals(array('_route' => 'a', 'a' => 'a', 'b' => 'b'), $matcher->match('/a/b')); } + public function testSubroutine() + { + $coll = new RouteCollection(); + $coll->setSubroutine('date', '\d{4}-\d{2}-\d{2}'); + $coll->add('a', new Route('/{a}', array(), array('a' => '(?&date)'))); + + $matcher = $this->getUrlMatcher($coll); + $this->assertEquals(array('_route' => 'a', 'a' => '2018-03-14'), $matcher->match('/2018-03-14')); + } + protected function getUrlMatcher(RouteCollection $routes, RequestContext $context = null) { return new UrlMatcher($routes, $context ?: new RequestContext()); diff --git a/src/Symfony/Component/Routing/Tests/RouteCollectionBuilderTest.php b/src/Symfony/Component/Routing/Tests/RouteCollectionBuilderTest.php index 76a042d670b29..7fa1209ae4f7c 100644 --- a/src/Symfony/Component/Routing/Tests/RouteCollectionBuilderTest.php +++ b/src/Symfony/Component/Routing/Tests/RouteCollectionBuilderTest.php @@ -361,4 +361,13 @@ public function testAddsThePrefixOnlyOnceWhenLoadingMultipleCollections() $this->assertEquals('/other/a', $routes['a']->getPath()); $this->assertEquals('/other/b', $routes['b']->getPath()); } + + public function testSubroutine() + { + $routeCollectionBuilder = new RouteCollectionBuilder(new YamlFileLoader(new FileLocator(array(__DIR__.'/Fixtures/controller')))); + $routeCollectionBuilder->import('routing.yml'); + $routeCollection = $routeCollectionBuilder->build(); + + $this->assertSame(array('number' => '\d'), $routeCollection->getSubroutines()); + } }