8000 [Routing] Redirect from trailing slash to no-slash when possible · symfony/symfony@94ca7d9 · GitHub
[go: up one dir, main page]

Skip to content
Sign in

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 94ca7d9

Browse files
[Routing] Redirect from trailing slash to no-slash when possible
1 parent a38cbd0 commit 94ca7d9

20 files changed

+375
-393
lines changed

src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php

Lines changed: 68 additions & 89 deletions
Large diffs are not rendered by default.

src/Symfony/Component/Routing/Matcher/Dumper/StaticPrefixCollection.php

Lines changed: 0 additions & 6 deletions
< F438 th scope="col">Diff line number
Original file line numberDiff line change
@@ -180,12 +180,6 @@ private function getCommonPrefix(string $prefix, string $anotherPrefix): array
180180
break;
181181
}
182182
}
183-
if (1 < $i && '/' === $prefix[$i - 1]) {
184-
--$i;
185-
}
186-
if (null !== $staticLength && 1 < $staticLength && '/' === $prefix[$staticLength - 1]) {
187-
--$staticLength;
188-
}
189183

190184
return array(substr($prefix, 0, $i), substr($prefix, 0, $staticLength ?? $i));
191185
}

src/Symfony/Component/Routing/Matcher/RedirectableUrlMatcher.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,21 @@ abstract class RedirectableUrlMatcher extends UrlMatcher implements Redirectable
2525
public function match($pathinfo)
2626
{
2727
try {
28-
$parameters = parent::match($pathinfo);
28+
return parent::match($pathinfo);
2929
} catch (ResourceNotFoundException $e) {
30-
if ('/' === substr($pathinfo, -1) || !in_array($this->context->getMethod(), array('HEAD', 'GET'))) {
30+
if (!\in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
3131
throw $e;
3232
}
3333

3434
try {
35-
$parameters = parent::match($pathinfo.'/');
35+
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
36+
$ret = parent::match($pathinfo);
3637

37-
return array_replace($parameters, $this->redirect($pathinfo.'/', isset($parameters['_route']) ? $parameters['_route'] : null));
38+
return $this->redirect($pathinfo, $ret['_route'] ?? null) + $ret;
3839
} catch (ResourceNotFoundException $e2) {
3940
throw $e;
4041
}
4142
}
42-
43-
return $parameters;
4443
}
4544

4645
/**

src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher0.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ public function match($rawPathinfo)
1919
{
2020
$allow = array();
2121
$pathinfo = rawurldecode($rawPathinfo);
22-
$trimmedPathinfo = rtrim($pathinfo, '/');
2322
$context = $this->context;
2423
$requestMethod = $canonicalMethod = $context->getMethod();
2524

src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ public function match($rawPathinfo)
1919
{
2020
$allow = array();
2121
$pathinfo = rawurldecode($rawPathinfo);
22-
$trimmedPathinfo = rtrim($pathinfo, '/');
2322
$context = $this->context;
2423
$requestMethod = $canonicalMethod = $context->getMethod();
2524
$host = strtolower($context->getHost());
@@ -82,41 +81,41 @@ public function match($rawPathinfo)
8281
.'|/([^/]++)(*:57)'
8382
.'|head/([^/]++)(*:77)'
8483
.')'
85-
.'|/test/([^/]++)(?'
86-
.'|/(*:103)'
84+
.'|/test/([^/]++)/(?'
85+
.'|(*:103)'
8786
.')'
8887
.'|/([\']+)(*:119)'
89-
.'|/a(?'
90-
.'|/b\'b/([^/]++)(?'
88+
.'|/a/(?'
89+
.'|b\'b/([^/]++)(?'
9190
.'|(*:148)'
9291
.'|(*:156)'
9392
.')'
94-
.'|/(.*)(*:170)'
95-
.'|/b\'b/([^/]++)(?'
96-
.'|(*:194)'
97-
.'|(*:202)'
93+
.'|(.*)(*:169)'
94+
.'|b\'b/([^/]++)(?'
95+
.'|(*:192)'
96+
.'|(*:200)'
9897
.')'
9998
.')'
100-
.'|/multi/hello(?:/([^/]++))?(*:238)'
99+
.'|/multi/hello(?:/([^/]++))?(*:236)'
101100
.'|/([^/]++)/b/([^/]++)(?'
102-
.'|(*:269)'
103-
.'|(*:277)'
101+
.'|(*:267)'
102+
.'|(*:275)'
104103
.')'
105-
.'|/aba/([^/]++)(*:299)'
104+
.'|/aba/([^/]++)(*:297)'
106105
.')|(?i:([^\\.]++)\\.example\\.com)(?'
107106
.'|/route1(?'
108-
.'|3/([^/]++)(*:359)'
109-
.'|4/([^/]++)(*:377)'
107+
.'|3/([^/]++)(*:357)'
108+
.'|4/([^/]++)(*:375)'
110109
.')'
111110
.')|(?i:c\\.example\\.com)(?'
112-
.'|/route15/([^/]++)(*:427)'
111+
.'|/route15/([^/]++)(*:425)'
113112
.')|[^/]*+(?'
114-
.'|/route16/([^/]++)(*:462)'
115-
.'|/a(?'
116-
.'|/a\\.\\.\\.(*:483)'
117-
.'|/b(?'
118-
.'|/([^/]++)(*:505)'
119-
.'|/c/([^/]++)(*:524)'
113+
.'|/route16/([^/]++)(*:460)'
114+
.'|/a/(?'
115+
.'|a\\.\\.\\.(*:481)'
116+
.'|b/(?'
117+
.'|([^/]++)(*:502)'
118+
.'|c/([^/]++)(*:520)'
120119
.')'
121120
.')'
122121
.')'
@@ -167,14 +166,14 @@ public function match($rawPathinfo)
167166
not_foo1:
168167

169168
break;
170-
case 194:
169+
case 192:
171170
$matches = array('foo1' => $matches[1] ?? null);
172171

173172
// foo2
174173
return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo2')), array());
175174

176175
break;
177-
case 269:
176+
case 267:
178177
$matches = array('_locale' => $matches[1] ?? null, 'foo' => $matches[2] ?? null);
179178

180179
// foo3
@@ -188,18 +187,18 @@ public function match($rawPathinfo)
188187
77 => array(array('_route' => 'barhead'), array('foo'), array('GET' => 0), null),
189188
119 => array(array('_route' => 'quoter'), array('quoter'), null, null),
190189
156 => array(array('_route' => 'bar1'), array('bar'), null, null),
191-
170 => array(array('_route' => 'overridden'), array('var'), null, null),
192-
202 => array(array('_route' => 'bar2'), array('bar1'), null, null),
193-
238 => array(array('_route' => 'helloWorld', 'who' => 'World!'), array('who'), null, null),
194-
277 => array(array('_route' => 'bar3'), array('_locale', 'bar'), null, null),
195-
299 => array(array('_route' => 'foo4'), array('foo'), null, null),
196-
359 => array(array('_route' => 'route13'), array('var1', 'name'), null, null),
197-
377 => array(array('_route' => 'route14', 'var1' => 'val'), array('var1', 'name'), null, null),
198-
427 => array(array('_route' => 'route15'), array('name'), null, null),
199-
462 => array(array('_route' => 'route16', 'var1' => 'val'), array('name'), null, null),
200-
483 => array(array('_route' => 'a'), array(), null, null),
201-
505 => array(array('_route' => 'b'), array('var'), null, null),
202-
524 => array(array('_route' => 'c'), array('var'), null, null),
190+
169 => array(array('_route' => 'overridden'), array('var'), null, null),
191+
200 => array(array('_route' => 'bar2'), array('bar1'), null, null),
192+
236 => array(array('_route' => 'helloWorld', 'who' => 'World!'), array('who'), null, null),
193+
275 => array(array('_route' => 'bar3'), array('_locale', 'bar'), null, null),
194+
297 => array(array('_route' => 'foo4'), array('foo'), null, null),
195+
357 => array(array('_route' => 'route13'), array('var1', 'name'), null, null),
196+
375 => array(array('_route' => 'route14', 'var1' => 'val'), array('var1', 'name'), null, null),
197+
425 => array(array('_route' => 'route15'), array('name'), null, null),
198+
460 => array(array('_route' => 'route16', 'var1' => 'val'), array('name'), null, null),
199+
481 => array(array('_route' => 'a'), array(), null, null),
200+
502 => array(array('_route' => 'b'), array('var'), null, null),
201+
520 => array(array('_route' => 'c'), array('var'), null, null),
203202
);
204203

205204
list($ret, $vars, $requiredMethods, $requiredSchemes) = $routes[$m];
@@ -218,7 +217,7 @@ public function match($rawPathinfo)
218217
return $ret;
219218
}
220219

221-
if (524 === $m) {
220+
if (520 === $m) {
222221
break;
223222
}
224223
$regex = substr_replace($regex, 'F', $m - $offset, 1 + strlen($m));

src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher10.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ public function match($rawPathinfo)
1919
{
2020
$allow = array();
2121
$pathinfo = rawurldecode($rawPathinfo);
22-
$trimmedPathinfo = rtrim($pathinfo, '/');
2322
$context = $this->context;
2423
$requestMethod = $canonicalMethod = $context->getMethod();
2524

src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher11.php

Lines changed: 57 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,30 @@ public function __construct(RequestContext $context)
1515
$this->context = $context;
1616
}
1717

18-
public function match($rawPathinfo)
18+
public function match($pathinfo)
19+
{
20+
try {
21+
return $this->doMatch($pathinfo);
22+
} catch (ResourceNotFoundException $e) {
23+
if (!in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
24+
throw $e;
25+
}
26+
27+
try {
28+
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
29+
$ret = $this->doMatch($pathinfo);
30+
31+
return $this->redirect($pathinfo, $ret['_route']) + $ret;
32+
} catch (ResourceNotFoundException $e2) {
33+
throw $e;
34+
}
35+
}
36+
}
37+
38+
private function doMatch($rawPathinfo)
1939
{
2040
$allow = array();
2141
$pathinfo = rawurldecode($rawPathinfo);
22-
$trimmedPathinfo = rtrim($pathinfo, '/');
2342
$context = $this->context;
2443
$requestMethod = $canonicalMethod = $context->getMethod();
2544

@@ -30,32 +49,34 @@ public function match($rawPathinfo)
3049
$matchedPathinfo = $pathinfo;
3150
$regexList = array(
3251
0 => '{^(?'
33-
.'|/(en|fr)(?'
34-
.'|/admin/post(?'
35-
.'|/?(*:34)'
36-
.'|/new(*:45)'
37-
.'|/(\\d+)(?'
38-
.'|(*:61)'
39-
.'|/edit(*:73)'
40-
.'|/delete(*:87)'
52+
.'|/(en|fr)/(?'
53+
.'|admin/post/(?'
54+
.'|(*:33)'
55+
.'|new(*:43)'
56+
.'|(\\d+)(?'
57+
.'|(*:58)'
58+
.'|/(?'
59+
.'|edit(*:73)'
60+
.'|delete(*:86)'
61+
.')'
4162
.')'
4263
.')'
43-
.'|/blog(?'
44-
.'|/?(*:106)'
45-
.'|/rss\\.xml(*:123)'
46-
.'|/p(?'
47-
.'|age/([^/]++)(*:148)'
48-
.'|osts/([^/]++)(*:169)'
64+
.'|blog/(?'
65+
.'|(*:104)'
66+
.'|rss\\.xml(*:120)'
67+
.'|p(?'
68+
.'|age/([^/]++)(*:144)'
69+
.'|osts/([^/]++)(*:165)'
4970
.')'
50-
.'|/comments/(\\d+)/new(*:197)'
51-
.'|/search(*:212)'
71+
.'|comments/(\\d+)/new(*:192)'
72+
.'|search(*:206)'
5273
.')'
53-
.'|/log(?'
54-
.'|in(*:230)'
55-
.'|out(*:241)'
74+
.'|log(?'
75+
.'|in(*:223)'
76+
.'|out(*:234)'
5677
.')'
5778
.')'
58-
.'|/(en|fr)?(*:260)'
79+
.'|/(en|fr)?(*:253)'
5980
.')$}sD',
6081
);
6182

@@ -64,20 +85,20 @@ public function match($rawPathinfo)
6485
switch ($m = (int) $matches['MARK']) {
6586
default:
6687
$routes = array(
67-
34 => array(array('_route' => 'a', '_locale' => 'en'), array('_locale'), null, null, true),
68-
45 => array(array('_route' => 'b', '_locale' => 'en'), array('_locale'), null, null),
69-
61 => array(array('_route' => 'c', '_locale' => 'en'), array('_locale', 'id'), null, null),
88+
33 => array(array('_route' => 'a', '_locale' => 'en'), array('_locale'), null, null),
89+
43 => array(array('_route' => 'b', '_locale' => 'en'), array('_locale'), null, null),
90+
58 => array(array('_route' => 'c', '_locale' => 'en'), array('_locale', 'id'), null, null),
7091
73 => array(array('_route' => 'd', '_locale' => 'en'), array('_locale', 'id'), null, null),
71-
87 => array(array('_route' => 'e', '_locale' => 'en'), array('_locale', 'id'), null, null),
72-
106 => array(array('_route' => 'f', '_locale' => 'en'), array('_locale'), null, null, true),
73-
123 => array(array('_route' => 'g', '_locale' => 'en'), array('_locale'), null, null),
74-
148 => array(array('_route' => 'h', '_locale' => 'en'), array('_locale', 'page'), null, null),
75-
169 => array(array('_route' => 'i', '_locale' => 'en'), array('_locale', 'page'), null, null),
76-
197 => array(array('_route' => 'j', '_locale' => 'en'), array('_locale', 'id'), null, null),
77-
212 => array(array('_route' => 'k', '_locale' => 'en'), array('_locale'), null, null),
78-
230 => array(array('_route' => 'l', '_locale' => 'en'), array('_locale'), null, null),
79-
241 => array(array('_route' => 'm', '_locale' => 'en'), array('_locale'), null, null),
80-
260 => array(array('_route' => 'n', '_locale' => 'en'), array('_locale'), null, null),
92+
86 => array(array('_route' => 'e', '_locale' => 'en'), array('_locale', 'id'), null, null),
93+
104 => array(array('_route' => 'f', '_locale' => 'en'), array('_locale'), null, null),
94+
120 => array(array('_route' => 'g', '_locale' => 'en'), array('_locale'), null, null),
95+
144 => array(array('_route' => 'h', '_locale' => 'en'), array('_locale', 'page'), null, null),
96+
165 => array(array('_route' => 'i', '_locale' => 'en'), array('_locale', 'page'), null, null),
97+
192 => array(array('_route' => 'j', '_locale' => 'en'), array('_locale', 'id'), null, null),
98+
206 => array(array('_route' => 'k', '_locale' => 'en'), array('_locale'), null, null),
99+
223 => array(array('_route' => 'l', '_locale' => 'en'), array('_locale'), null, null),
100+
234 => array(array('_route' => 'm', '_locale' => 'en'), array('_locale'), null, null),
101+
253 => array(array('_route' => 'n', '_locale' => 'en'), array('_locale'), null, null),
81102
);
82103

83104
list($ret, $vars, $requiredMethods, $requiredSchemes) = $routes[$m];
@@ -88,15 +109,6 @@ public function match($rawPathinfo)
88109
}
89110
}
90111

91-
if (empty($routes[$m][4]) || '/' === $pathinfo[-1]) {
92-
// no-op
93-
} elseif ('GET' !== $canonicalMethod) {
94-
$allow['GET'] = 'GET';
95-
break;
96-
} else {
97-
return array_replace($ret, $this->redirect($rawPathinfo.'/', $ret['_route']));
98-
}
99-
100112
if ($requiredSchemes && !isset($requiredSchemes[$context->getScheme()])) {
101113
if ('GET' !== $canonicalMethod) {
102114
$allow['GET'] = 'GET';
@@ -114,7 +126,7 @@ public function match($rawPathinfo)
114126
return $ret;
115127
}
116128

117-
if (260 === $m) {
129+
if (253 === $m) {
118130
break;
119131
}
120132
$regex = substr_replace($regex, 'F', $m - $offset, 1 + strlen($m));

src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher12.php

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ public function match($rawPathinfo)
1919
{
2020
$allow = array();
2121
$pathinfo = rawurldecode($rawPathinfo);
22-
$trimmedPathinfo = rtrim($pathinfo, '/');
2322
$context = $this->context;
2423
$requestMethod = $canonicalMethod = $context->getMethod();
2524

@@ -30,19 +29,19 @@ public function match($rawPathinfo)
3029
$matchedPathinfo = $pathinfo;
3130
$regexList = array(
3231
0 => '{^(?'
33-
.'|/abc([^/]++)(?'
34-
.'|/1(?'
32+
.'|/abc([^/]++)/(?'
33+
.'|1(?'
3534
.'|(*:27)'
3635
.'|0(?'
3736
.'|(*:38)'
3837
.'|0(*:46)'
3938
.')'
4039
.')'
41-
.'|/2(?'
42-
.'|(*:60)'
40+
.'|2(?'
41+
.'|(*:59)'
4342
.'|0(?'
44-
.'|(*:71)'
45-
.'|0(*:79)'
43+
.'|(*:70)'
44+
.'|0(*:78)'
4645
.')'
4746
.')'
4847
.')'
@@ -57,9 +56,9 @@ public function match($rawPathinfo)
5756
27 => array(array('_route' => 'r1'), array('foo'), null, null),
5857
38 => array(array('_route' => 'r10'), array('foo'), null, null),
5958
46 => array(array('_route' => 'r100'), array('foo'), null, null),
60-
60 => array(array('_route' => 'r2'), array('foo'), null, null),
61-
71 => array(array('_route' => 'r20'), array('foo'), null, null),
62-
79 => array(array('_route' => 'r200'), array('foo'), null, null),
59+
59 => array(array('_route' => 'r2'), array('foo'), null, null),
60+
70 => array(array('_route' => 'r20'), array('foo'), null, null),
61+
78 => array(array('_route' => 'r200'), array('foo'), null, null),
6362
);
6463

6564
list($ret, $vars, $requiredMethods, $requiredSchemes) = $routes[$m];
@@ -78,7 +77,7 @@ public function match($rawPathinfo)
7877
return $ret;
7978
}
8079

81-
if (79 === $m) {
80+
if (78 === $m) {
8281
break;
8382
}
8483
$regex = substr_replace($regex, 'F', $m - $offset, 1 + strlen($m));

src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher13.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ public function match($rawPathinfo)
1919
{
2020
$allow = array();
2121
$pathinfo = rawurldecode($rawPathinfo);
22-
$trimmedPathinfo = rtrim($pathinfo, '/');
2322
$context = $this->context;
2423
$requestMethod = $canonicalMethod = $context->getMethod();
2524
$host = strtolower($context->getHost());

0 commit comments

Comments
 (0)
0