diff --git a/app/code/core/Mage/Core/Helper/EnvironmentConfigLoader.php b/app/code/core/Mage/Core/Helper/EnvironmentConfigLoader.php new file mode 100644 index 00000000000..61abc39e84e --- /dev/null +++ b/app/code/core/Mage/Core/Helper/EnvironmentConfigLoader.php @@ -0,0 +1,158 @@ +__ + * ^ Where scope is DEFAULT, WEBSITES__ or STORES__ + * + * ^ Where GROUP, SECTION and FIELD are separated by self::ENV_KEY_SEPARATOR + * + * Each example will override the 'general/store_information/name' value. + * Override from the default configuration: + * @example OPENMAGE_CONFIG__DEFAULT__GENERAL__STORE_INFORMATION__NAME=default + * Override the website 'base' configuration: + * @example OPENMAGE_CONFIG__WEBSITES__BASE__GENERAL__STORE_INFORMATION__NAME=website + * Override the store 'german' configuration: + * @example OPENMAGE_CONFIG__STORES__GERMAN__GENERAL__STORE_INFORMATION__NAME=store_german + * + * @param Varien_Simplexml_Config $xmlConfig + * @return void + */ + public function overrideEnvironment(Varien_Simplexml_Config $xmlConfig) + { + $env = $this->getEnv(); + + foreach ($env as $configKey => $value) { + if (!$this->isConfigKeyValid($configKey)) { + continue; + } + + list($configKeyParts, $scope) = $this->getConfigKey($configKey); + + switch ($scope) { + case static::CONFIG_KEY_DEFAULT: + list($_, $_, $section, $group, $field) = $configKeyParts; + $path = $this->buildPath($section, $group, $field); + $xmlConfig->setNode($this->buildNodePath($scope, $path), $value); + break; + + case static::CONFIG_KEY_WEBSITES: + case static::CONFIG_KEY_STORES: + list($_, $_, $code, $section, $group, $field) = $configKeyParts; + $path = $this->buildPath($section, $group, $field); + $nodePath = sprintf('%s/%s/%s', strtolower($scope), strtolower($code), $path); + $xmlConfig->setNode($nodePath, $value); + break; + } + } + } + + /** + * @internal method mostly for mocking + */ + public function setEnvStore(array $envStorage): void + { + $this->envStore = $envStorage; + } + + public function getEnv(): array + { + if (empty($this->envStore)) { + $this->envStore = getenv(); + } + return $this->envStore; + } + + protected function getConfigKey(string $configKey): array + { + $configKeyParts = array_filter( + explode( + static::ENV_KEY_SEPARATOR, + $configKey + ), + 'trim' + ); + list($_, $scope) = $configKeyParts; + return array($configKeyParts, $scope); + } + + protected function isConfigKeyValid(string $configKey): bool + { + if (!str_starts_with($configKey, static::ENV_STARTS_WITH)) { + return false; + } + + $sectionGroupFieldRegexp = sprintf('([%s]*)', implode('', static::ALLOWED_CHARS)); + $allowedChars = sprintf('[%s]', implode('', static::ALLOWED_CHARS)); + $regexp = '/' . static::ENV_STARTS_WITH . static::ENV_KEY_SEPARATOR . '(WEBSITES' . static::ENV_KEY_SEPARATOR + . $allowedChars . '+|DEFAULT|STORES' . static::ENV_KEY_SEPARATOR . $allowedChars . '+)' + . static::ENV_KEY_SEPARATOR . $sectionGroupFieldRegexp + . static::ENV_KEY_SEPARATOR . $sectionGroupFieldRegexp + . static::ENV_KEY_SEPARATOR . $sectionGroupFieldRegexp . '/'; + // /OPENMAGE_CONFIG__(WEBSITES__[A-Z-_]+|DEFAULT|STORES__[A-Z-_]+)__([A-Z-_]*)__([A-Z-_]*)__([A-Z-_]*)/ + + return preg_match($regexp, $configKey); + } + + /** + * Build configuration path. + * + * @param string $section + * @param string $group + * @param string $field + * @return string + */ + protected function buildPath(string $section, string $group, string $field): string + { + return strtolower(implode('/', [$section, $group, $field])); + } + + /** + * Build configuration node path. + * + * @param string $scope + * @param string $path + * @return string + */ + protected function buildNodePath(string $scope, string $path): string + { + return strtolower($scope) . '/' . $path; + } +} diff --git a/app/code/core/Mage/Core/Model/App.php b/app/code/core/Mage/Core/Model/App.php index a44360011fd..9989bd21744 100644 --- a/app/code/core/Mage/Core/Model/App.php +++ b/app/code/core/Mage/Core/Model/App.php @@ -442,6 +442,7 @@ protected function _initModules() Varien_Profiler::stop('mage::app::init::apply_db_schema_updates'); } $this->_config->loadDb(); + $this->_config->loadEnv(); $this->_config->saveCache(); } } finally { diff --git a/app/code/core/Mage/Core/Model/Config.php b/app/code/core/Mage/Core/Model/Config.php index b4498800c31..e5a6fb86b6d 100644 --- a/app/code/core/Mage/Core/Model/Config.php +++ b/app/code/core/Mage/Core/Model/Config.php @@ -320,6 +320,7 @@ public function init($options = []) $this->_useCache = false; $this->loadModules(); $this->loadDb(); + $this->loadEnv(); $this->saveCache(); } } finally { @@ -422,6 +423,23 @@ public function loadDb() return $this; } + /** + * Load environment variables and override config + * + * @return self + */ + public function loadEnv(): Mage_Core_Model_Config + { + if ($this->_isLocalConfigLoaded && Mage::isInstalled()) { + Varien_Profiler::start('config/load-env'); + /** @var Mage_Core_Helper_EnvironmentConfigLoader $environmentConfigLoaderHelper */ + $environmentConfigLoaderHelper = Mage::helper('core/environmentConfigLoader'); + $environmentConfigLoaderHelper->overrideEnvironment($this); + Varien_Profiler::stop('config/load-env'); + } + return $this; + } + /** * Reinitialize configuration * diff --git a/dev/tests/unit/Mage/Core/Helper/EnvironmentConfigLoaderTest.php b/dev/tests/unit/Mage/Core/Helper/EnvironmentConfigLoaderTest.php new file mode 100644 index 00000000000..ed7911d29ba --- /dev/null +++ b/dev/tests/unit/Mage/Core/Helper/EnvironmentConfigLoaderTest.php @@ -0,0 +1,315 @@ +buildPath($section, $group, $field); + } + + public function exposedBuildNodePath(string $scope, string $path): string + { + return $this->buildNodePath($scope, $path); + } +} + +class EnvironmentConfigLoaderTest extends TestCase +{ + public function setup(): void + { + \Mage::setRoot(''); + } + + public function testBuildPath() + { + $environmentConfigLoaderHelper = new TestEnvLoaderHelper(); + $path = $environmentConfigLoaderHelper->exposedBuildPath('GENERAL', 'STORE_INFORMATION', 'NAME'); + $this->assertEquals('general/store_information/name', $path); + } + + public function testBuildNodePath() + { + $environmentConfigLoaderHelper = new TestEnvLoaderHelper(); + $nodePath = $environmentConfigLoaderHelper->exposedBuildNodePath('DEFAULT', 'general/store_information/name'); + $this->assertEquals('default/general/store_information/name', $nodePath); + } + + public function test_xml_has_test_strings() + { + $xmlStruct = $this->getTestXml(); + $xml = new \Varien_Simplexml_Config(); + $xml->loadString($xmlStruct); + $this->assertEquals('test_default', (string)$xml->getNode('default/general/store_information/name')); + $this->assertEquals('test_website', (string)$xml->getNode('websites/base/general/store_information/name')); + $this->assertEquals('test_store', (string)$xml->getNode('stores/german/general/store_information/name')); + } + + /** + * @dataProvider env_overrides_correct_config_keys + * @test + */ + public function env_overrides_for_valid_config_keys(array $config) + { + $xmlStruct = $this->getTestXml(); + + $xmlDefault = new \Varien_Simplexml_Config(); + $xmlDefault->loadString($xmlStruct); + $xml = new \Varien_Simplexml_Config(); + $xml->loadString($xmlStruct); + + // act + $loader = new Mage_Core_Helper_EnvironmentConfigLoader(); + $loader->setEnvStore([ + $config['env_path'] => $config['value'] + ]); + $loader->overrideEnvironment($xml); + + $configPath = $config['xml_path']; + $defaultValue = $xmlDefault->getNode($configPath); + $valueAfterOverride = $xml->getNode($configPath); + + // assert + $this->assertNotEquals((string)$defaultValue, (string)$valueAfterOverride, 'Default value was not overridden.'); + } + + public function env_overrides_correct_config_keys(): array + { + $defaultPath = 'OPENMAGE_CONFIG__DEFAULT__GENERAL__STORE_INFORMATION__NAME'; + $defaultPathWithDash = 'OPENMAGE_CONFIG__DEFAULT__GENERAL__FOO-BAR__NAME'; + $defaultPathWithUnderscore = 'OPENMAGE_CONFIG__DEFAULT__GENERAL__FOO_BAR__NAME'; + + $websitePath = 'OPENMAGE_CONFIG__WEBSITES__BASE__GENERAL__STORE_INFORMATION__NAME'; + $websiteWithDashPath = 'OPENMAGE_CONFIG__WEBSITES__BASE-AT__GENERAL__STORE_INFORMATION__NAME'; + $websiteWithUnderscorePath = 'OPENMAGE_CONFIG__WEBSITES__BASE_CH__GENERAL__STORE_INFORMATION__NAME'; + + $storeWithDashPath = 'OPENMAGE_CONFIG__STORES__GERMAN-AT__GENERAL__STORE_INFORMATION__NAME'; + $storeWithUnderscorePath = 'OPENMAGE_CONFIG__STORES__GERMAN_CH__GENERAL__STORE_INFORMATION__NAME'; + $storePath = 'OPENMAGE_CONFIG__STORES__GERMAN__GENERAL__STORE_INFORMATION__NAME'; + + return [ + [ + 'Case DEFAULT overrides.' => [ + 'case' => 'DEFAULT', + 'xml_path' => 'default/general/store_information/name', + 'env_path' => $defaultPath, + 'value' => 'default_new_value' + ] + ], + [ + 'Case DEFAULT overrides.' => [ + 'case' => 'DEFAULT', + 'xml_path' => 'default/general/foo-bar/name', + 'env_path' => $defaultPathWithDash, + 'value' => 'baz' + ] + ], + [ + 'Case DEFAULT overrides.' => [ + 'case' => 'DEFAULT', + 'xml_path' => 'default/general/foo_bar/name', + 'env_path' => $defaultPathWithUnderscore, + 'value' => 'baz' + ] + ], + [ + 'Case STORE overrides.' => [ + 'case' => 'STORE', + 'xml_path' => 'stores/german/general/store_information/name', + 'env_path' => $storePath, + 'value' => 'store_new_value' + ] + ], + [ + 'Case STORE overrides.' => [ + 'case' => 'STORE', + 'xml_path' => 'stores/german-at/general/store_information/name', + 'env_path' => $storeWithDashPath, + 'value' => 'store_new_value' + ] + ], + [ + 'Case STORE overrides.' => [ + 'case' => 'STORE', + 'xml_path' => 'stores/german_ch/general/store_information/name', + 'env_path' => $storeWithUnderscorePath, + 'value' => 'store_new_value' + ] + ], + [ + 'Case WEBSITE overrides.' => [ + 'case' => 'WEBSITE', + 'xml_path' => 'websites/base/general/store_information/name', + 'env_path' => $websitePath, + 'value' => 'website_new_value' + ] + ], + [ + 'Case WEBSITE overrides.' => [ + 'case' => 'WEBSITE', + 'xml_path' => 'websites/base_ch/general/store_information/name', + 'env_path' => $websiteWithUnderscorePath, + 'value' => 'website_new_value' + ] + ], + [ + 'Case WEBSITE overrides.' => [ + 'case' => 'WEBSITE', + 'xml_path' => 'websites/base-at/general/store_information/name', + 'env_path' => $websiteWithDashPath, + 'value' => 'website_new_value' + ] + ] + ]; + } + + /** + * @dataProvider env_does_not_override_on_wrong_config_keys + * @test + */ + public function env_does_not_override_for_invalid_config_keys(array $config) + { + $xmlStruct = $this->getTestXml(); + + $xmlDefault = new \Varien_Simplexml_Config(); + $xmlDefault->loadString($xmlStruct); + $xml = new \Varien_Simplexml_Config(); + $xml->loadString($xmlStruct); + + $defaultValue = 'test_default'; + $this->assertEquals($defaultValue, (string)$xml->getNode('default/general/store_information/name')); + $defaultWebsiteValue = 'test_website'; + $this->assertEquals($defaultWebsiteValue, (string)$xml->getNode('websites/base/general/store_information/name')); + $defaultStoreValue = 'test_store'; + $this->assertEquals($defaultStoreValue, (string)$xml->getNode('stores/german/general/store_information/name')); + + // act + $loader = new Mage_Core_Helper_EnvironmentConfigLoader(); + $loader->setEnvStore([ + $config['path'] => $config['value'] + ]); + $loader->overrideEnvironment($xml); + + switch ($config['case']) { + case 'DEFAULT': + $valueAfterCheck = $xml->getNode('default/general/store_information/name'); + break; + case 'STORE': + $valueAfterCheck = $xml->getNode('stores/german/general/store_information/name'); + break; + case 'WEBSITE': + $valueAfterCheck = $xml->getNode('websites/base/general/store_information/name'); + break; + } + + // assert + $this->assertTrue(!str_contains('value_will_not_be_changed', (string)$valueAfterCheck), 'Default value was wrongfully overridden.'); + } + + public function env_does_not_override_on_wrong_config_keys(): array + { + $defaultPath = 'OPENMAGE_CONFIG__DEFAULT__GENERAL__ST'; + $websitePath = 'OPENMAGE_CONFIG__WEBSITES__BASE__GENERAL__ST'; + $storePath = 'OPENMAGE_CONFIG__STORES__GERMAN__GENERAL__ST'; + return [ + [ + 'Case DEFAULT with ' . $defaultPath . ' will not override.' => [ + 'case' => 'DEFAULT', + 'path' => $defaultPath, + 'value' => 'default_value_will_not_be_changed' + ] + ], + [ + 'Case STORE with ' . $storePath . ' will not override.' => [ + 'case' => 'STORE', + 'path' => $storePath, + 'value' => 'store_value_will_not_be_changed' + ] + ], + [ + 'Case WEBSITE with ' . $websitePath . ' will not override.' => [ + 'case' => 'WEBSITE', + 'path' => $websitePath, + 'value' => 'website_value_will_not_be_changed' + ] + ] + ]; + } + + /** + * @return string + */ + public function getTestXml(): string + { + return << + + + + + test_default + + + test_default + + + test_default + + + + + + + + + test_website + + + + + + + test_website + + + + + + + test_website + + + + + + + + + test_store + + + + + + + test_store + + + + + + + test_store + + + + + +XML; + } +}