diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 2e3aa0a9c07e..76f3a9989b07 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -271,6 +271,11 @@ class RouteCollection implements RouteCollectionInterface */ private ?string $httpHost = null; + /** + * Whether create() has already loaded the array helper. + */ + private bool $arrayHelperLoaded = false; + /** * Flag to limit or not the routes with {locale} placeholder to App::$supportedLocales */ @@ -1399,8 +1404,11 @@ protected function create(string $verb, string $from, $to, ?array $options = nul // Replace our regex pattern placeholders with the actual thing // so that the Router doesn't need to know about any of this. - foreach ($this->placeholders as $tag => $pattern) { - $routeKey = str_ireplace(':' . $tag, $pattern, $routeKey); + // Without a ':' no placeholder can match, so skip the loop. + if (str_contains($routeKey, ':')) { + foreach ($this->placeholders as $tag => $pattern) { + $routeKey = str_ireplace(':' . $tag, $pattern, $routeKey); + } } // If is redirect, No processing @@ -1417,7 +1425,11 @@ protected function create(string $verb, string $from, $to, ?array $options = nul $name = $options['as'] ?? $routeKey; - helper('array'); + // Not used here; kept so apps relying on the helper being loaded keep working. + if (! $this->arrayHelperLoaded) { + helper('array'); + $this->arrayHelperLoaded = true; + } // Don't overwrite any existing 'froms' so that auto-discovered routes // do not overwrite any app/Config/Routes settings. The app diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index 71a4959d7d0f..0769b23c1b25 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -248,6 +248,50 @@ public function testAddReplacesCustomPlaceholders(): void $this->assertSame($expects, $routes); } + public function testAddWithoutPlaceholdersKeepsExactKey(): void + { + $routes = $this->getCollector(); + + $routes->add('users/profile-edit', 'controller'); + $routes->add('products/([a-z]+)/(\d+)', 'controller'); + + $expects = [ + 'users/profile-edit' => '\controller', + 'products/([a-z]+)/(\d+)' => '\controller', + ]; + + $this->assertSame($expects, $routes->getRoutes()); + } + + public function testAddReplacesCustomNonCapturingPlaceholderMixedWithDefaults(): void + { + $routes = $this->getCollector(); + $routes->addPlaceholder('slug', '(?:[a-z]+-)*[a-z]+'); + + $routes->add('blog/(:num)/(:slug)/(:segment)', 'controller'); + + $expects = [ + 'blog/([0-9]+)/((?:[a-z]+-)*[a-z]+)/([^/]+)' => '\controller', + ]; + + $this->assertSame($expects, $routes->getRoutes()); + } + + public function testAddReplacesPlaceholdersCaseInsensitivelyWithoutParentheses(): void + { + $routes = $this->getCollector(); + + $routes->add('users/(:NUM)', 'controller'); + $routes->add('posts/:segment', 'controller'); + + $expects = [ + 'users/([0-9]+)' => '\controller', + 'posts/[^/]+' => '\controller', + ]; + + $this->assertSame($expects, $routes->getRoutes()); + } + public function testAddRecognizesCustomNamespaces(): void { $routes = $this->getCollector(); diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index e3e116d1f996..ddfbee6915bb 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -455,6 +455,7 @@ Changes ******* - **Config:** Added the ``md`` key for ``Config\Mimes::$mimes`` for Markdown files. +- **Router:** Reduced route registration overhead in ``RouteCollection::create()`` by skipping placeholder replacement for route keys without ``:`` and calling ``helper('array')`` only once per collection. ************ Deprecations