Skip to content

Commit ed5d07e

Browse files
authored
Merge pull request #7880 from kenjis/fix-reverse-route-nested-parentheses
fix: reverse routing causes ErrorException
2 parents 546087e + 50cb4b4 commit ed5d07e

File tree

2 files changed

+63
-17
lines changed

2 files changed

+63
-17
lines changed

system/Router/RouteCollection.php

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,11 +1151,13 @@ public function environment(string $env, Closure $callback): RouteCollectionInte
11511151
public function reverseRoute(string $search, ...$params)
11521152
{
11531153
// Named routes get higher priority.
1154-
foreach ($this->routesNames as $collection) {
1154+
foreach ($this->routesNames as $verb => $collection) {
11551155
if (array_key_exists($search, $collection)) {
11561156
$routeKey = $collection[$search];
11571157

1158-
return $this->buildReverseRoute($routeKey, $params);
1158+
$from = $this->routes[$verb][$routeKey]['from'];
1159+
1160+
return $this->buildReverseRoute($from, $params);
11591161
}
11601162
}
11611163

@@ -1171,8 +1173,9 @@ public function reverseRoute(string $search, ...$params)
11711173
// If it's not a named route, then loop over
11721174
// all routes to find a match.
11731175
foreach ($this->routes as $collection) {
1174-
foreach ($collection as $routeKey => $route) {
1175-
$to = $route['handler'];
1176+
foreach ($collection as $route) {
1177+
$to = $route['handler'];
1178+
$from = $route['from'];
11761179

11771180
// ignore closures
11781181
if (! is_string($to)) {
@@ -1196,7 +1199,7 @@ public function reverseRoute(string $search, ...$params)
11961199
continue;
11971200
}
11981201

1199-
return $this->buildReverseRoute($routeKey, $params);
1202+
return $this->buildReverseRoute($from, $params);
12001203
}
12011204
}
12021205

@@ -1311,21 +1314,21 @@ protected function fillRouteParams(string $from, ?array $params = null): string
13111314
* @param array $params One or more parameters to be passed to the route.
13121315
* The last parameter allows you to set the locale.
13131316
*/
1314-
protected function buildReverseRoute(string $routeKey, array $params): string
1317+
protected function buildReverseRoute(string $from, array $params): string
13151318
{
13161319
$locale = null;
13171320

13181321
// Find all of our back-references in the original route
1319-
preg_match_all('/\(([^)]+)\)/', $routeKey, $matches);
1322+
preg_match_all('/\(([^)]+)\)/', $from, $matches);
13201323

13211324
if (empty($matches[0])) {
1322-
if (strpos($routeKey, '{locale}') !== false) {
1325+
if (strpos($from, '{locale}') !== false) {
13231326
$locale = $params[0] ?? null;
13241327
}
13251328

1326-
$routeKey = $this->replaceLocale($routeKey, $locale);
1329+
$from = $this->replaceLocale($from, $locale);
13271330

1328-
return '/' . ltrim($routeKey, '/');
1331+
return '/' . ltrim($from, '/');
13291332
}
13301333

13311334
// Locale is passed?
@@ -1336,25 +1339,31 @@ protected function buildReverseRoute(string $routeKey, array $params): string
13361339

13371340
// Build our resulting string, inserting the $params in
13381341
// the appropriate places.
1339-
foreach ($matches[0] as $index => $pattern) {
1342+
foreach ($matches[0] as $index => $placeholder) {
13401343
if (! isset($params[$index])) {
13411344
throw new InvalidArgumentException(
1342-
'Missing argument for "' . $pattern . '" in route "' . $routeKey . '".'
1345+
'Missing argument for "' . $placeholder . '" in route "' . $from . '".'
13431346
);
13441347
}
1348+
1349+
// Remove `(:` and `)` when $placeholder is a placeholder.
1350+
$placeholderName = substr($placeholder, 2, -1);
1351+
// or maybe $placeholder is not a placeholder, but a regex.
1352+
$pattern = $this->placeholders[$placeholderName] ?? $placeholder;
1353+
13451354
if (! preg_match('#^' . $pattern . '$#u', $params[$index])) {
13461355
throw RouterException::forInvalidParameterType();
13471356
}
13481357

13491358
// Ensure that the param we're inserting matches
13501359
// the expected param type.
1351-
$pos = strpos($routeKey, $pattern);
1352-
$routeKey = substr_replace($routeKey, $params[$index], $pos, strlen($pattern));
1360+
$pos = strpos($from, $placeholder);
1361+
$from = substr_replace($from, $params[$index], $pos, strlen($placeholder));
13531362
}
13541363

1355-
$routeKey = $this->replaceLocale($routeKey, $locale);
1364+
$from = $this->replaceLocale($from, $locale);
13561365

1357-
return '/' . ltrim($routeKey, '/');
1366+
return '/' . ltrim($from, '/');
13581367
}
13591368

13601369
/**

tests/system/Helpers/URLHelper/MiscUrlTest.php

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -906,13 +906,50 @@ public function testUrlToWithSupportedLocaleInRoute(): void
906906
);
907907
}
908908

909+
public function testUrlToWithNamedRouteWithNestedParentheses(): void
910+
{
911+
Services::createRequest(new App());
912+
$routes = service('routes');
913+
914+
// The route will be:
915+
// docs/(master|\d+\.(?:\d+|x))/([a-z0-9-]+)
916+
$routes->addPlaceholder([
917+
'version' => 'master|\d+\.(?:\d+|x)',
918+
'page' => '[a-z0-9-]+',
919+
]);
920+
$routes->get('docs/(:version)/(:page)', static function () {
921+
echo 'Test the documentation segment';
922+
}, ['as' => 'docs.version']);
923+
924+
$this->assertSame(
925+
'http://example.com/index.php/docs/10.9/install',
926+
url_to('docs.version', '10.9', 'install')
927+
);
928+
}
929+
930+
public function testUrlToWithRouteWithNestedParentheses(): void
931+
{
932+
Services::createRequest(new App());
933+
$routes = service('routes');
934+
935+
// The route will be:
936+
// images/(^.*\.(?:jpg|png)$)
937+
$routes->addPlaceholder('imgFileExt', '^.*\.(?:jpg|png)$');
938+
$routes->get('images/(:imgFileExt)', 'Images::getFile/$1');
939+
940+
$this->assertSame(
941+
'http://example.com/index.php/images/test.jpg',
942+
url_to('Images::getFile', 'test.jpg')
943+
);
944+
}
945+
909946
/**
910947
* @see https://github.com/codeigniter4/CodeIgniter4/issues/7651
911948
*/
912949
public function testUrlToMissingArgument(): void
913950
{
914951
$this->expectException(InvalidArgumentException::class);
915-
$this->expectExceptionMessage('Missing argument for "([a-zA-Z]+)" in route "([a-zA-Z]+)/login".');
952+
$this->expectExceptionMessage('Missing argument for "(:alpha)" in route "(:alpha)/login".');
916953

917954
$routes = Services::routes();
918955
$routes->group('(:alpha)', static function ($routes): void {

0 commit comments

Comments
 (0)