* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Routing\Tests; use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCompiler; class RouteCompilerTest extends TestCase { /** * @dataProvider provideCompileData */ public function testCompile($name, $arguments, $prefix, $regex, $variables, $tokens) { $r = new \ReflectionClass(Route::class); $route = $r->newInstanceArgs($arguments); $compiled = $route->compile(); $this->assertEquals($prefix, $compiled->getStaticPrefix(), $name.' (static prefix)'); $this->assertEquals($regex, $compiled->getRegex(), $name.' (regex)'); $this->assertEquals($variables, $compiled->getVariables(), $name.' (variables)'); $this->assertEquals($tokens, $compiled->getTokens(), $name.' (tokens)'); } public static function provideCompileData() { return [ [ 'Static route', ['/foo'], '/foo', '{^/foo$}sD', [], [ ['text', '/foo'], ], ], [ 'Route with a variable', ['/foo/{bar}'], '/foo', '{^/foo/(?P[^/]++)$}sD', ['bar'], [ ['variable', '/', '[^/]++', 'bar'], ['text', '/foo'], ], ], [ 'Route with a variable that has a default value', ['/foo/{bar}', ['bar' => 'bar']], '/foo', '{^/foo(?:/(?P[^/]++))?$}sD', ['bar'], [ ['variable', '/', '[^/]++', 'bar'], ['text', '/foo'], ], ], [ 'Route with several variables', ['/foo/{bar}/{foobar}'], '/foo', '{^/foo/(?P[^/]++)/(?P[^/]++)$}sD', ['bar', 'foobar'], [ ['variable', '/', '[^/]++', 'foobar'], ['variable', '/', '[^/]++', 'bar'], ['text', '/foo'], ], ], [ 'Route with several variables that have default values', ['/foo/{bar}/{foobar}', ['bar' => 'bar', 'foobar' => '']], '/foo', '{^/foo(?:/(?P[^/]++)(?:/(?P[^/]++))?)?$}sD', ['bar', 'foobar'], [ ['variable', '/', '[^/]++', 'foobar'], ['variable', '/', '[^/]++', 'bar'], ['text', '/foo'], ], ], [ 'Route with several variables but some of them have no default values', ['/foo/{bar}/{foobar}', ['bar' => 'bar']], '/foo', '{^/foo/(?P[^/]++)/(?P[^/]++)$}sD', ['bar', 'foobar'], [ ['variable', '/', '[^/]++', 'foobar'], ['variable', '/', '[^/]++', 'bar'], ['text', '/foo'], ], ], [ 'Route with an optional variable as the first segment', ['/{bar}', ['bar' => 'bar']], '', '{^/(?P[^/]++)?$}sD', ['bar'], [ ['variable', '/', '[^/]++', 'bar'], ], ], [ 'Route with a requirement of 0', ['/{bar}', ['bar' => null], ['bar' => '0']], '', '{^/(?P0)?$}sD', ['bar'], [ ['variable', '/', '0', 'bar'], ], ], [ 'Route with an optional variable as the first segment with requirements', ['/{bar}', ['bar' => 'bar'], ['bar' => '(foo|bar)']], '', '{^/(?P(?:foo|bar))?$}sD', ['bar'], [ ['variable', '/', '(?:foo|bar)', 'bar'], ], ], [ 'Route with only optional variables', ['/{foo}/{bar}', ['foo' => 'foo', 'bar' => 'bar']], '', '{^/(?P[^/]++)?(?:/(?P[^/]++))?$}sD', ['foo', 'bar'], [ ['variable', '/', '[^/]++', 'bar'], ['variable', '/', '[^/]++', 'foo'], ], ], [ 'Route with a variable in last position', ['/foo-{bar}'], '/foo-', '{^/foo\-(?P[^/]++)$}sD', ['bar'], [ ['variable', '-', '[^/]++', 'bar'], ['text', '/foo'], ], ], [ 'Route with nested placeholders', ['/{static{var}static}'], '/{static', '{^/\{static(?P[^/]+)static\}$}sD', ['var'], [ ['text', 'static}'], ['variable', '', '[^/]+', 'var'], ['text', '/{static'], ], ], [ 'Route without separator between variables', ['/{w}{x}{y}{z}.{_format}', ['z' => 'default-z', '_format' => 'html'], ['y' => '(y|Y)']], '', '{^/(?P[^/\.]+)(?P[^/\.]+)(?P(?:y|Y))(?:(?P[^/\.]++)(?:\.(?P<_format>[^/]++))?)?$}sD', ['w', 'x', 'y', 'z', '_format'], [ ['variable', '.', '[^/]++', '_format'], ['variable', '', '[^/\.]++', 'z'], ['variable', '', '(?:y|Y)', 'y'], ['variable', '', '[^/\.]+', 'x'], ['variable', '/', '[^/\.]+', 'w'], ], ], [ 'Route with a format', ['/foo/{bar}.{_format}'], '/foo', '{^/foo/(?P[^/\.]++)\.(?P<_format>[^/]++)$}sD', ['bar', '_format'], [ ['variable', '.', '[^/]++', '_format'], ['variable', '/', '[^/\.]++', 'bar'], ['text', '/foo'], ], ], [ 'Static non UTF-8 route', ["/fo\xE9"], "/fo\xE9", "{^/fo\xE9$}sD", [], [ ['text', "/fo\xE9"], ], ], [ 'Route with an explicit UTF-8 requirement', ['/{bar}', ['bar' => null], ['bar' => '.'], ['utf8' => true]], '', '{^/(?P.)?$}sDu', ['bar'], [ ['variable', '/', '.', 'bar', true], ], ], ]; } /** * @dataProvider provideCompileImplicitUtf8Data */ public function testCompileImplicitUtf8Data($name, $arguments, $prefix, $regex, $variables, $tokens, $deprecationType) { $this->expectException(\LogicException::class); $r = new \ReflectionClass(Route::class); $route = $r->newInstanceArgs($arguments); $compiled = $route->compile(); $this->assertEquals($prefix, $compiled->getStaticPrefix(), $name.' (static prefix)'); $this->assertEquals($regex, $compiled->getRegex(), $name.' (regex)'); $this->assertEquals($variables, $compiled->getVariables(), $name.' (variables)'); $this->assertEquals($tokens, $compiled->getTokens(), $name.' (tokens)'); } public static function provideCompileImplicitUtf8Data() { return [ [ 'Static UTF-8 route', ['/foé'], '/foé', '{^/foé$}sDu', [], [ ['text', '/foé'], ], 'patterns', ], [ 'Route with an implicit UTF-8 requirement', ['/{bar}', ['bar' => null], ['bar' => 'é']], '', '{^/(?Pé)?$}sDu', ['bar'], [ ['variable', '/', 'é', 'bar', true], ], 'requirements', ], [ 'Route with a UTF-8 class requirement', ['/{bar}', ['bar' => null], ['bar' => '\pM']], '', '{^/(?P\pM)?$}sDu', ['bar'], [ ['variable', '/', '\pM', 'bar', true], ], 'requirements', ], [ 'Route with a UTF-8 separator', ['/foo/{bar}§{_format}', [], [], ['compiler_class' => Utf8RouteCompiler::class]], '/foo', '{^/foo/(?P[^/§]++)§(?P<_format>[^/]++)$}sDu', ['bar', '_format'], [ ['variable', '§', '[^/]++', '_format', true], ['variable', '/', '[^/§]++', 'bar', true], ['text', '/foo'], ], 'patterns', ], ]; } public function testRouteWithSameVariableTwice() { $this->expectException(\LogicException::class); $route = new Route('/{name}/{name}'); $route->compile(); } public function testRouteCharsetMismatch() { $this->expectException(\LogicException::class); $route = new Route("/\xE9/{bar}", [], ['bar' => '.'], ['utf8' => true]); $route->compile(); } public function testRequirementCharsetMismatch() { $this->expectException(\LogicException::class); $route = new Route('/foo/{bar}', [], ['bar' => "\xE9"], ['utf8' => true]); $route->compile(); } public function testRouteWithFragmentAsPathParameter() { $this->expectException(\InvalidArgumentException::class); $route = new Route('/{_fragment}'); $route->compile(); } /** * @dataProvider getVariableNamesStartingWithADigit */ public function testRouteWithVariableNameStartingWithADigit($name) { $this->expectException(\DomainException::class); $route = new Route('/{'.$name.'}'); $route->compile(); } public static function getVariableNamesStartingWithADigit() { return [ ['09'], ['123'], ['1e2'], ]; } /** * @dataProvider provideCompileWithHostData */ public function testCompileWithHost($name, $arguments, $prefix, $regex, $variables, $pathVariables, $tokens, $hostRegex, $hostVariables, $hostTokens) { $r = new \ReflectionClass(Route::class); $route = $r->newInstanceArgs($arguments); $compiled = $route->compile(); $this->assertEquals($prefix, $compiled->getStaticPrefix(), $name.' (static prefix)'); $this->assertEquals($regex, str_replace(["\n", ' '], '', $compiled->getRegex()), $name.' (regex)'); $this->assertEquals($variables, $compiled->getVariables(), $name.' (variables)'); $this->assertEquals($pathVariables, $compiled->getPathVariables(), $name.' (path variables)'); $this->assertEquals($tokens, $compiled->getTokens(), $name.' (tokens)'); $this->assertEquals($hostRegex, str_replace(["\n", ' '], '', $compiled->getHostRegex()), $name.' (host regex)'); $this->assertEquals($hostVariables, $compiled->getHostVariables(), $name.' (host variables)'); $this->assertEquals($hostTokens, $compiled->getHostTokens(), $name.' (host tokens)'); } public static function provideCompileWithHostData() { return [ [ 'Route with host pattern', ['/hello', [], [], [], 'www.example.com'], '/hello', '{^/hello$}sD', [], [], [ ['text', '/hello'], ], '{^www\.example\.com$}sDi', [], [ ['text', 'www.example.com'], ], ], [ 'Route with host pattern and some variables', ['/hello/{name}', [], [], [], 'www.example.{tld}'], '/hello', '{^/hello/(?P[^/]++)$}sD', ['tld', 'name'], ['name'], [ ['variable', '/', '[^/]++', 'name'], ['text', '/hello'], ], '{^www\.example\.(?P[^\.]++)$}sDi', ['tld'], [ ['variable', '.', '[^\.]++', 'tld'], ['text', 'www.example'], ], ], [ 'Route with variable at beginning of host', ['/hello', [], [], [], '{locale}.example.{tld}'], '/hello', '{^/hello$}sD', ['locale', 'tld'], [], [ ['text', '/hello'], ], '{^(?P[^\.]++)\.example\.(?P[^\.]++)$}sDi', ['locale', 'tld'], [ ['variable', '.', '[^\.]++', 'tld'], ['text', '.example'], ['variable', '', '[^\.]++', 'locale'], ], ], [ 'Route with host variables that has a default value', ['/hello', ['locale' => 'a', 'tld' => 'b'], [], [], '{locale}.example.{tld}'], '/hello', '{^/hello$}sD', ['locale', 'tld'], [], [ ['text', '/hello'], ], '{^(?P[^\.]++)\.example\.(?P[^\.]++)$}sDi', ['locale', 'tld'], [ ['variable', '.', '[^\.]++', 'tld'], ['text', '.example'], ['variable', '', '[^\.]++', 'locale'], ], ], ]; } public function testRouteWithTooLongVariableName() { $this->expectException(\DomainException::class); $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}', [], ['foo' => $requirement]); $this->assertSame($regex, $route->compile()->getRegex()); } public static function provideRemoveCapturingGroup() { yield ['{^/(?Pa(?:b|c)(?:d|e)f)$}sD', 'a(b|c)(d|e)f']; yield ['{^/(?Pa\(b\)c)$}sD', 'a\(b\)c']; yield ['{^/(?P(?:b))$}sD', '(?:b)']; yield ['{^/(?P(?(b)b))$}sD', '(?(b)b)']; yield ['{^/(?P(*F))$}sD', '(*F)']; yield ['{^/(?P(?:(?:foo)))$}sD', '((foo))']; } } class Utf8RouteCompiler extends RouteCompiler { public const SEPARATORS = '/§'; }