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());
+ }
}