From 60378154be2d290910c2d41099324ac5a919e52d Mon Sep 17 00:00:00 2001 From: Jeanmarcos Juarez Date: Sun, 9 Aug 2026 01:10:09 -0400 Subject: [PATCH] perf(di): store non-global compiled areas as their diff from global --- .../ObjectManager/Environment/Compiled.php | 13 ++- .../ObjectManager/Config/Compiled.php | 56 +++++++++++- .../ObjectManager/ConfigLoaderInterface.php | 10 +++ .../Test/Unit/Config/CompiledTest.php | 87 +++++++++++++++++++ .../Module/Di/App/Task/Operation/Area.php | 43 +++++++++ .../Test/Unit/Module/Di/App/Task/AreaTest.php | 70 +++++++++++++++ 6 files changed, 271 insertions(+), 8 deletions(-) diff --git a/lib/internal/Magento/Framework/App/ObjectManager/Environment/Compiled.php b/lib/internal/Magento/Framework/App/ObjectManager/Environment/Compiled.php index 26bfb9925c8..18355331cbf 100644 --- a/lib/internal/Magento/Framework/App/ObjectManager/Environment/Compiled.php +++ b/lib/internal/Magento/Framework/App/ObjectManager/Environment/Compiled.php @@ -8,6 +8,7 @@ use Magento\Framework\App\EnvironmentInterface; use Magento\Framework\App\Interception\Cache\CompiledConfig; +use Magento\Framework\ObjectManager\ConfigLoaderInterface; use Magento\Framework\ObjectManager\FactoryInterface; use Magento\Framework\App\Area; use Magento\Framework\Interception\ObjectManager\ConfigInterface; @@ -64,7 +65,8 @@ public function getDiConfig() { if (!$this->config) { $this->config = new \Magento\Framework\Interception\ObjectManager\Config\Compiled( - $this->getConfigData() + $this->getConfigData(), + $this->getObjectManagerConfigLoader() ); } @@ -78,7 +80,8 @@ public function getDiConfig() */ protected function getConfigData() { - return $this->getObjectManagerConfigLoader()->load(Area::AREA_GLOBAL); + return $this->getObjectManagerConfigLoader()->load(Area::AREA_GLOBAL) + + [ConfigLoaderInterface::AREA_KEY => Area::AREA_GLOBAL]; } /** @@ -103,11 +106,7 @@ public function configureObjectManager(ConfigInterface $diConfig, &$sharedInstan { $objectManager = ObjectManager::getInstance(); - $objectManager->configure( - $objectManager - ->get(\Magento\Framework\ObjectManager\ConfigLoaderInterface::class) - ->load(Area::AREA_GLOBAL) - ); + $objectManager->configure($this->getConfigData()); $objectManager->get(\Magento\Framework\Config\ScopeInterface::class) ->setCurrentScope('global'); $diConfig->setInterceptionConfig( diff --git a/lib/internal/Magento/Framework/ObjectManager/Config/Compiled.php b/lib/internal/Magento/Framework/ObjectManager/Config/Compiled.php index 1a6c7bd88dd..54ad4c5b67c 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Config/Compiled.php +++ b/lib/internal/Magento/Framework/ObjectManager/Config/Compiled.php @@ -7,6 +7,7 @@ use Magento\Framework\ObjectManager\ConfigInterface; use Magento\Framework\ObjectManager\ConfigCacheInterface; +use Magento\Framework\ObjectManager\ConfigLoaderInterface; use Magento\Framework\ObjectManager\LazyTypeAwareInterface; use Magento\Framework\ObjectManager\RelationsInterface; @@ -37,11 +38,26 @@ class Compiled implements ConfigInterface, LazyTypeAwareInterface */ private array $lazyTypes = []; + /** + * Area whose configuration is currently applied, null when the state matches no single area + * + * @var string|null + */ + private $appliedArea; + + /** + * @var ConfigLoaderInterface|null + */ + private $configLoader; + /** * @param array $data + * @param ConfigLoaderInterface|null $configLoader */ - public function __construct($data) + public function __construct($data, ?ConfigLoaderInterface $configLoader = null) { + $this->configLoader = $configLoader; + $this->appliedArea = $data[ConfigLoaderInterface::AREA_KEY] ?? null; $this->arguments = isset($data['arguments']) && is_array($data['arguments']) ? $data['arguments'] : []; $this->virtualTypes = isset($data['instanceTypes']) && is_array($data['instanceTypes']) @@ -164,6 +180,8 @@ public function getPreference($type) */ public function extend(array $configuration) { + $configuration = $this->resolveAgainstAppliedArea($configuration); + $this->arguments = isset($configuration['arguments']) && is_array($configuration['arguments']) ? array_replace($this->arguments, $configuration['arguments']) : $this->arguments; @@ -178,6 +196,42 @@ public function extend(array $configuration) : $this->lazyTypes; } + /** + * Returns a configuration that is equivalent to the complete one on top of the current state + * + * @param array $configuration + * @return array + */ + private function resolveAgainstAppliedArea(array $configuration) + { + $base = $configuration[ConfigLoaderInterface::EXTENDS_KEY] ?? null; + $resolved = $base !== null && $base !== $this->appliedArea && $this->configLoader !== null + ? self::merge($this->configLoader->load($base), $configuration) + : $configuration; + + $this->appliedArea = $configuration[ConfigLoaderInterface::AREA_KEY] ?? null; + + return $resolved; + } + + /** + * Applies a configuration on top of another one, per top-level key of each section + * + * @param array $base + * @param array $configuration + * @return array + */ + private static function merge(array $base, array $configuration) + { + foreach (['arguments', 'instanceTypes', 'preferences', 'lazyTypes'] as $section) { + if (isset($configuration[$section]) && is_array($configuration[$section])) { + $base[$section] = array_replace($base[$section] ?? [], $configuration[$section]); + } + } + + return $base; + } + /** * Retrieve all virtual types * diff --git a/lib/internal/Magento/Framework/ObjectManager/ConfigLoaderInterface.php b/lib/internal/Magento/Framework/ObjectManager/ConfigLoaderInterface.php index 8c30898ccb1..246192af613 100644 --- a/lib/internal/Magento/Framework/ObjectManager/ConfigLoaderInterface.php +++ b/lib/internal/Magento/Framework/ObjectManager/ConfigLoaderInterface.php @@ -14,6 +14,16 @@ */ interface ConfigLoaderInterface { + /** + * When present, names the area the loaded configuration only holds the differences against + */ + public const EXTENDS_KEY = '_extends'; + + /** + * When present, names the area the loaded configuration belongs to + */ + public const AREA_KEY = '_area'; + /** * Load modules DI configuration * diff --git a/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Config/CompiledTest.php b/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Config/CompiledTest.php index b28a912ca3f..76b295629e3 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Config/CompiledTest.php +++ b/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Config/CompiledTest.php @@ -8,6 +8,7 @@ namespace Magento\Framework\ObjectManager\Test\Unit\Config; use Magento\Framework\ObjectManager\Config\Compiled; +use Magento\Framework\ObjectManager\ConfigLoaderInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\DataProvider; @@ -347,4 +348,90 @@ public function testExtendMergesLazyTypes(): void $this->assertFalse($compiled->isNonLazyType('Second\\Type')); $this->assertTrue($compiled->isNonLazyType('Other\\Type')); } + + public function testExtendAppliesADeltaOnTopOfTheAreaItExtends(): void + { + $configLoader = $this->createMock(ConfigLoaderInterface::class); + $configLoader->expects($this->never())->method('load'); + + $compiled = new Compiled( + [ + 'preferences' => ['preference1' => 'globalValue'], + ConfigLoaderInterface::AREA_KEY => 'global', + ], + $configLoader + ); + + $compiled->extend([ + ConfigLoaderInterface::EXTENDS_KEY => 'global', + 'preferences' => ['preference1' => 'frontendValue'], + ]); + + $this->assertSame('frontendValue', $compiled->getPreference('preference1')); + } + + public function testExtendRebuildsFromTheBaseWhenAnotherAreaIsAlreadyApplied(): void + { + $globalConfig = ['preferences' => ['preference1' => 'globalValue']]; + $configLoader = $this->createMock(ConfigLoaderInterface::class); + $configLoader->expects($this->once()) + ->method('load') + ->with('global') + ->willReturn($globalConfig); + + $compiled = new Compiled( + $globalConfig + [ConfigLoaderInterface::AREA_KEY => 'global'], + $configLoader + ); + + $compiled->extend([ + ConfigLoaderInterface::EXTENDS_KEY => 'global', + 'preferences' => ['preference1' => 'adminhtmlValue'], + ]); + $compiled->extend([ + ConfigLoaderInterface::EXTENDS_KEY => 'global', + 'preferences' => ['preference2' => 'frontendOnly'], + ]); + + $this->assertSame('globalValue', $compiled->getPreference('preference1')); + $this->assertSame('frontendOnly', $compiled->getPreference('preference2')); + } + + public function testExtendRebuildsFromTheBaseAfterAnArbitraryConfiguration(): void + { + $globalConfig = ['preferences' => ['preference1' => 'globalValue']]; + $configLoader = $this->createMock(ConfigLoaderInterface::class); + $configLoader->expects($this->once()) + ->method('load') + ->with('global') + ->willReturn($globalConfig); + + $compiled = new Compiled( + $globalConfig + [ConfigLoaderInterface::AREA_KEY => 'global'], + $configLoader + ); + + $compiled->extend(['preferences' => ['preference1' => 'mockedValue']]); + $compiled->extend([ + ConfigLoaderInterface::EXTENDS_KEY => 'global', + 'preferences' => [], + ]); + + $this->assertSame('globalValue', $compiled->getPreference('preference1')); + } + + public function testExtendAppliesADeltaWhenNoLoaderIsAvailable(): void + { + $compiled = new Compiled([ + 'preferences' => ['preference1' => 'globalValue'], + ConfigLoaderInterface::AREA_KEY => 'global', + ]); + + $compiled->extend([ + ConfigLoaderInterface::EXTENDS_KEY => 'global', + 'preferences' => ['preference1' => 'frontendValue'], + ]); + + $this->assertSame('frontendValue', $compiled->getPreference('preference1')); + } } diff --git a/setup/src/Magento/Setup/Module/Di/App/Task/Operation/Area.php b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/Area.php index 53318874d4a..c4f5d197e25 100644 --- a/setup/src/Magento/Setup/Module/Di/App/Task/Operation/Area.php +++ b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/Area.php @@ -7,6 +7,7 @@ use Magento\Setup\Module\Di\App\Task\OperationInterface; use Magento\Framework\App; +use Magento\Framework\ObjectManager\ConfigLoaderInterface; use Magento\Setup\Module\Di\Compiler\Config; use Magento\Setup\Module\Di\Definition\Collection as DefinitionsCollection; @@ -15,6 +16,11 @@ */ class Area implements OperationInterface { + /** + * Sections of the compiled configuration that are merged per top-level key + */ + private const MERGED_SECTIONS = ['arguments', 'preferences', 'instanceTypes', 'lazyTypes']; + /** * @var App\AreaList */ @@ -91,6 +97,7 @@ public function doOperation() $this->sortDefinitions($definitionsCollection); $areaCodes = array_merge([App\Area::AREA_GLOBAL], $this->areaList->getCodes()); + $globalConfig = []; foreach ($areaCodes as $areaCode) { $config = $this->configReader->generateCachePerScope($definitionsCollection, $areaCode); $config = $this->modificationChain->modify($config); @@ -100,10 +107,46 @@ public function doOperation() ksort($config['preferences']); ksort($config['instanceTypes']); + if ($areaCode === App\Area::AREA_GLOBAL) { + $globalConfig = $config; + } else { + $config = $this->extractDiff($config, $globalConfig); + } + $this->configWriter->write($areaCode, $config); } } + /** + * Reduces an area configuration to the entries that differ from the global one + * + * @param array $config + * @param array $globalConfig + * @return array + */ + private function extractDiff(array $config, array $globalConfig) + { + $diff = [ConfigLoaderInterface::EXTENDS_KEY => App\Area::AREA_GLOBAL]; + + foreach ($config as $section => $values) { + if (!is_array($values) || !in_array($section, self::MERGED_SECTIONS, true)) { + $diff[$section] = $values; + continue; + } + + $globalValues = $globalConfig[$section] ?? []; + $sectionDiff = []; + foreach ($values as $key => $value) { + if (!array_key_exists($key, $globalValues) || $globalValues[$key] !== $value) { + $sectionDiff[$key] = $value; + } + } + $diff[$section] = $sectionDiff; + } + + return $diff; + } + /** * Returns definitions collection * diff --git a/setup/src/Magento/Setup/Test/Unit/Module/Di/App/Task/AreaTest.php b/setup/src/Magento/Setup/Test/Unit/Module/Di/App/Task/AreaTest.php index f80617ca9fa..ae7dddfd283 100644 --- a/setup/src/Magento/Setup/Test/Unit/Module/Di/App/Task/AreaTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Module/Di/App/Task/AreaTest.php @@ -10,6 +10,7 @@ use Magento\Framework\App; use Magento\Framework\App\AreaList; use Magento\Framework\App\ObjectManager\ConfigWriterInterface; +use Magento\Framework\ObjectManager\ConfigLoaderInterface; use Magento\Setup\Module\Di\App\Task\Operation\Area; use Magento\Setup\Module\Di\Compiler\Config; use Magento\Setup\Module\Di\Compiler\Config\ModificationChain; @@ -126,4 +127,73 @@ public function testDoOperationGlobalArea() $areaOperation->doOperation(); } + + public function testDoOperationWritesOnlyTheDifferencesOfNonGlobalAreas() + { + $path = 'path/to/codebase/'; + + $globalConfig = [ + 'arguments' => [ + 'Overridden' => ['b' => 2], + 'Shared' => ['a' => 1], + ], + 'preferences' => ['SomeInterface' => 'GlobalImplementation'], + 'instanceTypes' => ['globalVirtual' => 'GlobalType'], + ]; + $frontendConfig = [ + 'arguments' => [ + 'FrontendOnly' => ['c' => 3], + 'Overridden' => ['b' => 'frontend'], + 'Shared' => ['a' => 1], + ], + 'preferences' => ['SomeInterface' => 'FrontendImplementation'], + 'instanceTypes' => ['globalVirtual' => 'GlobalType'], + ]; + + $areaOperation = new Area( + $this->areaListMock, + $this->areaInstancesNamesList, + $this->configReaderMock, + $this->configWriterMock, + $this->configChain, + [$path] + ); + + $this->areaListMock->expects($this->once()) + ->method('getCodes') + ->willReturn([App\Area::AREA_FRONTEND]); + $this->areaInstancesNamesList->expects($this->once()) + ->method('getList') + ->with($path) + ->willReturn([]); + $this->configReaderMock->method('generateCachePerScope') + ->willReturnCallback( + static fn ($definitions, $areaCode) => $areaCode === App\Area::AREA_GLOBAL + ? $globalConfig + : $frontendConfig + ); + $this->configChain->method('modify')->willReturnArgument(0); + + $written = []; + $this->configWriterMock->method('write') + ->willReturnCallback(function ($areaCode, $config) use (&$written) { + $written[$areaCode] = $config; + }); + + $areaOperation->doOperation(); + + $this->assertSame($globalConfig, $written[App\Area::AREA_GLOBAL]); + $this->assertSame( + [ + ConfigLoaderInterface::EXTENDS_KEY => App\Area::AREA_GLOBAL, + 'arguments' => [ + 'FrontendOnly' => ['c' => 3], + 'Overridden' => ['b' => 'frontend'], + ], + 'preferences' => ['SomeInterface' => 'FrontendImplementation'], + 'instanceTypes' => [], + ], + $written[App\Area::AREA_FRONTEND] + ); + } }