1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\Routing\Tests\Matcher\Dumper; 13 14use PHPUnit\Framework\TestCase; 15use Symfony\Component\Routing\Matcher\Dumper\PhpMatcherDumper; 16use Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface; 17use Symfony\Component\Routing\Matcher\UrlMatcher; 18use Symfony\Component\Routing\RequestContext; 19use Symfony\Component\Routing\Route; 20use Symfony\Component\Routing\RouteCollection; 21 22/** 23 * @group legacy 24 */ 25class PhpMatcherDumperTest extends TestCase 26{ 27 /** 28 * @var string 29 */ 30 private $matcherClass; 31 32 /** 33 * @var string 34 */ 35 private $dumpPath; 36 37 protected function setUp(): void 38 { 39 parent::setUp(); 40 41 $this->matcherClass = uniqid('ProjectUrlMatcher'); 42 $this->dumpPath = sys_get_temp_dir().\DIRECTORY_SEPARATOR.'php_matcher.'.$this->matcherClass.'.php'; 43 } 44 45 protected function tearDown(): void 46 { 47 parent::tearDown(); 48 49 @unlink($this->dumpPath); 50 } 51 52 public function testRedirectPreservesUrlEncoding() 53 { 54 $collection = new RouteCollection(); 55 $collection->add('foo', new Route('/foo:bar/')); 56 57 $class = $this->generateDumpedMatcher($collection, true); 58 59 $matcher = $this->getMockBuilder($class) 60 ->setMethods(['redirect']) 61 ->setConstructorArgs([new RequestContext()]) 62 ->getMock(); 63 64 $matcher->expects($this->once())->method('redirect')->with('/foo%3Abar/', 'foo')->willReturn([]); 65 66 $matcher->match('/foo%3Abar'); 67 } 68 69 /** 70 * @dataProvider getRouteCollections 71 */ 72 public function testDump(RouteCollection $collection, $fixture, $options = []) 73 { 74 $basePath = __DIR__.'/../../Fixtures/dumper/'; 75 76 $dumper = new PhpMatcherDumper($collection); 77 $this->assertStringEqualsFile($basePath.$fixture, $dumper->dump($options), '->dump() correctly dumps routes as optimized PHP code.'); 78 } 79 80 public function getRouteCollections() 81 { 82 /* test case 1 */ 83 84 $collection = new RouteCollection(); 85 86 $collection->add('overridden', new Route('/overridden')); 87 88 // defaults and requirements 89 $collection->add('foo', new Route( 90 '/foo/{bar}', 91 ['def' => 'test'], 92 ['bar' => 'baz|symfony'] 93 )); 94 // method requirement 95 $collection->add('bar', new Route( 96 '/bar/{foo}', 97 [], 98 [], 99 [], 100 '', 101 [], 102 ['GET', 'head'] 103 )); 104 // GET method requirement automatically adds HEAD as valid 105 $collection->add('barhead', new Route( 106 '/barhead/{foo}', 107 [], 108 [], 109 [], 110 '', 111 [], 112 ['GET'] 113 )); 114 // simple 115 $collection->add('baz', new Route( 116 '/test/baz' 117 )); 118 // simple with extension 119 $collection->add('baz2', new Route( 120 '/test/baz.html' 121 )); 122 // trailing slash 123 $collection->add('baz3', new Route( 124 '/test/baz3/' 125 )); 126 // trailing slash with variable 127 $collection->add('baz4', new Route( 128 '/test/{foo}/' 129 )); 130 // trailing slash and method 131 $collection->add('baz5', new Route( 132 '/test/{foo}/', 133 [], 134 [], 135 [], 136 '', 137 [], 138 ['post'] 139 )); 140 // complex name 141 $collection->add('baz.baz6', new Route( 142 '/test/{foo}/', 143 [], 144 [], 145 [], 146 '', 147 [], 148 ['put'] 149 )); 150 // defaults without variable 151 $collection->add('foofoo', new Route( 152 '/foofoo', 153 ['def' => 'test'] 154 )); 155 // pattern with quotes 156 $collection->add('quoter', new Route( 157 '/{quoter}', 158 [], 159 ['quoter' => '[\']+'] 160 )); 161 // space in pattern 162 $collection->add('space', new Route( 163 '/spa ce' 164 )); 165 166 // prefixes 167 $collection1 = new RouteCollection(); 168 $collection1->add('overridden', new Route('/overridden1')); 169 $collection1->add('foo1', (new Route('/{foo}'))->setMethods('PUT')); 170 $collection1->add('bar1', new Route('/{bar}')); 171 $collection1->addPrefix('/b\'b'); 172 $collection2 = new RouteCollection(); 173 $collection2->addCollection($collection1); 174 $collection2->add('overridden', new Route('/{var}', [], ['var' => '.*'])); 175 $collection1 = new RouteCollection(); 176 $collection1->add('foo2', new Route('/{foo1}')); 177 $collection1->add('bar2', new Route('/{bar1}')); 178 $collection1->addPrefix('/b\'b'); 179 $collection2->addCollection($collection1); 180 $collection2->addPrefix('/a'); 181 $collection->addCollection($collection2); 182 183 // overridden through addCollection() and multiple sub-collections with no own prefix 184 $collection1 = new RouteCollection(); 185 $collection1->add('overridden2', new Route('/old')); 186 $collection1->add('helloWorld', new Route('/hello/{who}', ['who' => 'World!'])); 187 $collection2 = new RouteCollection(); 188 $collection3 = new RouteCollection(); 189 $collection3->add('overridden2', new Route('/new')); 190 $collection3->add('hey', new Route('/hey/')); 191 $collection2->addCollection($collection3); 192 $collection1->addCollection($collection2); 193 $collection1->addPrefix('/multi'); 194 $collection->addCollection($collection1); 195 196 // "dynamic" prefix 197 $collection1 = new RouteCollection(); 198 $collection1->add('foo3', new Route('/{foo}')); 199 $collection1->add('bar3', new Route('/{bar}')); 200 $collection1->addPrefix('/b'); 201 $collection1->addPrefix('{_locale}'); 202 $collection->addCollection($collection1); 203 204 // route between collections 205 $collection->add('ababa', new Route('/ababa')); 206 207 // collection with static prefix but only one route 208 $collection1 = new RouteCollection(); 209 $collection1->add('foo4', new Route('/{foo}')); 210 $collection1->addPrefix('/aba'); 211 $collection->addCollection($collection1); 212 213 // prefix and host 214 215 $collection1 = new RouteCollection(); 216 217 $route1 = new Route('/route1', [], [], [], 'a.example.com'); 218 $collection1->add('route1', $route1); 219 220 $route2 = new Route('/c2/route2', [], [], [], 'a.example.com'); 221 $collection1->add('route2', $route2); 222 223 $route3 = new Route('/c2/route3', [], [], [], 'b.example.com'); 224 $collection1->add('route3', $route3); 225 226 $route4 = new Route('/route4', [], [], [], 'a.example.com'); 227 $collection1->add('route4', $route4); 228 229 $route5 = new Route('/route5', [], [], [], 'c.example.com'); 230 $collection1->add('route5', $route5); 231 232 $route6 = new Route('/route6', [], [], [], null); 233 $collection1->add('route6', $route6); 234 235 $collection->addCollection($collection1); 236 237 // host and variables 238 239 $collection1 = new RouteCollection(); 240 241 $route11 = new Route('/route11', [], [], [], '{var1}.example.com'); 242 $collection1->add('route11', $route11); 243 244 $route12 = new Route('/route12', ['var1' => 'val'], [], [], '{var1}.example.com'); 245 $collection1->add('route12', $route12); 246 247 $route13 = new Route('/route13/{name}', [], [], [], '{var1}.example.com'); 248 $collection1->add('route13', $route13); 249 250 $route14 = new Route('/route14/{name}', ['var1' => 'val'], [], [], '{var1}.example.com'); 251 $collection1->add('route14', $route14); 252 253 $route15 = new Route('/route15/{name}', [], [], [], 'c.example.com'); 254 $collection1->add('route15', $route15); 255 256 $route16 = new Route('/route16/{name}', ['var1' => 'val'], [], [], null); 257 $collection1->add('route16', $route16); 258 259 $route17 = new Route('/route17', [], [], [], null); 260 $collection1->add('route17', $route17); 261 262 $collection->addCollection($collection1); 263 264 // multiple sub-collections with a single route and a prefix each 265 $collection1 = new RouteCollection(); 266 $collection1->add('a', new Route('/a...')); 267 $collection2 = new RouteCollection(); 268 $collection2->add('b', new Route('/{var}')); 269 $collection3 = new RouteCollection(); 270 $collection3->add('c', new Route('/{var}')); 271 $collection3->addPrefix('/c'); 272 $collection2->addCollection($collection3); 273 $collection2->addPrefix('/b'); 274 $collection1->addCollection($collection2); 275 $collection1->addPrefix('/a'); 276 $collection->addCollection($collection1); 277 278 /* test case 2 */ 279 280 $redirectCollection = clone $collection; 281 282 // force HTTPS redirection 283 $redirectCollection->add('secure', new Route( 284 '/secure', 285 [], 286 [], 287 [], 288 '', 289 ['https'] 290 )); 291 292 // force HTTP redirection 293 $redirectCollection->add('nonsecure', new Route( 294 '/nonsecure', 295 [], 296 [], 297 [], 298 '', 299 ['http'] 300 )); 301 302 /* test case 3 */ 303 304 $rootprefixCollection = new RouteCollection(); 305 $rootprefixCollection->add('static', new Route('/test')); 306 $rootprefixCollection->add('dynamic', new Route('/{var}')); 307 $rootprefixCollection->addPrefix('rootprefix'); 308 $route = new Route('/with-condition'); 309 $route->setCondition('context.getMethod() == "GET"'); 310 $rootprefixCollection->add('with-condition', $route); 311 312 /* test case 4 */ 313 $headMatchCasesCollection = new RouteCollection(); 314 $headMatchCasesCollection->add('just_head', new Route( 315 '/just_head', 316 [], 317 [], 318 [], 319 '', 320 [], 321 ['HEAD'] 322 )); 323 $headMatchCasesCollection->add('head_and_get', new Route( 324 '/head_and_get', 325 [], 326 [], 327 [], 328 '', 329 [], 330 ['HEAD', 'GET'] 331 )); 332 $headMatchCasesCollection->add('get_and_head', new Route( 333 '/get_and_head', 334 [], 335 [], 336 [], 337 '', 338 [], 339 ['GET', 'HEAD'] 340 )); 341 $headMatchCasesCollection->add('post_and_head', new Route( 342 '/post_and_head', 343 [], 344 [], 345 [], 346 '', 347 [], 348 ['POST', 'HEAD'] 349 )); 350 $headMatchCasesCollection->add('put_and_post', new Route( 351 '/put_and_post', 352 [], 353 [], 354 [], 355 '', 356 [], 357 ['PUT', 'POST'] 358 )); 359 $headMatchCasesCollection->add('put_and_get_and_head', new Route( 360 '/put_and_post', 361 [], 362 [], 363 [], 364 '', 365 [], 366 ['PUT', 'GET', 'HEAD'] 367 )); 368 369 /* test case 5 */ 370 $groupOptimisedCollection = new RouteCollection(); 371 $groupOptimisedCollection->add('a_first', new Route('/a/11')); 372 $groupOptimisedCollection->add('a_second', new Route('/a/22')); 373 $groupOptimisedCollection->add('a_third', new Route('/a/333')); 374 $groupOptimisedCollection->add('a_wildcard', new Route('/{param}')); 375 $groupOptimisedCollection->add('a_fourth', new Route('/a/44/')); 376 $groupOptimisedCollection->add('a_fifth', new Route('/a/55/')); 377 $groupOptimisedCollection->add('a_sixth', new Route('/a/66/')); 378 $groupOptimisedCollection->add('nested_wildcard', new Route('/nested/{param}')); 379 $groupOptimisedCollection->add('nested_a', new Route('/nested/group/a/')); 380 $groupOptimisedCollection->add('nested_b', new Route('/nested/group/b/')); 381 $groupOptimisedCollection->add('nested_c', new Route('/nested/group/c/')); 382 383 $groupOptimisedCollection->add('slashed_a', new Route('/slashed/group/')); 384 $groupOptimisedCollection->add('slashed_b', new Route('/slashed/group/b/')); 385 $groupOptimisedCollection->add('slashed_c', new Route('/slashed/group/c/')); 386 387 /* test case 6 & 7 */ 388 $trailingSlashCollection = new RouteCollection(); 389 $trailingSlashCollection->add('simple_trailing_slash_no_methods', new Route('/trailing/simple/no-methods/', [], [], [], '', [], [])); 390 $trailingSlashCollection->add('simple_trailing_slash_GET_method', new Route('/trailing/simple/get-method/', [], [], [], '', [], ['GET'])); 391 $trailingSlashCollection->add('simple_trailing_slash_HEAD_method', new Route('/trailing/simple/head-method/', [], [], [], '', [], ['HEAD'])); 392 $trailingSlashCollection->add('simple_trailing_slash_POST_method', new Route('/trailing/simple/post-method/', [], [], [], '', [], ['POST'])); 393 $trailingSlashCollection->add('regex_trailing_slash_no_methods', new Route('/trailing/regex/no-methods/{param}/', [], [], [], '', [], [])); 394 $trailingSlashCollection->add('regex_trailing_slash_GET_method', new Route('/trailing/regex/get-method/{param}/', [], [], [], '', [], ['GET'])); 395 $trailingSlashCollection->add('regex_trailing_slash_HEAD_method', new Route('/trailing/regex/head-method/{param}/', [], [], [], '', [], ['HEAD'])); 396 $trailingSlashCollection->add('regex_trailing_slash_POST_method', new Route('/trailing/regex/post-method/{param}/', [], [], [], '', [], ['POST'])); 397 398 $trailingSlashCollection->add('simple_not_trailing_slash_no_methods', new Route('/not-trailing/simple/no-methods', [], [], [], '', [], [])); 399 $trailingSlashCollection->add('simple_not_trailing_slash_GET_method', new Route('/not-trailing/simple/get-method', [], [], [], '', [], ['GET'])); 400 $trailingSlashCollection->add('simple_not_trailing_slash_HEAD_method', new Route('/not-trailing/simple/head-method', [], [], [], '', [], ['HEAD'])); 401 $trailingSlashCollection->add('simple_not_trailing_slash_POST_method', new Route('/not-trailing/simple/post-method', [], [], [], '', [], ['POST'])); 402 $trailingSlashCollection->add('regex_not_trailing_slash_no_methods', new Route('/not-trailing/regex/no-methods/{param}', [], [], [], '', [], [])); 403 $trailingSlashCollection->add('regex_not_trailing_slash_GET_method', new Route('/not-trailing/regex/get-method/{param}', [], [], [], '', [], ['GET'])); 404 $trailingSlashCollection->add('regex_not_trailing_slash_HEAD_method', new Route('/not-trailing/regex/head-method/{param}', [], [], [], '', [], ['HEAD'])); 405 $trailingSlashCollection->add('regex_not_trailing_slash_POST_method', new Route('/not-trailing/regex/post-method/{param}', [], [], [], '', [], ['POST'])); 406 407 /* test case 8 */ 408 $unicodeCollection = new RouteCollection(); 409 $unicodeCollection->add('a', new Route('/{a}', [], ['a' => 'a'], ['utf8' => false])); 410 $unicodeCollection->add('b', new Route('/{a}', [], ['a' => '.'], ['utf8' => true])); 411 $unicodeCollection->add('c', new Route('/{a}', [], ['a' => '.'], ['utf8' => false])); 412 413 /* test case 9 */ 414 $hostTreeCollection = new RouteCollection(); 415 $hostTreeCollection->add('a', (new Route('/'))->setHost('{d}.e.c.b.a')); 416 $hostTreeCollection->add('b', (new Route('/'))->setHost('d.c.b.a')); 417 $hostTreeCollection->add('c', (new Route('/'))->setHost('{e}.e.c.b.a')); 418 419 /* test case 10 */ 420 $chunkedCollection = new RouteCollection(); 421 for ($i = 0; $i < 1000; ++$i) { 422 $h = substr(md5($i), 0, 6); 423 $chunkedCollection->add('_'.$i, new Route('/'.$h.'/{a}/{b}/{c}/'.$h)); 424 } 425 426 /* test case 11 */ 427 $demoCollection = new RouteCollection(); 428 $demoCollection->add('a', new Route('/admin/post/')); 429 $demoCollection->add('b', new Route('/admin/post/new')); 430 $demoCollection->add('c', (new Route('/admin/post/{id}'))->setRequirements(['id' => '\d+'])); 431 $demoCollection->add('d', (new Route('/admin/post/{id}/edit'))->setRequirements(['id' => '\d+'])); 432 $demoCollection->add('e', (new Route('/admin/post/{id}/delete'))->setRequirements(['id' => '\d+'])); 433 $demoCollection->add('f', new Route('/blog/')); 434 $demoCollection->add('g', new Route('/blog/rss.xml')); 435 $demoCollection->add('h', (new Route('/blog/page/{page}'))->setRequirements(['id' => '\d+'])); 436 $demoCollection->add('i', (new Route('/blog/posts/{page}'))->setRequirements(['id' => '\d+'])); 437 $demoCollection->add('j', (new Route('/blog/comments/{id}/new'))->setRequirements(['id' => '\d+'])); 438 $demoCollection->add('k', new Route('/blog/search')); 439 $demoCollection->add('l', new Route('/login')); 440 $demoCollection->add('m', new Route('/logout')); 441 $demoCollection->addPrefix('/{_locale}'); 442 $demoCollection->add('n', new Route('/{_locale}')); 443 $demoCollection->addRequirements(['_locale' => 'en|fr']); 444 $demoCollection->addDefaults(['_locale' => 'en']); 445 446 /* test case 12 */ 447 $suffixCollection = new RouteCollection(); 448 $suffixCollection->add('r1', new Route('abc{foo}/1')); 449 $suffixCollection->add('r2', new Route('abc{foo}/2')); 450 $suffixCollection->add('r10', new Route('abc{foo}/10')); 451 $suffixCollection->add('r20', new Route('abc{foo}/20')); 452 $suffixCollection->add('r100', new Route('abc{foo}/100')); 453 $suffixCollection->add('r200', new Route('abc{foo}/200')); 454 455 /* test case 13 */ 456 $hostCollection = new RouteCollection(); 457 $hostCollection->add('r1', (new Route('abc{foo}'))->setHost('{foo}.exampple.com')); 458 $hostCollection->add('r2', (new Route('abc{foo}'))->setHost('{foo}.exampple.com')); 459 460 return [ 461 [new RouteCollection(), 'url_matcher0.php', []], 462 [$collection, 'url_matcher1.php', []], 463 [$redirectCollection, 'url_matcher2.php', ['base_class' => 'Symfony\Component\Routing\Tests\Fixtures\RedirectableUrlMatcher']], 464 [$rootprefixCollection, 'url_matcher3.php', []], 465 [$headMatchCasesCollection, 'url_matcher4.php', []], 466 [$groupOptimisedCollection, 'url_matcher5.php', ['base_class' => 'Symfony\Component\Routing\Tests\Fixtures\RedirectableUrlMatcher']], 467 [$trailingSlashCollection, 'url_matcher6.php', []], 468 [$trailingSlashCollection, 'url_matcher7.php', ['base_class' => 'Symfony\Component\Routing\Tests\Fixtures\RedirectableUrlMatcher']], 469 [$unicodeCollection, 'url_matcher8.php', []], 470 [$hostTreeCollection, 'url_matcher9.php', []], 471 [$chunkedCollection, 'url_matcher10.php', []], 472 [$demoCollection, 'url_matcher11.php', ['base_class' => 'Symfony\Component\Routing\Tests\Fixtures\RedirectableUrlMatcher']], 473 [$suffixCollection, 'url_matcher12.php', []], 474 [$hostCollection, 'url_matcher13.php', []], 475 ]; 476 } 477 478 private function generateDumpedMatcher(RouteCollection $collection, $redirectableStub = false) 479 { 480 $options = ['class' => $this->matcherClass]; 481 482 if ($redirectableStub) { 483 $options['base_class'] = '\Symfony\Component\Routing\Tests\Matcher\Dumper\RedirectableUrlMatcherStub'; 484 } 485 486 $dumper = new PhpMatcherDumper($collection); 487 $code = $dumper->dump($options); 488 489 file_put_contents($this->dumpPath, $code); 490 include $this->dumpPath; 491 492 return $this->matcherClass; 493 } 494 495 public function testGenerateDumperMatcherWithObject() 496 { 497 $this->expectException('InvalidArgumentException'); 498 $this->expectExceptionMessage('Symfony\Component\Routing\Route cannot contain objects'); 499 $routeCollection = new RouteCollection(); 500 $routeCollection->add('_', new Route('/', [new \stdClass()])); 501 $dumper = new PhpMatcherDumper($routeCollection); 502 $dumper->dump(); 503 } 504} 505 506abstract class RedirectableUrlMatcherStub extends UrlMatcher implements RedirectableUrlMatcherInterface 507{ 508 public function redirect($path, $route, $scheme = null): array 509 { 510 } 511} 512