Skip to content

Commit c02bae9

Browse files
authored
Merge pull request #7175 from kenjis/perf-router
perf: RouteCollection $routes optimization
2 parents d97ee0e + df8556e commit c02bae9

File tree

4 files changed

+125
-56
lines changed

4 files changed

+125
-56
lines changed

system/Router/RouteCollection.php

Lines changed: 112 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,20 @@ class RouteCollection implements RouteCollectionInterface
116116
*
117117
* [
118118
* verb => [
119-
* routeName => [
120-
* 'route' => [
121-
* routeKey(regex) => handler,
122-
* ],
119+
* routeKey(regex) => [
120+
* 'name' => routeName
121+
* 'handler' => handler,
122+
* 'from' => from,
123+
* ],
124+
* ],
125+
* // redirect route
126+
* '*' => [
127+
* routeKey(regex)(from) => [
128+
* 'name' => routeName
129+
* 'handler' => [routeKey(regex)(to) => handler],
130+
* 'from' => from,
123131
* 'redirect' => statusCode,
124-
* ]
132+
* ],
125133
* ],
126134
* ]
127135
*/
@@ -138,6 +146,30 @@ class RouteCollection implements RouteCollectionInterface
138146
'cli' => [],
139147
];
140148

149+
/**
150+
* Array of routes names
151+
*
152+
* @var array
153+
*
154+
* [
155+
* verb => [
156+
* routeName => routeKey(regex)
157+
* ],
158+
* ]
159+
*/
160+
protected $routesNames = [
161+
'*' => [],
162+
'options' => [],
163+
'get' => [],
164+
'head' => [],
165+
'post' => [],
166+
'put' => [],
167+
'delete' => [],
168+
'trace' => [],
169+
'connect' => [],
170+
'cli' => [],
171+
];
172+
141173
/**
142174
* Array of routes options
143175
*
@@ -537,9 +569,8 @@ public function getRoutes(?string $verb = null): array
537569
// before any of the generic, "add" routes.
538570
$collection = $this->routes[$verb] + ($this->routes['*'] ?? []);
539571

540-
foreach ($collection as $r) {
541-
$key = key($r['route']);
542-
$routes[$key] = $r['route'][$key];
572+
foreach ($collection as $routeKey => $r) {
573+
$routes[$routeKey] = $r['handler'];
543574
}
544575
}
545576

@@ -638,42 +669,63 @@ public function add(string $from, $to, ?array $options = null): RouteCollectionI
638669
public function addRedirect(string $from, string $to, int $status = 302)
639670
{
640671
// Use the named route's pattern if this is a named route.
641-
if (array_key_exists($to, $this->routes['*'])) {
642-
$to = $this->routes['*'][$to]['route'];
643-
} elseif (array_key_exists($to, $this->routes['get'])) {
644-
$to = $this->routes['get'][$to]['route'];
672+
if (array_key_exists($to, $this->routesNames['*'])) {
673+
$routeName = $to;
674+
$routeKey = $this->routesNames['*'][$routeName];
675+
$redirectTo = [$routeKey => $this->routes['*'][$routeKey]['handler']];
676+
} elseif (array_key_exists($to, $this->routesNames['get'])) {
677+
$routeName = $to;
678+
$routeKey = $this->routesNames['get'][$routeName];
679+
$redirectTo = [$routeKey => $this->routes['get'][$routeKey]['handler']];
680+
} else {
681+
// The named route is not found.
682+
$redirectTo = $to;
645683
}
646684

647-
$this->create('*', $from, $to, ['redirect' => $status]);
685+
$this->create('*', $from, $redirectTo, ['redirect' => $status]);
648686

649687
return $this;
650688
}
651689

652690
/**
653691
* Determines if the route is a redirecting route.
692+
*
693+
* @param string $routeKey routeKey or route name
654694
*/
655-
public function isRedirect(string $from): bool
695+
public function isRedirect(string $routeKey): bool
656696
{
657-
foreach ($this->routes['*'] as $name => $route) {
658-
// Named route?
659-
if ($name === $from || key($route['route']) === $from) {
660-
return isset($route['redirect']) && is_numeric($route['redirect']);
661-
}
697+
if (isset($this->routes['*'][$routeKey]['redirect'])) {
698+
return true;
699+
}
700+
701+
// This logic is not used. Should be deprecated?
702+
$routeName = $this->routes['*'][$routeKey]['name'] ?? null;
703+
if ($routeName === $routeKey) {
704+
$routeKey = $this->routesNames['*'][$routeName];
705+
706+
return isset($this->routes['*'][$routeKey]['redirect']);
662707
}
663708

664709
return false;
665710
}
666711

667712
/**
668713
* Grabs the HTTP status code from a redirecting Route.
714+
*
715+
* @param string $routeKey routeKey or route name
669716
*/
670-
public function getRedirectCode(string $from): int
717+
public function getRedirectCode(string $routeKey): int
671718
{
672-
foreach ($this->routes['*'] as $name => $route) {
673-
// Named route?
674-
if ($name === $from || key($route['route']) === $from) {
675-
return $route['redirect'] ?? 0;
676-
}
719+
if (isset($this->routes['*'][$routeKey]['redirect'])) {
720+
return $this->routes['*'][$routeKey]['redirect'];
721+
}
722+
723+
// This logic is not used. Should be deprecated?
724+
$routeName = $this->routes['*'][$routeKey]['name'] ?? null;
725+
if ($routeName === $routeKey) {
726+
$routeKey = $this->routesNames['*'][$routeName];
727+
728+
return $this->routes['*'][$routeKey]['redirect'];
677729
}
678730

679731
return 0;
@@ -1088,9 +1140,11 @@ public function environment(string $env, Closure $callback): RouteCollectionInte
10881140
public function reverseRoute(string $search, ...$params)
10891141
{
10901142
// Named routes get higher priority.
1091-
foreach ($this->routes as $collection) {
1143+
foreach ($this->routesNames as $collection) {
10921144
if (array_key_exists($search, $collection)) {
1093-
return $this->buildReverseRoute(key($collection[$search]['route']), $params);
1145+
$routeKey = $collection[$search];
1146+
1147+
return $this->buildReverseRoute($routeKey, $params);
10941148
}
10951149
}
10961150

@@ -1106,9 +1160,8 @@ public function reverseRoute(string $search, ...$params)
11061160
// If it's not a named route, then loop over
11071161
// all routes to find a match.
11081162
foreach ($this->routes as $collection) {
1109-
foreach ($collection as $route) {
1110-
$from = key($route['route']);
1111-
$to = $route['route'][$from];
1163+
foreach ($collection as $routeKey => $route) {
1164+
$to = $route['handler'];
11121165

11131166
// ignore closures
11141167
if (! is_string($to)) {
@@ -1132,7 +1185,7 @@ public function reverseRoute(string $search, ...$params)
11321185
continue;
11331186
}
11341187

1135-
return $this->buildReverseRoute($from, $params);
1188+
return $this->buildReverseRoute($routeKey, $params);
11361189
}
11371190
}
11381191

@@ -1247,21 +1300,21 @@ protected function fillRouteParams(string $from, ?array $params = null): string
12471300
* @param array $params One or more parameters to be passed to the route.
12481301
* The last parameter allows you to set the locale.
12491302
*/
1250-
protected function buildReverseRoute(string $from, array $params): string
1303+
protected function buildReverseRoute(string $routeKey, array $params): string
12511304
{
12521305
$locale = null;
12531306

12541307
// Find all of our back-references in the original route
1255-
preg_match_all('/\(([^)]+)\)/', $from, $matches);
1308+
preg_match_all('/\(([^)]+)\)/', $routeKey, $matches);
12561309

12571310
if (empty($matches[0])) {
1258-
if (strpos($from, '{locale}') !== false) {
1311+
if (strpos($routeKey, '{locale}') !== false) {
12591312
$locale = $params[0] ?? null;
12601313
}
12611314

1262-
$from = $this->replaceLocale($from, $locale);
1315+
$routeKey = $this->replaceLocale($routeKey, $locale);
12631316

1264-
return '/' . ltrim($from, '/');
1317+
return '/' . ltrim($routeKey, '/');
12651318
}
12661319

12671320
// Locale is passed?
@@ -1279,13 +1332,13 @@ protected function buildReverseRoute(string $from, array $params): string
12791332

12801333
// Ensure that the param we're inserting matches
12811334
// the expected param type.
1282-
$pos = strpos($from, $pattern);
1283-
$from = substr_replace($from, $params[$index], $pos, strlen($pattern));
1335+
$pos = strpos($routeKey, $pattern);
1336+
$routeKey = substr_replace($routeKey, $params[$index], $pos, strlen($pattern));
12841337
}
12851338

1286-
$from = $this->replaceLocale($from, $locale);
1339+
$routeKey = $this->replaceLocale($routeKey, $locale);
12871340

1288-
return '/' . ltrim($from, '/');
1341+
return '/' . ltrim($routeKey, '/');
12891342
}
12901343

12911344
/**
@@ -1333,7 +1386,7 @@ protected function create(string $verb, string $from, $to, ?array $options = nul
13331386
}
13341387

13351388
// When redirecting to named route, $to is an array like `['zombies' => '\Zombies::index']`.
1336-
if (is_array($to) && count($to) === 2) {
1389+
if (is_array($to) && isset($to[0])) {
13371390
$to = $this->processArrayCallableSyntax($from, $to);
13381391
}
13391392

@@ -1385,10 +1438,11 @@ protected function create(string $verb, string $from, $to, ?array $options = nul
13851438
}
13861439
}
13871440

1441+
$routeKey = $from;
13881442
// Replace our regex pattern placeholders with the actual thing
13891443
// so that the Router doesn't need to know about any of this.
13901444
foreach ($this->placeholders as $tag => $pattern) {
1391-
$from = str_ireplace(':' . $tag, $pattern, $from);
1445+
$routeKey = str_ireplace(':' . $tag, $pattern, $routeKey);
13921446
}
13931447

13941448
// If is redirect, No processing
@@ -1403,7 +1457,7 @@ protected function create(string $verb, string $from, $to, ?array $options = nul
14031457
$to = '\\' . ltrim($to, '\\');
14041458
}
14051459

1406-
$name = $options['as'] ?? $from;
1460+
$name = $options['as'] ?? $routeKey;
14071461

14081462
helper('array');
14091463

@@ -1412,20 +1466,22 @@ protected function create(string $verb, string $from, $to, ?array $options = nul
14121466
// routes should always be the "source of truth".
14131467
// this works only because discovered routes are added just prior
14141468
// to attempting to route the request.
1415-
$fromExists = dot_array_search('*.route.' . $from, $this->routes[$verb] ?? []) !== null;
1416-
if ((isset($this->routes[$verb][$name]) || $fromExists) && ! $overwrite) {
1469+
$routeKeyExists = isset($this->routes[$verb][$routeKey]);
1470+
if ((isset($this->routesNames[$verb][$name]) || $routeKeyExists) && ! $overwrite) {
14171471
return;
14181472
}
14191473

1420-
$this->routes[$verb][$name] = [
1421-
'route' => [$from => $to],
1474+
$this->routes[$verb][$routeKey] = [
1475+
'name' => $name,
1476+
'handler' => $to,
1477+
'from' => $from,
14221478
];
1423-
1424-
$this->routesOptions[$verb][$from] = $options;
1479+
$this->routesOptions[$verb][$routeKey] = $options;
1480+
$this->routesNames[$verb][$name] = $routeKey;
14251481

14261482
// Is this a redirect?
14271483
if (isset($options['redirect']) && is_numeric($options['redirect'])) {
1428-
$this->routes['*'][$name]['redirect'] = $options['redirect'];
1484+
$this->routes['*'][$routeKey]['redirect'] = $options['redirect'];
14291485
}
14301486
}
14311487

@@ -1566,12 +1622,15 @@ private function determineCurrentSubdomain()
15661622
*/
15671623
public function resetRoutes()
15681624
{
1569-
$this->routes = ['*' => []];
1625+
$this->routes = $this->routesNames = ['*' => []];
15701626

15711627
foreach ($this->defaultHTTPMethods as $verb) {
1572-
$this->routes[$verb] = [];
1628+
$this->routes[$verb] = [];
1629+
$this->routesNames[$verb] = [];
15731630
}
15741631

1632+
$this->routesOptions = [];
1633+
15751634
$this->prioritizeDetected = false;
15761635
$this->didDiscover = false;
15771636
}
@@ -1638,8 +1697,7 @@ public function getRegisteredControllers(?string $verb = '*'): array
16381697
if ($verb === '*') {
16391698
foreach ($this->defaultHTTPMethods as $tmpVerb) {
16401699
foreach ($this->routes[$tmpVerb] as $route) {
1641-
$routeKey = key($route['route']);
1642-
$controller = $this->getControllerName($route['route'][$routeKey]);
1700+
$controller = $this->getControllerName($route['handler']);
16431701
if ($controller !== null) {
16441702
$controllers[] = $controller;
16451703
}

system/Router/RouteCollectionInterface.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,12 +179,12 @@ public function reverseRoute(string $search, ...$params);
179179
/**
180180
* Determines if the route is a redirecting route.
181181
*/
182-
public function isRedirect(string $from): bool;
182+
public function isRedirect(string $routeKey): bool;
183183

184184
/**
185185
* Grabs the HTTP status code from a redirecting Route.
186186
*/
187-
public function getRedirectCode(string $from): int;
187+
public function getRedirectCode(string $routeKey): int;
188188

189189
/**
190190
* Get the flag that limit or not the routes with {locale} placeholder to App::$supportedLocales

user_guide_src/source/changelogs/v4.4.0.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ Changes
144144
So if you installed CodeIgniter under the folder that contains the special
145145
characters like ``(``, ``)``, etc., CodeIgniter didn't work. Since v4.4.0,
146146
this restriction has been removed.
147+
- **RouteCollection:** The array structure of the protected property ``$routes``
148+
has been modified for performance.
147149

148150
Deprecations
149151
************

user_guide_src/source/installation/upgrade_440.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@ Interface Changes
6666
Some interface changes have been made. Classes that implement them should update
6767
their APIs to reflect the changes. See :ref:`v440-interface-changes` for details.
6868

69+
RouteCollection::$routes
70+
========================
71+
72+
The array structure of the protected property ``$routes`` has been modified for
73+
performance.
74+
75+
If you extend ``RouteCollection`` and use the ``$routes``, update your code to
76+
match the new array structure.
77+
6978
Mandatory File Changes
7079
**********************
7180

0 commit comments

Comments
 (0)