diff --git a/src/Symfony/Component/Routing/RouteCompiler.php b/src/Symfony/Component/Routing/RouteCompiler.php index 42ddde0077..7a89edd489 100644 --- a/src/Symfony/Component/Routing/RouteCompiler.php +++ b/src/Symfony/Component/Routing/RouteCompiler.php @@ -180,6 +180,7 @@ class RouteCompiler implements RouteCompilerInterface if (!$useUtf8 && $needsUtf8) { throw new \LogicException(sprintf('Cannot mix UTF-8 requirement with non-UTF-8 charset for variable "%s" in pattern "%s".', $varName, $pattern)); } + $regexp = self::transformCapturingGroupsToNonCapturings($regexp); } $tokens[] = array('variable', $isSeparator ? $precedingChar : '', $regexp, $varName); @@ -247,7 +248,7 @@ class RouteCompiler implements RouteCompilerInterface } /** - * Returns the next static character in the Route pattern that will serve as a separator (or the empty string when none available) + * Returns the next static character in the Route pattern that will serve as a separator (or the empty string when none available). */ private static function findNextSeparator(string $pattern, bool $useUtf8): string { @@ -304,4 +305,25 @@ class RouteCompiler implements RouteCompilerInterface } } } + + private static function transformCapturingGroupsToNonCapturings(string $regexp): string + { + for ($i = 0; $i < \strlen($regexp); ++$i) { + if ('\\' === $regexp[$i]) { + ++$i; + continue; + } + if ('(' !== $regexp[$i] || !isset($regexp[$i + 2])) { + continue; + } + if ('*' === $regexp[++$i] || '?' === $regexp[$i]) { + ++$i; + continue; + } + $regexp = substr_replace($regexp, '?:', $i, 0); + $i += 2; + } + + return $regexp; + } } diff --git a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php index a18a84f701..cf7ded2551 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php @@ -587,6 +587,15 @@ class UrlMatcherTest extends TestCase $this->assertEquals(array('_route' => 'b', 'a' => 'é'), $matcher->match('/é')); } + public function testRequirementWithCapturingGroup() + { + $coll = new RouteCollection(); + $coll->add('a', new Route('/{a}/{b}', array(), array('a' => '(a|b)'))); + + $matcher = $this->getUrlMatcher($coll); + $this->assertEquals(array('_route' => 'a', 'a' => 'a', 'b' => 'b'), $matcher->match('/a/b')); + } + protected function getUrlMatcher(RouteCollection $routes, RequestContext $context = null) { return new UrlMatcher($routes, $context ?: new RequestContext()); diff --git a/src/Symfony/Component/Routing/Tests/RouteCompilerTest.php b/src/Symfony/Component/Routing/Tests/RouteCompilerTest.php index d9db127da9..1ce132b4d2 100644 --- a/src/Symfony/Component/Routing/Tests/RouteCompilerTest.php +++ b/src/Symfony/Component/Routing/Tests/RouteCompilerTest.php @@ -110,8 +110,8 @@ class RouteCompilerTest extends TestCase array( 'Route with an optional variable as the first segment with requirements', array('/{bar}', array('bar' => 'bar'), array('bar' => '(foo|bar)')), - '', '#^/(?P(foo|bar))?$#sD', array('bar'), array( - array('variable', '/', '(foo|bar)', 'bar'), + '', '#^/(?P(?:foo|bar))?$#sD', array('bar'), array( + array('variable', '/', '(?:foo|bar)', 'bar'), ), ), @@ -146,10 +146,10 @@ class RouteCompilerTest extends TestCase array( 'Route without separator between variables', array('/{w}{x}{y}{z}.{_format}', array('z' => 'default-z', '_format' => 'html'), array('y' => '(y|Y)')), - '', '#^/(?P[^/\.]+)(?P[^/\.]+)(?P(y|Y))(?:(?P[^/\.]++)(?:\.(?P<_format>[^/]++))?)?$#sD', array('w', 'x', 'y', 'z', '_format'), array( + '', '#^/(?P[^/\.]+)(?P[^/\.]+)(?P(?:y|Y))(?:(?P[^/\.]++)(?:\.(?P<_format>[^/]++))?)?$#sD', array('w', 'x', 'y', 'z', '_format'), array( array('variable', '.', '[^/]++', '_format'), array('variable', '', '[^/\.]++', 'z'), - array('variable', '', '(y|Y)', 'y'), + array('variable', '', '(?:y|Y)', 'y'), array('variable', '', '[^/\.]+', 'x'), array('variable', '/', '[^/\.]+', 'w'), ), @@ -380,6 +380,25 @@ class RouteCompilerTest extends TestCase $route = new Route(sprintf('/{%s}', str_repeat('a', RouteCompiler::VARIABLE_MAXIMUM_LENGTH + 1))); $route->compile(); } + + /** + * @dataProvider provideRemoveCapturingGroup + */ + public function testRemoveCapturingGroup($regex, $requirement) + { + $route = new Route('/{foo}', array(), array('foo' => $requirement)); + + $this->assertSame($regex, $route->compile()->getRegex()); + } + + public function provideRemoveCapturingGroup() + { + yield array('#^/(?Pa(?:b|c)(?:d|e)f)$#sD', 'a(b|c)(d|e)f'); + yield array('#^/(?Pa\(b\)c)$#sD', 'a\(b\)c'); + yield array('#^/(?P(?:b))$#sD', '(?:b)'); + yield array('#^/(?P(?(b)b))$#sD', '(?(b)b)'); + yield array('#^/(?P(*F))$#sD', '(*F)'); + } } class Utf8RouteCompiler extends RouteCompiler