From 20474a9d3eb32dd7227d7f8913de4abe6f1509d1 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Tue, 30 Sep 2025 15:28:52 +0300 Subject: [PATCH] state config --- .gitignore | 1 + application/Espo/Core/Utils/Config.php | 36 +++- .../Espo/Core/Utils/Config/ConfigWriter.php | 66 ++++++- .../Utils/Config/InternalConfigHelper.php | 14 ++ .../Core/Utils/Config/ConfigWriterTest.php | 183 +++++++++++++++++- tests/unit/Espo/Core/Utils/ConfigTest.php | 13 +- 6 files changed, 285 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 88a4b1b811..8dc78dd415 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ /data/config-internal.php /data/config-override.php /data/config-internal-override.php +/data/state.php /data/tmp/* /build /node_modules diff --git a/application/Espo/Core/Utils/Config.php b/application/Espo/Core/Utils/Config.php index 250b23e8f9..918bacb6aa 100644 --- a/application/Espo/Core/Utils/Config.php +++ b/application/Espo/Core/Utils/Config.php @@ -46,6 +46,7 @@ class Config private string $internalConfigPath = 'data/config-internal.php'; private string $overrideConfigPath = 'data/config-override.php'; private string $internalOverrideConfigPath = 'data/config-internal-override.php'; + private string $stateConfigPath = 'data/state.php'; private string $cacheTimestamp = 'cacheTimestamp'; /** @var string[] */ protected $associativeArrayAttributeList = [ @@ -81,12 +82,22 @@ class Config * A path to the internal config file. * * @todo Move to ConfigData. + * @internal */ public function getInternalConfigPath(): string { return $this->internalConfigPath; } + /** + * @todo Move to ConfigData. + * @internal + */ + public function getStateConfigPath(): string + { + return $this->stateConfigPath; + } + /** * Get a parameter value. * @@ -302,13 +313,15 @@ class Config $internalData = $this->readFile($this->internalConfigPath); $overrideData = $this->readFile($this->overrideConfigPath); $internalOverrideData = $this->readFile($this->internalOverrideConfigPath); + $stateConfigData = $this->readFile($this->stateConfigPath); $this->data = $this->mergeData( - $systemData, - $data, - $internalData, - $overrideData, - $internalOverrideData + systemData: $systemData, + data: $data, + internalData: $internalData, + overrideData: $overrideData, + internalOverrideData: $internalOverrideData, + stateData: $stateConfigData, ); $this->internalParamList = array_values(array_merge( @@ -325,6 +338,7 @@ class Config * @param array $internalData * @param array $overrideData * @param array $internalOverrideData + * @param array $stateData * @return array */ private function mergeData( @@ -332,7 +346,8 @@ class Config array $data, array $internalData, array $overrideData, - array $internalOverrideData + array $internalOverrideData, + array $stateData, ): array { /** @var array $mergedData */ @@ -344,8 +359,13 @@ class Config /** @var array $mergedData */ $mergedData = Util::merge($mergedData, $overrideData); - /** @var array */ - return Util::merge($mergedData, $internalOverrideData); + /** @var array $mergedData */ + $mergedData = Util::merge($mergedData, $internalOverrideData); + + /** @var array $mergedData */ + $mergedData = Util::merge($mergedData, $stateData); + + return $mergedData; } /** diff --git a/application/Espo/Core/Utils/Config/ConfigWriter.php b/application/Espo/Core/Utils/Config/ConfigWriter.php index 53516877b4..03874446a5 100644 --- a/application/Espo/Core/Utils/Config/ConfigWriter.php +++ b/application/Espo/Core/Utils/Config/ConfigWriter.php @@ -107,6 +107,7 @@ class ConfigWriter $configPath = $this->config->getConfigPath(); $internalConfigPath = $this->config->getInternalConfigPath(); + $stateConfigPath = $this->config->getStateConfigPath(); if (!$this->fileManager->isFile($configPath)) { throw new RuntimeException("Config file '$configPath' not found."); @@ -117,6 +118,9 @@ class ConfigWriter $dataInternal = $this->fileManager->isFile($internalConfigPath) ? $this->fileManager->getPhpContents($internalConfigPath) : []; + $dataState = $this->fileManager->isFile($stateConfigPath) ? + $this->fileManager->getPhpContents($stateConfigPath) : []; + if (!is_array($data)) { throw new RuntimeException("Could not read config."); } @@ -125,12 +129,45 @@ class ConfigWriter throw new RuntimeException("Could not read config-internal."); } + if (!is_array($dataState)) { + throw new RuntimeException("Could not read state."); + } + $toSaveInternal = false; + $toSaveState = false; + $toSaveMain = false; + + foreach (array_merge(array_keys($changedData), $this->removeParamList) as $key) { + if (array_key_exists($key, $data)) { + $toSaveMain = true; + } + + if (array_key_exists($key, $dataInternal)) { + $toSaveInternal = true; + } + + if (array_key_exists($key, $dataState)) { + $toSaveState = true; + } + } foreach ($changedData as $key => $value) { + if ($this->internalConfigHelper->isParamForStateConfig($key)) { + $dataState[$key] = $value; + + unset($data[$key]); + unset($dataInternal[$key]); + + $toSaveState = true; + + continue; + } + if ($this->internalConfigHelper->isParamForInternalConfig($key)) { $dataInternal[$key] = $value; + unset($data[$key]); + unset($dataState[$key]); $toSaveInternal = true; @@ -138,25 +175,38 @@ class ConfigWriter } $data[$key] = $value; + + unset($dataState[$key]); + unset($dataInternal[$key]); + + $toSaveMain = true; } foreach ($this->removeParamList as $key) { - if ($this->internalConfigHelper->isParamForInternalConfig($key)) { - unset($dataInternal[$key]); - - $toSaveInternal = true; - - continue; + if (array_key_exists($key, $data)) { + unset($data[$key]); } - unset($data[$key]); + if (array_key_exists($key, $dataInternal)) { + unset($dataInternal[$key]); + } + + if (array_key_exists($key, $dataState)) { + unset($dataState[$key]); + } } if ($toSaveInternal) { $this->saveData($internalConfigPath, $dataInternal, 'microtimeInternal'); } - $this->saveData($configPath, $data, 'microtime'); + if ($toSaveMain) { + $this->saveData($configPath, $data, 'microtime'); + } + + if ($toSaveState) { + $this->saveData($stateConfigPath, $dataState, 'microtimeState'); + } $this->changedData = []; $this->removeParamList = []; diff --git a/application/Espo/Core/Utils/Config/InternalConfigHelper.php b/application/Espo/Core/Utils/Config/InternalConfigHelper.php index f8e8ccfc05..c470f14a05 100644 --- a/application/Espo/Core/Utils/Config/InternalConfigHelper.php +++ b/application/Espo/Core/Utils/Config/InternalConfigHelper.php @@ -34,9 +34,23 @@ use Espo\Core\Utils\Metadata; class InternalConfigHelper { + /** @var string[] */ + private array $stateParamList = [ + 'appTimestamp', + 'cacheTimestamp', + 'version', + 'latestVersion', + 'latestExtensionVersions', + ]; + public function __construct(private Config $config, private Metadata $metadata) {} + public function isParamForStateConfig(string $name): bool + { + return in_array($name, $this->stateParamList); + } + public function isParamForInternalConfig(string $name): bool { if ($this->config->isInternal($name)) { diff --git a/tests/unit/Espo/Core/Utils/Config/ConfigWriterTest.php b/tests/unit/Espo/Core/Utils/Config/ConfigWriterTest.php index 3a6258b64e..66adcf7f0e 100644 --- a/tests/unit/Espo/Core/Utils/Config/ConfigWriterTest.php +++ b/tests/unit/Espo/Core/Utils/Config/ConfigWriterTest.php @@ -46,6 +46,7 @@ class ConfigWriterTest extends TestCase private $configPath; private $internalConfigPath; + private $stateConfigPath; protected function setUp(): void { @@ -61,8 +62,9 @@ class ConfigWriterTest extends TestCase $this->internalConfigHelper ); - $this->configPath = 'somepath'; - $this->internalConfigPath = 'internalSomepath'; + $this->configPath = 'somePath'; + $this->internalConfigPath = 'internalSomePath'; + $this->stateConfigPath = 'stateSomePath'; $this->config ->expects($this->any()) @@ -73,6 +75,11 @@ class ConfigWriterTest extends TestCase ->expects($this->any()) ->method('getInternalConfigPath') ->willReturn($this->internalConfigPath); + + $this->config + ->expects($this->any()) + ->method('getStateConfigPath') + ->willReturn($this->stateConfigPath); } public function testSave1(): void @@ -121,6 +128,7 @@ class ConfigWriterTest extends TestCase ->willReturnMap([ [$this->configPath, true], [$this->internalConfigPath, false], + [$this->stateConfigPath, false], ]); $this->fileManager @@ -145,6 +153,7 @@ class ConfigWriterTest extends TestCase $this->configWriter->set('k2', 'v2'); $this->internalConfigHelper + ->expects($this->any()) ->method('isParamForInternalConfig') ->willReturnMap( [ @@ -154,8 +163,19 @@ class ConfigWriterTest extends TestCase ] ); + $this->internalConfigHelper + ->expects($this->any()) + ->method('isParamForStateConfig') + ->willReturnMap( + [ + ['k1', false], + ['k2', false], + ['cacheTimestamp', true], + ] + ); + $this->helper - ->expects($this->exactly(2)) + ->expects($this->exactly(3)) ->method('generateMicrotime') ->willReturn(1.0); @@ -170,16 +190,16 @@ class ConfigWriterTest extends TestCase ->willReturnMap([ [$this->configPath, true], [$this->internalConfigPath, true], + [$this->stateConfigPath, true], ]); $this->fileManager - ->expects($this->exactly(4)) + ->expects($this->exactly(6)) ->method('getPhpContents') ->willReturnMap([ [$this->configPath, []], [$this->internalConfigPath, []], - [$this->internalConfigPath, []], - [$this->configPath, []], + [$this->stateConfigPath, []], ]); $this->fileManager @@ -192,7 +212,156 @@ class ConfigWriterTest extends TestCase ], [ $this->configPath, - ['k1' => 'v1', 'cacheTimestamp' => 1, 'microtime' => 1.0] + ['k1' => 'v1', 'microtime' => 1.0] + ], + [ + $this->stateConfigPath, + ['cacheTimestamp' => 1, 'microtimeState' => 1.0] + ], + ]); + + $this->configWriter->save(); + } + + public function testSave3(): void + { + $this->configWriter->set('k1', 'v1'); + $this->configWriter->set('k2', 'v2'); + + $this->internalConfigHelper + ->expects($this->any()) + ->method('isParamForInternalConfig') + ->willReturnMap( + [ + ['k1', false], + ['k2', true], + ['cacheTimestamp', false], + ] + ); + + $this->internalConfigHelper + ->expects($this->any()) + ->method('isParamForStateConfig') + ->willReturnMap( + [ + ['k1', false], + ['k2', false], + ['cacheTimestamp', true], + ] + ); + + $this->helper + ->expects($this->exactly(3)) + ->method('generateMicrotime') + ->willReturn(1.0); + + $this->helper + ->expects($this->once()) + ->method('generateCacheTimestamp') + ->willReturn(1); + + $this->fileManager + ->expects(self::any()) + ->method('isFile') + ->willReturnMap([ + [$this->configPath, true], + [$this->internalConfigPath, true], + [$this->stateConfigPath, true], + ]); + + $this->fileManager + ->expects($this->exactly(6)) + ->method('getPhpContents') + ->willReturnMap([ + [$this->configPath, []], + [$this->internalConfigPath, []], + [$this->stateConfigPath, []], + ]); + + $this->fileManager + ->expects(self::any()) + ->method('putPhpContents') + ->willReturnMap([ + [ + $this->internalConfigPath, + ['k2' => 'v2', 'microtimeInternal' => 1.0] + ], + [ + $this->configPath, + ['k1' => 'v1', 'microtime' => 1.0] + ], + [ + $this->stateConfigPath, + ['cacheTimestamp' => 1, 'microtimeState' => 1.0] + ], + ]); + + $this->configWriter->save(); + } + + public function testsRemove1(): void + { + $this->configWriter->remove('k1'); + + $this->internalConfigHelper + ->expects($this->any()) + ->method('isParamForInternalConfig') + ->willReturnMap( + [ + ['k1', false], + ['cacheTimestamp', false], + ] + ); + + $this->internalConfigHelper + ->expects($this->any()) + ->method('isParamForStateConfig') + ->willReturnMap( + [ + ['k1', false], + ['cacheTimestamp', false], + ] + ); + + $this->helper + ->expects($this->once()) + ->method('generateCacheTimestamp') + ->willReturn(1); + + $this->fileManager + ->expects(self::any()) + ->method('isFile') + ->willReturnMap([ + [$this->configPath, true], + [$this->internalConfigPath, true], + [$this->stateConfigPath, true], + ]); + + $this->fileManager + ->expects(self::any()) + ->method('isFile') + ->willReturnMap([ + [$this->configPath, true], + [$this->internalConfigPath, true], + [$this->stateConfigPath, true], + ]); + + $this->fileManager + ->expects($this->exactly(4)) + ->method('getPhpContents') + ->willReturnMap([ + [$this->configPath, ['k1' => 'v1', 'k2' => 'v2']], + [$this->internalConfigPath, []], + [$this->stateConfigPath, []], + ]); + + $this->fileManager + ->expects($this->exactly(1)) + ->method('putPhpContents') + ->willReturnMap([ + [ + $this->configPath, + ['k2' => 'v2', 'microtime' => 1.0] ], ]); diff --git a/tests/unit/Espo/Core/Utils/ConfigTest.php b/tests/unit/Espo/Core/Utils/ConfigTest.php index f7de8fea9b..7b5e969a69 100644 --- a/tests/unit/Espo/Core/Utils/ConfigTest.php +++ b/tests/unit/Espo/Core/Utils/ConfigTest.php @@ -43,6 +43,7 @@ class ConfigTest extends TestCase private $configPath = 'tests/unit/testData/cache/config.php'; private $systemConfigPath = 'tests/unit/testData/Utils/Config/systemConfig.php'; private $internalConfigPath = 'tests/unit/testData/cache/config-internal.php'; + private $stateConfigPath = 'tests/unit/testData/cache/state.php'; private $reflection; private $fileManager; @@ -63,11 +64,7 @@ class ConfigTest extends TestCase $this->reflection->setProperty('configPath', $this->configPath); $this->reflection->setProperty('systemConfigPath', $this->systemConfigPath); $this->reflection->setProperty('internalConfigPath', $this->internalConfigPath); - } - - protected function tearDown(): void - { - $this->config = NULL; + $this->reflection->setProperty('stateConfigPath', $this->stateConfigPath); } public function testLoadConfig() @@ -123,6 +120,7 @@ class ConfigTest extends TestCase ['data/config-internal.php', true], ['application/Espo/Resources/defaults/systemConfig.php', true], ['data/config-override.php', false], + ['data/state.php', true], ['data/config-internal-override.php', false], ] ); @@ -145,6 +143,7 @@ class ConfigTest extends TestCase [ ['data/config.php', $data], ['data/config-internal.php', $dataInternal], + ['data/state.php', []], ['application/Espo/Resources/defaults/systemConfig.php', $dataSystem], ] ); @@ -181,6 +180,7 @@ class ConfigTest extends TestCase ['data/config-internal.php', true], ['application/Espo/Resources/defaults/systemConfig.php', true], ['data/config-override.php', false], + ['data/state.php', true], ['data/config-internal-override.php', false], ] ); @@ -208,6 +208,7 @@ class ConfigTest extends TestCase [ ['data/config.php', $data], ['data/config-internal.php', $dataInternal], + ['data/state.php', []], ['application/Espo/Resources/defaults/systemConfig.php', $dataSystem], ] ); @@ -239,6 +240,7 @@ class ConfigTest extends TestCase ['data/config.php', true], ['data/config-internal.php', true], ['data/config-override.php', true], + ['data/state.php', true], ['data/config-internal-override.php', true], ] ); @@ -273,6 +275,7 @@ class ConfigTest extends TestCase ['data/config.php', $data], ['data/config-internal.php', $dataInternal], ['data/config-override.php', $dataOverride], + ['data/state.php', []], ['data/config-internal-override.php', $dataInternalOverride], ] );