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