From d3ec1e43b648007bb2a5d8ad8a1de26e20681cde Mon Sep 17 00:00:00 2001 From: michalsn Date: Thu, 9 Apr 2026 20:49:03 +0200 Subject: [PATCH] fix: store SPL closures in register() so unregister() can remove them --- system/Autoloader/Autoloader.php | 29 ++++++++++++--- tests/system/Autoloader/AutoloaderTest.php | 39 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.3.rst | 2 ++ 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 69906d4e162a..29d119727f94 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Autoloader; +use Closure; use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Exceptions\RuntimeException; @@ -93,6 +94,14 @@ class Autoloader */ protected $helpers = ['url']; + /** + * Stores the closures registered with spl_autoload_register() + * so that unregister() can remove the exact same instances. + * + * @var list + */ + private array $registeredClosures = []; + public function __construct(private readonly string $composerPath = COMPOSER_PATH) { } @@ -170,8 +179,17 @@ private function loadComposerAutoloader(Modules $modules): void */ public function register() { - spl_autoload_register($this->loadClassmap(...), true); - spl_autoload_register($this->loadClass(...), true); + // Store the exact Closure instances so unregister() can remove them. + // First-class callable syntax (e.g. $this->loadClass(...)) creates a + // new Closure object on every call, so we must reuse the same instances. + $loadClassmap = $this->loadClassmap(...); + $loadClass = $this->loadClass(...); + + $this->registeredClosures[] = $loadClassmap; + $this->registeredClosures[] = $loadClass; + + spl_autoload_register($loadClassmap, true); + spl_autoload_register($loadClass, true); foreach ($this->files as $file) { $this->includeFile($file); @@ -183,8 +201,11 @@ public function register() */ public function unregister(): void { - spl_autoload_unregister($this->loadClass(...)); - spl_autoload_unregister($this->loadClassmap(...)); + foreach ($this->registeredClosures as $closure) { + spl_autoload_unregister($closure); + } + + $this->registeredClosures = []; } /** diff --git a/tests/system/Autoloader/AutoloaderTest.php b/tests/system/Autoloader/AutoloaderTest.php index ef76091baa39..be22e4a403f1 100644 --- a/tests/system/Autoloader/AutoloaderTest.php +++ b/tests/system/Autoloader/AutoloaderTest.php @@ -123,6 +123,45 @@ public function testServiceAutoLoaderFromShareInstances(): void $this->assertSame($expected, $actual); } + public function testUnregisterRemovesClosuresFromSplStack(): void + { + $countBefore = count(spl_autoload_functions()); + + $config = new Autoload(); + $modules = new Modules(); + $modules->discoverInComposer = false; + $config->psr4 = ['CodeIgniter' => SYSTEMPATH]; + + $loader = new Autoloader(); + $loader->initialize($config, $modules)->register(); + + $this->assertCount($countBefore + 2, spl_autoload_functions()); + + $loader->unregister(); + + $this->assertCount($countBefore, spl_autoload_functions()); + } + + public function testUnregisterRemovesAllClosuresAfterMultipleRegistrations(): void + { + $countBefore = count(spl_autoload_functions()); + + $config = new Autoload(); + $modules = new Modules(); + $modules->discoverInComposer = false; + $config->psr4 = ['CodeIgniter' => SYSTEMPATH]; + + $loader = new Autoloader(); + $loader->initialize($config, $modules)->register(); + $loader->register(); + + $this->assertCount($countBefore + 4, spl_autoload_functions()); + + $loader->unregister(); + + $this->assertCount($countBefore, spl_autoload_functions()); + } + public function testServiceAutoLoader(): void { $autoloader = service('autoloader', false); diff --git a/user_guide_src/source/changelogs/v4.7.3.rst b/user_guide_src/source/changelogs/v4.7.3.rst index b0b0bf1b8c2d..1947048fe142 100644 --- a/user_guide_src/source/changelogs/v4.7.3.rst +++ b/user_guide_src/source/changelogs/v4.7.3.rst @@ -30,6 +30,8 @@ Deprecations Bugs Fixed ********** +- **Autoloader:** Fixed a bug where ``Autoloader::unregister()`` (used during tests) silently failed to remove handlers from the SPL autoload stack, causing closures to accumulate permanently. + See the repo's `CHANGELOG.md `_ for a complete list of bugs fixed.