From 2273a613297c5e8f915a4ce499ae148d893c9c0f Mon Sep 17 00:00:00 2001 From: Paul Hachmang Date: Fri, 18 Sep 2026 09:35:48 +0200 Subject: [PATCH 1/2] An installation without compiled DI metadata serves requests AreaConfigLoader included generated/metadata/.php in every case. Without setup:di:compile, as in developer mode, every request failed on the missing file. The loader now hands such an installation to the runtime loader of Magento. --- CHANGELOG.md | 4 ++++ .../Model/ObjectManager/AreaConfigLoader.php | 13 +++++++++---- .../Test/Unit/Model/AreaConfigLoaderTest.php | 18 +++++++++++++++++- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a15fa46..613bd4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Read the di.xml files through the runtime loader of Magento when the installation has no compiled metadata, so a developer-mode install serves requests. + ## 0.2.0-rc5 - Use Credis when the PHP Redis extension is unavailable; phpredis remains the preferred transport when installed. diff --git a/src/FastBoot/Model/ObjectManager/AreaConfigLoader.php b/src/FastBoot/Model/ObjectManager/AreaConfigLoader.php index c0fae7e..a26ef5e 100644 --- a/src/FastBoot/Model/ObjectManager/AreaConfigLoader.php +++ b/src/FastBoot/Model/ObjectManager/AreaConfigLoader.php @@ -8,6 +8,7 @@ use GraphCommerce\FastBootCache\Model\Release; use Magento\Framework\App\Area; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager\ConfigLoader; use Magento\Framework\App\ObjectManager\ConfigLoader\Compiled; use Magento\Framework\ObjectManager\ConfigLoaderInterface; @@ -17,7 +18,9 @@ * global one it already holds: 15 000 arguments replaced by themselves on * every request. This loader hands the object manager only the entries the * area changes, from a file under var/cache/fastboot/metadata that follows - * an immutable release identity (or the source hashes without one). + * an immutable release identity (or the source hashes without one). An + * installation without compiled metadata reads the di.xml files through the + * runtime loader of Magento. */ class AreaConfigLoader implements ConfigLoaderInterface { @@ -30,6 +33,7 @@ public function __construct( private readonly DirectoryList $directoryList, private readonly Feature $feature, private readonly Release $release, + private readonly ConfigLoader $runtimeLoader, ) { } @@ -40,9 +44,10 @@ public function load($area, bool $rebuild = false) } $areaFile = Compiled::getFilePath($area); $globalFile = Compiled::getFilePath(Area::AREA_GLOBAL); - if ($area === Area::AREA_GLOBAL || !is_file($areaFile) || !is_file($globalFile) - || !$this->feature->on(self::SWITCH) - ) { + if (!is_file($areaFile) || !is_file($globalFile)) { + return $this->loaded[$area] = $this->runtimeLoader->load($area); + } + if ($area === Area::AREA_GLOBAL || !$this->feature->on(self::SWITCH)) { return $this->loaded[$area] = include $areaFile; } $release = $this->release->id(); diff --git a/src/FastBoot/Test/Unit/Model/AreaConfigLoaderTest.php b/src/FastBoot/Test/Unit/Model/AreaConfigLoaderTest.php index e3a46f8..0ec45b0 100644 --- a/src/FastBoot/Test/Unit/Model/AreaConfigLoaderTest.php +++ b/src/FastBoot/Test/Unit/Model/AreaConfigLoaderTest.php @@ -8,12 +8,28 @@ use GraphCommerce\FastBootCache\Model\Feature; use GraphCommerce\FastBootCache\Model\Release; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager\ConfigLoader; use PHPUnit\Framework\Attributes\PreserveGlobalState; use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\TestCase; class AreaConfigLoaderTest extends TestCase { + #[RunInSeparateProcess] + #[PreserveGlobalState(false)] + public function testInstallationWithoutCompiledMetadataUsesTheRuntimeLoader(): void + { + $root = sys_get_temp_dir().'/fastboot-area-'.bin2hex(random_bytes(8)); + define('BP', $root); + $runtime = $this->createMock(ConfigLoader::class); + $runtime->expects(self::once())->method('load')->with('graphql')->willReturn(['arguments' => ['runtime' => true]]); + $feature = $this->createStub(Feature::class); + $feature->method('on')->willReturn(true); + $loader = new AreaConfigLoader($this->createStub(DirectoryList::class), $feature, $this->createStub(Release::class), $runtime); + self::assertSame(['arguments' => ['runtime' => true]], $loader->load('graphql')); + self::assertSame(['arguments' => ['runtime' => true]], $loader->load('graphql')); + } + #[RunInSeparateProcess] #[PreserveGlobalState(false)] public function testCorruptArtifactsRepairAndPreparationRebuildsChangedMetadata(): void @@ -34,7 +50,7 @@ public function testCorruptArtifactsRepairAndPreparationRebuildsChangedMetadata( $feature->method('on')->willReturn(true); $release = $this->createStub(Release::class); $release->method('id')->willReturn('deployed-build'); - $loader = fn () => new AreaConfigLoader($directory, $feature, $release); + $loader = fn () => new AreaConfigLoader($directory, $feature, $release, $this->createStub(ConfigLoader::class)); $expected = ['arguments' => ['changed' => ['x' => 2]]]; try { self::assertSame($expected, $loader()->load('graphql')); From bf09cd11200bcb1a3769780f75058c0b16c44e8b Mon Sep 17 00:00:00 2001 From: Paul Hachmang Date: Fri, 18 Sep 2026 09:39:12 +0200 Subject: [PATCH 2/2] The fallback test records the runtime loader calls without a mock expectation --- .../Test/Unit/Model/AreaConfigLoaderTest.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/FastBoot/Test/Unit/Model/AreaConfigLoaderTest.php b/src/FastBoot/Test/Unit/Model/AreaConfigLoaderTest.php index 0ec45b0..e76d2da 100644 --- a/src/FastBoot/Test/Unit/Model/AreaConfigLoaderTest.php +++ b/src/FastBoot/Test/Unit/Model/AreaConfigLoaderTest.php @@ -21,13 +21,18 @@ public function testInstallationWithoutCompiledMetadataUsesTheRuntimeLoader(): v { $root = sys_get_temp_dir().'/fastboot-area-'.bin2hex(random_bytes(8)); define('BP', $root); - $runtime = $this->createMock(ConfigLoader::class); - $runtime->expects(self::once())->method('load')->with('graphql')->willReturn(['arguments' => ['runtime' => true]]); + $calls = 0; + $runtime = $this->createStub(ConfigLoader::class); + $runtime->method('load')->willReturnCallback(static function (string $area) use (&$calls): array { + $calls++; + return ['arguments' => ['runtime' => $area]]; + }); $feature = $this->createStub(Feature::class); $feature->method('on')->willReturn(true); $loader = new AreaConfigLoader($this->createStub(DirectoryList::class), $feature, $this->createStub(Release::class), $runtime); - self::assertSame(['arguments' => ['runtime' => true]], $loader->load('graphql')); - self::assertSame(['arguments' => ['runtime' => true]], $loader->load('graphql')); + self::assertSame(['arguments' => ['runtime' => 'graphql']], $loader->load('graphql')); + self::assertSame(['arguments' => ['runtime' => 'graphql']], $loader->load('graphql')); + self::assertSame(1, $calls); } #[RunInSeparateProcess]