@@ -29,6 +29,7 @@ class PhpMatcherDumper extends MatcherDumper
2929{
3030 private $ expressionLanguage ;
3131 private $ signalingException ;
32+ private $ supportsRedirections ;
3233
3334 /**
3435 * @var ExpressionFunctionProviderInterface[]
@@ -56,7 +57,7 @@ public function dump(array $options = array())
5657
5758 // trailing slash support is only enabled if we know how to redirect the user
5859 $ interfaces = class_implements ($ options ['base_class ' ]);
59- $ supportsRedirections = isset ($ interfaces [RedirectableUrlMatcherInterface::class]);
60+ $ this -> supportsRedirections = isset ($ interfaces [RedirectableUrlMatcherInterface::class]);
6061
6162 return <<<EOF
6263<?php
@@ -76,7 +77,7 @@ public function __construct(RequestContext \$context)
7677 \$this->context = \$context;
7778 }
7879
79- {$ this ->generateMatchMethod ($ supportsRedirections )}
80+ {$ this ->generateMatchMethod ()}
8081}
8182
8283EOF ;
@@ -90,7 +91,7 @@ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterfac
9091 /**
9192 * Generates the code for the match method implementing UrlMatcherInterface.
9293 */
93- private function generateMatchMethod (bool $ supportsRedirections ): string
94+ private function generateMatchMethod (): string
9495 {
9596 // Group hosts by same-suffix, re-order when possible
9697 $ matchHost = false ;
@@ -111,7 +112,7 @@ private function generateMatchMethod(bool $supportsRedirections): string
111112 $ code = <<<EOF
112113 {
113114 \$allow = \$allowSchemes = array();
114- \$pathinfo = rawurldecode( \$rawPathinfo);
115+ \$pathinfo = rawurldecode( \$rawPathinfo) ?: '/' ;
115116 \$context = \$this->context;
116117 \$requestMethod = \$canonicalMethod = \$context->getMethod();
117118{$ fetchHost }
@@ -123,7 +124,7 @@ private function generateMatchMethod(bool $supportsRedirections): string
123124
124125EOF ;
125126
126- if ($ supportsRedirections ) {
127+ if ($ this -> supportsRedirections ) {
127128 return <<<'EOF'
128129 public function match($pathinfo)
129130 {
@@ -213,9 +214,18 @@ private function groupStaticRoutes(RouteCollection $collection): array
213214 $ compiledRoute = $ route ->compile ();
214215 $ hostRegex = $ compiledRoute ->getHostRegex ();
215216 $ 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+
216223 if (!$ compiledRoute ->getPathVariables ()) {
217224 $ host = !$ compiledRoute ->getHostVariables () ? $ route ->getHost () : '' ;
218225 $ url = $ route ->getPath ();
226+ if ($ hasTrailingSlash ) {
227+ $ url = substr ($ url , 0 , -1 );
228+ }
219229 foreach ($ dynamicRegex as list ($ hostRx , $ rx )) {
220230 if (preg_match ($ rx , $ url ) && (!$ host || !$ hostRx || preg_match ($ hostRx , $ host ))) {
221231 $ dynamicRegex [] = array ($ hostRegex , $ regex );
@@ -224,7 +234,7 @@ private function groupStaticRoutes(RouteCollection $collection): array
224234 }
225235 }
226236
227- $ staticRoutes [$ url ][$ name ] = $ route ;
237+ $ staticRoutes [$ url ][$ name ] = array ( $ route, $ hasTrailingSlash ) ;
228238 } else {
229239 $ dynamicRegex [] = array ($ hostRegex , $ regex );
230240 $ dynamicRoutes ->add ($ name , $ route );
@@ -251,7 +261,7 @@ private function compileStaticRoutes(array $staticRoutes, bool $matchHost): stri
251261
252262 foreach ($ staticRoutes as $ url => $ routes ) {
253263 if (1 === \count ($ routes )) {
254- foreach ($ routes as $ name => $ route ) {
264+ foreach ($ routes as $ name => list ( $ route, $ hasTrailingSlash ) ) {
255265 }
256266
257267 if (!$ route ->getCondition ()) {
@@ -261,20 +271,21 @@ private function compileStaticRoutes(array $staticRoutes, bool $matchHost): stri
261271 unset($ defaults ['_canonical_route ' ]);
262272 }
263273 $ default .= sprintf (
264- "%s => array(%s, %s, %s, %s), \n" ,
274+ "%s => array(%s, %s, %s, %s, %s ), \n" ,
265275 self ::export ($ url ),
266276 self ::export (array ('_route ' => $ name ) + $ defaults ),
267277 self ::export (!$ route ->compile ()->getHostVariables () ? $ route ->getHost () : $ route ->compile ()->getHostRegex () ?: null ),
268278 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 )
270281 );
271282 continue ;
272283 }
273284 }
274285
275286 $ 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 );
278289 }
279290 $ code .= " break; \n" ;
280291 }
@@ -285,15 +296,15 @@ private function compileStaticRoutes(array $staticRoutes, bool $matchHost): stri
285296 \$routes = array(
286297{$ this ->indent ($ default , 4 )} );
287298
288- if (!isset( \$routes[ \$pathinfo ])) {
299+ if (!isset( \$routes[ \$trimmedPathinfo ])) {
289300 break;
290301 }
291- list( \$ret, \$requiredHost, \$requiredMethods, \$requiredSchemes) = \$routes[ \$pathinfo ];
302+ list( \$ret, \$requiredHost, \$requiredMethods, \$requiredSchemes, \$ hasTrailingSlash ) = \$routes[ \$trimmedPathinfo ];
292303{$ this ->compileSwitchDefault (false , $ matchHost )}
293304EOF ;
294305 }
295306
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 ));
297308 }
298309
299310 /**
@@ -394,7 +405,11 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $matchHo
394405
395406 $ state ->vars = array ();
396407 $ 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 ));
398413 }
399414
400415 $ code .= $ this ->compileStaticPrefixCollection ($ tree , $ state );
@@ -403,7 +418,7 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $matchHo
403418 $ code .= "\n .')' " ;
404419 $ state ->regex .= ') ' ;
405420 }
406- $ rx = ")$} {$ modifiers }" ;
421+ $ rx = ")(?:/?) $} {$ modifiers }" ;
407422 $ code .= "\n .' {$ rx }', " ;
408423 $ state ->regex .= $ rx ;
409424 $ state ->markTail = 0 ;
@@ -423,7 +438,7 @@ private function compileDynamicRoutes(RouteCollection $collection, bool $matchHo
423438 \$routes = array(
424439{$ this ->indent ($ state ->default , 4 )} );
425440
426- list( \$ret, \$vars, \$requiredMethods, \$requiredSchemes) = \$routes[ \$m];
441+ list( \$ret, \$vars, \$requiredMethods, \$requiredSchemes, \$ hasTrailingSlash ) = \$routes[ \$m];
427442{$ this ->compileSwitchDefault (true , $ matchHost )}
428443EOF ;
429444 }
@@ -478,11 +493,11 @@ private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \st
478493 continue ;
479494 }
480495
481- list ($ name , $ regex , $ vars , $ route ) = $ route ;
496+ list ($ name , $ regex , $ vars , $ route, $ hasTrailingSlash ) = $ route ;
482497 $ compiledRoute = $ route ->compile ();
483498
484499 if ($ compiledRoute ->getRegex () === $ prevRegex ) {
485- $ state ->switch =
38BA
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 );
486501 continue ;
487502 }
488503
@@ -501,12 +516,13 @@ private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \st
501516 unset($ defaults ['_canonical_route ' ]);
502517 }
503518 $ state ->default .= sprintf (
504- "%s => array(%s, %s, %s, %s), \n" ,
519+ "%s => array(%s, %s, %s, %s, %s ), \n" ,
505520 $ state ->mark ,
506521 self ::export (array ('_route ' => $ name ) + $ defaults ),
507522 self ::export ($ vars ),
508523 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 )
510526 );
511527 } else {
512528 $ prevRegex = $ compiledRoute ->getRegex ();
@@ -518,7 +534,7 @@ private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \st
518534
519535 $ state ->switch .= <<<EOF
520536 case {$ state ->mark }:
521- {$ combine }{$ this ->compileRoute ($ route , $ name , false )}
537+ {$ combine }{$ this ->compileRoute ($ route , $ name , false , $ hasTrailingSlash )}
522538 break;
523539
524540EOF ;
@@ -533,8 +549,15 @@ private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \st
533549 */
534550 private function compileSwitchDefault (bool $ hasVars , bool $ matchHost ): string
535551 {
552+ $ code = sprintf ("
553+ if ('/' !== \$pathinfo && \$hasTrailingSlash !== ('/' === \$pathinfo[-1])) {
554+ %s;
555+ } \n" ,
556+ $ this ->supportsRedirections ? 'return null ' : 'break '
557+ );
558+
536559 if ($ hasVars ) {
537- $ code = <<<EOF
560+ $ code . = <<<EOF
538561
539562 foreach ( \$vars as \$i => \$v) {
540563 if (isset( \$matches[1 + \$i])) {
@@ -544,7 +567,7 @@ private function compileSwitchDefault(bool $hasVars, bool $matchHost): string
544567
545568EOF ;
546569 } elseif ($ matchHost ) {
547- $ code = <<<EOF
570+ $ code . = <<<EOF
548571
549572 if ( \$requiredHost) {
550573 if ('#' !== \$requiredHost[0] ? \$requiredHost !== \$host : !preg_match( \$requiredHost, \$host, \$hostMatches)) {
@@ -557,8 +580,6 @@ private function compileSwitchDefault(bool $hasVars, bool $matchHost): string
557580 }
558581
559582EOF ;
560- } else {
561- $ code = '' ;
562583 }
563584
564585 $ code .= <<<EOF
@@ -587,9 +608,22 @@ private function compileSwitchDefault(bool $hasVars, bool $matchHost): string
587608 *
588609 * @throws \LogicException
589610 */
590- private function compileRoute (Route $ route , string $ name , bool $ checkHost ): string
611+ private function compileRoute (Route $ route , string $ name , bool $ checkHost, bool $ hasTrailingSlash ): string
591612 {
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+
593627 $ compiledRoute = $ route ->compile ();
594628 $ conditions = array ();
595629 $ matches = (bool ) $ compiledRoute ->getPathVariables ();
@@ -617,12 +651,11 @@ private function compileRoute(Route $route, string $name, bool $checkHost): stri
617651
618652 if ($ conditions ) {
619653 $ code .= <<<EOF
620- // $ name
621654 if ( $ conditions) {
622655
623656EOF ;
624657 } else {
625- $ code .= " // { $ name }\n" ;
658+ $ code = $ this -> indent ( $ code ) ;
626659 }
627660
628661 $ gotoname = 'not_ ' .preg_replace ('/[^A-Za-z0-9_]/ ' , '' , $ name );
0 commit comments