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