@@ -29,6 +29,7 @@ class PhpMatcherDumper extends MatcherDumper
29
29
{
30
30
private $ expressionLanguage ;
31
31
private $ signalingException ;
32
+ private $ supportsRedirections ;
32
33
33
34
/**
34
35
* @var ExpressionFunctionProviderInterface[]
@@ -56,7 +57,7 @@ public function dump(array $options = array())
56
57
57
58
// trailing slash support is only enabled if we know how to redirect the user
58
59
$ interfaces = class_implements ($ options ['base_class ' ]);
59
- $ supportsRedirections = isset ($ interfaces [RedirectableUrlMatcherInterface::class]);
60
+ $ this -> supportsRedirections = isset ($ interfaces [RedirectableUrlMatcherInterface::class]);
60
61
61
62
return <<<EOF
62
63
<?php
@@ -76,7 +77,7 @@ public function __construct(RequestContext \$context)
76
77
\$this->context = \$context;
77
78
}
78
79
79
- {$ this ->generateMatchMethod ($ supportsRedirections )}
80
+ {$ this ->generateMatchMethod ()}
80
81
}
81
82
82
83
EOF ;
@@ -90,7 +91,7 @@ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterfac
90
91
/**
91
92
* Generates the code for the match method implementing UrlMatcherInterface.
92
93
*/
93
- private function generateMatchMethod (bool $ supportsRedirections ): string
94
+ private function generateMatchMethod (): string
94
95
{
95
96
// Group hosts by same-suffix, re-order when possible
96
97
$ matchHost = false ;
@@ -111,7 +112,7 @@ private function generateMatchMethod(bool $supportsRedirections): string
111
112
$ code = <<<EOF
112
113
{
113
114
\$allow = \$allowSchemes = array();
114
- \$pathinfo = rawurldecode( \$rawPathinfo);
115
+ \$pathinfo = rawurldecode( \$rawPathinfo) ?: '/' ;
115
116
\$context = \$this->context;
116
117
\$requestMethod = \$canonicalMethod = \$context->getMethod();
117
118
{$ fetchHost }
@@ -123,7 +124,7 @@ private function generateMatchMethod(bool $supportsRedirections): string
123
124
124
125
EOF ;
125
126
126
- if ($ supportsRedirections ) {
127
+ if ($ this -> supportsRedirections ) {
127
128
return <<<'EOF'
128
129
public function match($pathinfo)
129
130
{
@@ -213,9 +214,18 @@ private function groupStaticRoutes(RouteCollection $collection): array
213
214
$ compiledRoute = $ route ->compile ();
214
215
$ hostRegex = $ compiledRoute ->getHostRegex ();
215
216
$ regex = $ compiledRoute ->getRegex ();
217
+ if ($ hasTrailingSlash = '/ ' !== $ route ->getPath ()) {
218
+ $ pos = strrpos ($ regex , '$ ' );
219
+ $ hasTrailingSlash = '/ ' === $ regex [$ pos - 1 ];
220
+ $ regex = substr_replace ($ regex , '/?$ ' , $ pos - $ hasTrailingSlash , 1 + $ hasTrailingSlash );
221
+ }
222
+
216
223
if (!$ compiledRoute ->getPathVariables ()) {
217
224
$ host = !$ compiledRoute ->getHostVariables () ? $ route ->getHost () : '' ;
218
225
$ url = $ route ->getPath ();
226
+ if ($ hasTrailingSlash ) {
227
+ $ url = substr ($ url , 0 , -1 );
228
+ }
219
229
foreach ($ dynamicRegex as list ($ hostRx , $ rx )) {
220
230
if (preg_match ($ rx , $ url ) && (!$ host || !$ hostRx || preg_match ($ hostRx , $ host ))) {
221
231
$ dynamicRegex [] = array ($ hostRegex , $ regex );
@@ -224,7 +234,7 @@ private function groupStaticRoutes(RouteCollection $collection): array
224
234
}
225
235
}
226
236
227
- $ staticRoutes [$ url ][$ name ] = $ route ;
237
+ $ staticRoutes [$ url ][$ name ] = array ( $ route, $ hasTrailingSlash ) ;
228
238
} else {
229
239
$ dynamicRegex [] = array ($ hostRegex , $ regex );
230
240
$ dynamicRoutes ->add ($ name , $ route );
@@ -251,7 +261,7 @@ private function compileStaticRoutes(array $staticRoutes, bool $matchHost): stri
251
261
252
262
foreach ($ staticRoutes as $ url => $ routes ) {
253
263
if (1 === \count ($ routes )) {
254
- foreach ($ routes as $ name => $ route ) {
264
+ foreach ($ routes as $ name => list ( $ route, $ hasTrailingSlash ) ) {
255
265
}
256
266
257
267
if (!$ route ->getCondition ()) {
@@ -261,20 +271,21 @@ private function compileStaticRoutes(array $staticRoutes, bool $matchHost): stri
261
271
unset($ defaults ['_canonical_route ' ]);
262
272
}
263
273
$ default .= sprintf (
264
- "%s => array(%s, %s, %s, %s), \n" ,
274
+ "%s => array(%s, %s, %s, %s, %s ), \n" ,
265
275
self ::export ($ url ),
266
276
self ::export (array ('_route ' => $ name ) + $ defaults ),
267
277
self ::export (!$ route ->compile ()->getHostVariables () ? $ route ->getHost () : $ route ->compile ()->getHostRegex () ?: null ),
268
278
self ::export (array_flip ($ route ->getMethods ()) ?: null ),
269
- self ::export (array_flip ($ route ->getSchemes ()) ?: null )
279
+ self ::export (array_flip ($ route ->getSchemes ()) ?: null ),
280
+ self ::export ($ hasTrailingSlash )
270
281
);
271
282
continue ;
272
283
}
273
284
}
274
285
275
286
$ code .= sprintf (" case %s: \n" , self ::export ($ url ));
276
- foreach ($ routes as $ name => $ route ) {
277
- $ code .= $ this ->compileRoute ($ route , $ name , true );
287
+ foreach ($ routes as $ name => list ( $ route, $ hasTrailingSlash ) ) {
288
+ $ code .= $ this ->compileRoute ($ route , $ name , true , $ hasTrailingSlash );
278
289
}
279
290
$ code .= " break; \n" ;
280
291
}
@@ -285,15 +296,15 @@ private function compileStaticRoutes(array $staticRoutes, bool $matchHost): stri
285
296
\$routes = array(
286
297
{$ this ->indent ($ default , 4 )} );
287
298
288
- if (!isset( \$routes[ \$pathinfo ])) {
299
+ if (!isset( \$routes[ \$trimmedPathinfo ])) {
289
300
break;
290
301
}
291
- list( \$ret, \$requiredHost, \$requiredMethods, \$requiredSchemes) = \$routes[ \$pathinfo ];
302
+ list( \$ret, \$requiredHost, \$requiredMethods, \$requiredSchemes, \$ hasTrailingSlash ) = \$routes[ \$trimmedPathinfo ];
292
303
{$ this ->compileSwitchDefault (false , $ matchHost )}
293
304
EOF ;
294
305
}
295
306
296
- return sprintf (" switch ( \$pathinfo) { \n%s } \n\n" , $ this ->indent ($ code ));
307
+ return sprintf (" switch ( \$trimmedPathinfo = '/' !== \$ pathinfo && '/' === \$ pathinfo[-1] ? substr( \$ pathinfo, 0, -1) : \$ pathinfo) { \n%s } \n\n" , $ this ->indent ($ code ));
297
308
}
298
309
299
310
/**
@@ -394,7 +405,11 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $matchHo
394
405
395
406
$ state ->vars = array ();
396
407
$ regex = preg_replace_callback ('#\?P<([^>]++)># ' , $ state ->getVars , $ rx [1 ]);
397
- $ tree ->addRoute ($ regex , array ($ name , $ regex , $ state ->vars , $ route ));
408
+ if ($ hasTrailingSlash = '/ ' !== $ regex && '/ ' === $ regex [-1 ]) {
409
+ $ regex = substr ($ regex , 0 , -1 );
410
+ }
411
+
412
+ $ tree ->addRoute ($ regex , array ($ name , $ regex , $ state ->vars , $ route , $ hasTrailingSlash ));
398
413
}
399
414
400
415
$ code .= $ this ->compileStaticPrefixCollection ($ tree , $ state );
@@ -403,7 +418,7 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $matchHo
403
418
$ code .= "\n .')' " ;
404
419
$ state ->regex .= ') ' ;
405
420
}
406
- $ rx = ")$} {$ modifiers }" ;
421
+ $ rx = ")(?:/?) $} {$ modifiers }" ;
407
422
$ code .= "\n .' {$ rx }', " ;
408
423
$ state ->regex .= $ rx ;
409
424
$ state ->markTail = 0 ;
@@ -423,7 +438,7 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $matchHo
423
438
\$routes = array(
424
439
{$ this ->indent ($ state ->default , 4 )} );
425
440
426
- list( \$ret, \$vars, \$requiredMethods, \$requiredSchemes) = \$routes[ \$m];
441
+ list( \$ret, \$vars, \$requiredMethods, \$requiredSchemes, \$ hasTrailingSlash ) = \$routes[ \$m];
427
442
{$ this ->compileSwitchDefault (true , $ matchHost )}
428
443
EOF ;
429
444
}
@@ -478,11 +493,11 @@ private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \st
478
493
continue ;
479
494
}
480
495
481
- list ($ name , $ regex , $ vars , $ route ) = $ route ;
496
+ list ($ name , $ regex , $ vars , $ route, $ hasTrailingSlash ) = $ route ;
482
497
$ compiledRoute = $ route ->compile ();
483
498
484
499
if ($ compiledRoute ->getRegex () === $ prevRegex ) {
485
- $ state ->switch = substr_replace ($ state ->switch , $ this ->compileRoute ($ route , $ name , false )."\n" , -19 , 0 );
500
+ $ state ->switch = substr_replace ($ state ->switch , $ this ->compileRoute ($ route , $ name , false , $ hasTrailingSlash )."\n" , -19 , 0 );
486
501
continue ;
487
502
}
488
503
@@ -501,12 +516,13 @@ private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \st
501
516
unset($ defaults ['_canonical_route ' ]);
502
517
}
503
518
$ state ->default .= sprintf (
504
- "%s => array(%s, %s, %s, %s), \n" ,
519
+ "%s => array(%s, %s, %s, %s, %s ), \n" ,
505
520
$ state ->mark ,
506
521
self ::export (array ('_route ' => $ name ) + $ defaults ),
507
522
self ::export ($ vars ),
508
523
self ::export (array_flip ($ route ->getMethods ()) ?: null ),
509
- self ::export (array_flip ($ route ->getSchemes ()) ?: null )
524
+ self ::export (array_flip ($ route ->getSchemes ()) ?: null ),
525
+ self ::export ($ hasTrailingSlash )
510
526
);
511
527
} else {
512
528
$ prevRegex = $ compiledRoute ->getRegex ();
@@ -518,7 +534,7 @@ private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \st
518
534
519
535
$ state ->switch .= <<<EOF
520
536
case {$ state ->mark }:
521
- {$ combine }{$ this ->compileRoute ($ route , $ name , false )}
537
+ {$ combine }{$ this ->compileRoute ($ route , $ name , false , $ hasTrailingSlash )}
522
538
break;
523
539
524
540
EOF ;
@@ -533,8 +549,15 @@ private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \st
533
549
*/
534
550
private function compileSwitchDefault (bool $ hasVars , bool $ matchHost ): string
535
551
{
552
+ $ code = sprintf ("
553
+ if ('/' !== \$pathinfo && \$hasTrailingSlash !== ('/' === \$pathinfo[-1])) {
554
+ %s;
555
+ } \n" ,
556
+ $ this ->supportsRedirections ? 'return null ' : 'break '
557
+ );
558
+
536
559
if ($ hasVars ) {
537
- $ code = <<<EOF
560
+ $ code . = <<<EOF
538
561
539
562
foreach ( \$vars as \$i => \$v) {
540
563
if (isset( \$matches[1 + \$i])) {
@@ -544,7 +567,7 @@ private function compileSwitchDefault(bool $hasVars, bool $matchHost): string
544
567
545
568
EOF ;
546
569
} elseif ($ matchHost ) {
547
- $ code = <<<EOF
570
+ $ code . = <<<EOF
548
571
549
572
if ( \$requiredHost) {
550
573
if ('#' !== \$requiredHost[0] ? \$requiredHost !== \$host : !preg_match( \$requiredHost, \$host, \$hostMatches)) {
@@ -557,8 +580,6 @@ private function compileSwitchDefault(bool $hasVars, bool $matchHost): string
557
580
}
558
581
559
582
EOF ;
560
- } else {
561
- $ code = '' ;
562
583
}
563
584
564
585
$ code .= <<<EOF
@@ -587,9 +608,22 @@ private function compileSwitchDefault(bool $hasVars, bool $matchHost): string
587
608
*
588
609
* @throws \LogicException
589
610
*/
590
- private function compileRoute (Route $ route , string $ name , bool $ checkHost ): string
611
+ private function compileRoute (Route $ route , string $ name , bool $ checkHost, bool $ hasTrailingSlash ): string
591
612
{
592
- $ code = '' ;
613
+ $ code = " // $ name " ;
614
+
615
+ if ('/ ' !== $ route ->getPath ()) {
616
+ $ code .= sprintf ("
617
+ if ('/' !== \$pathinfo && '/' %s \$pathinfo[-1]) {
618
+ %s;
619
+ } \n" ,
620
+ $ hasTrailingSlash ? '!== ' : '=== ' ,
621
+ $ this ->supportsRedirections ? 'return null ' : 'break '
622
+ );
623
+ } else {
624
+ $ code .= "\n" ;
625
+ }
626
+
593
627
$ compiledRoute = $ route ->compile ();
594
628
$ conditions = array ();
595
629
$ matches = (bool ) $ compiledRoute ->getPathVariables ();
@@ -617,12 +651,11 @@ private function compileRoute(Route $route, string $name, bool $checkHost): stri
617
651
618
652
if ($ conditions ) {
619
653
$ code .= <<<EOF
620
- // $ name
621
654
if ( $ conditions) {
622
655
623
656
EOF ;
624
657
} else {
625
- $ code .= " // { $ name }\n" ;
658
+ $ code = $ this -> indent ( $ code ) ;
626
659
}
627
660
628
661
$ gotoname = 'not_ ' .preg_replace ('/[^A-Za-z0-9_]/ ' , '' , $ name );
0 commit comments