From 26612d512b58475ab66525f3ac72593b7d702697 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 1 Nov 2026 23:41:09 +0100 Subject: [PATCH] perf: fast-path for default Composer namespaces loading in Autoloader When Config\Modules::$composerPackages has no filters defined (the default behavior in almost all applications), all PSR-4 prefixes from Composer belong to installed packages. Skip the expensive InstalledVersions::getAllRawData() scanning, versions array_merge, and triple-nested str_starts_with matching loop when neither 'only' nor 'exclude' is configured. When package filtering is configured, the original full validation logic is preserved with zero BC breaks. --- system/Autoloader/Autoloader.php | 32 +++++++++++++++++----- tests/system/Autoloader/AutoloaderTest.php | 20 ++++++++++++++ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 3d7068aa2a82..9dee4cda8334 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -404,6 +404,29 @@ private function loadComposerNamespaces(ClassLoader $composer, array $composerPa } } + // Check config for $composerPackages. + $only = $composerPackages['only'] ?? []; + $exclude = $composerPackages['exclude'] ?? []; + + if ($only !== [] && $exclude !== []) { + throw new ConfigException('Cannot use "only" and "exclude" at the same time in "Config\Modules::$composerPackages".'); + } + + // When no package filtering is configured (the default), all PSR-4 prefixes + // from Composer can be added directly without expensive InstalledVersions scanning. + if ($only === [] && $exclude === []) { + $newPaths = []; + + foreach ($namespacePaths as $namespace => $srcPaths) { + // Composer stores namespaces with trailing slash. We don't. + $newPaths[rtrim($namespace, '\\ ')] = $srcPaths; + } + + $this->addNamespace($newPaths); + + return; + } + if (! method_exists(InstalledVersions::class, 'getAllRawData')) { // @phpstan-ignore function.alreadyNarrowedType throw new RuntimeException( 'Your Composer version is too old.' @@ -411,6 +434,7 @@ private function loadComposerNamespaces(ClassLoader $composer, array $composerPa . ' and remove your vendor/ directory, and run `composer update`.', ); } + // This method requires Composer 2.0.14 or later. $allData = InstalledVersions::getAllRawData(); $packageList = []; @@ -419,15 +443,9 @@ private function loadComposerNamespaces(ClassLoader $composer, array $composerPa $packageList = array_merge($packageList, $list['versions']); } - // Check config for $composerPackages. - $only = $composerPackages['only'] ?? []; - $exclude = $composerPackages['exclude'] ?? []; - if ($only !== [] && $exclude !== []) { - throw new ConfigException('Cannot use "only" and "exclude" at the same time in "Config\Modules::$composerPackages".'); - } - // Get install paths of packages to add namespace for auto-discovery. $installPaths = []; + if ($only !== []) { foreach ($packageList as $packageName => $data) { if (in_array($packageName, $only, true) && isset($data['install_path'])) { diff --git a/tests/system/Autoloader/AutoloaderTest.php b/tests/system/Autoloader/AutoloaderTest.php index be22e4a403f1..7703e8c6b676 100644 --- a/tests/system/Autoloader/AutoloaderTest.php +++ b/tests/system/Autoloader/AutoloaderTest.php @@ -417,6 +417,26 @@ public function testFindsComposerRoutesWithComposerPathNotFound(): void $this->assertArrayNotHasKey('Laminas\\Escaper', $namespaces); } + public function testComposerPackagesDefaultLoadsAllNamespacesDirectly(): void + { + $config = new Autoload(); + $config->psr4 = []; + $modules = new Modules(); + $modules->discoverInComposer = true; + $modules->composerPackages = []; + + $loader = new Autoloader(); + $loader->initialize($config, $modules); + + $namespaces = $loader->getNamespace(); + + $this->assertArrayHasKey('Laminas\Escaper', $namespaces); + $this->assertArrayHasKey('Psr\Log', $namespaces); + $this->assertArrayNotHasKey('CodeIgniter', $namespaces); + $this->assertArrayNotHasKey(APP_NAMESPACE, $namespaces); + $this->assertArrayNotHasKey('Config', $namespaces); + } + public function testAutoloaderLoadsNonClassFiles(): void { $config = new Autoload();