diff --git a/Model/Processor/ImportProcessor.php b/Model/Processor/ImportProcessor.php index 470b643..56ac276 100644 --- a/Model/Processor/ImportProcessor.php +++ b/Model/Processor/ImportProcessor.php @@ -19,6 +19,8 @@ class ImportProcessor extends AbstractProcessor implements ImportProcessorInterface { private const DELETE_CONFIG_FLAG = '!!DELETE'; + private const KEEP_CONFIG_FLAG = '!!KEEP'; + /** * @var WriterInterface */ @@ -85,46 +87,87 @@ public function __construct( public function process() { $files = $this->finder->find(); - if (0 === count($files) && false === $this->getInput()->getOption('allow-empty-directories')) { - throw new \InvalidArgumentException('No files found for format: *.' . $this->getFormat()); - } else { - $this->getOutput()->writeln('No files found for format: *.' . $this->getFormat()); - $this->getOutput()->writeln('Maybe this is expected behaviour, because you passed the --allow-empty-directories option.'); + + if (0 === count($files)) { + if (false === $this->getInput()->getOption('allow-empty-directories')) { + throw new \InvalidArgumentException('No files found for format: *.' . $this->getFormat()); + } else { + $this->getOutput()->writeln('No files found for format: *.' . $this->getFormat()); + $this->getOutput()->writeln('Maybe this is expected behaviour, because you passed the --allow-empty-directories option.'); + $this->getOutput()->writeln(' '); + } } - foreach ($files as $file) { - $valuesSet = 0; - $configurations = $this->getConfigurationsFromFile($file); - foreach ($configurations as $configPath => $configValues) { - $scopeConfigValues = $this->transformConfigToScopeConfig($configPath, $configValues); - foreach ($scopeConfigValues as $scopeConfigValue) { - if ($scopeConfigValue['value'] === self::DELETE_CONFIG_FLAG) { - $this->configWriter->delete( - $configPath, - $scopeConfigValue['scope'], - $this->scopeConverter->convert($scopeConfigValue['scope_id'], $scopeConfigValue['scope']) - ); + $configurationValues = $this->collectConfigurationValues($files); + if (0 === count($configurationValues)) { + return; + } - $this->getOutput()->writeln(sprintf('%s => %s', $configPath, 'DELETED')); - $valuesSet++; + foreach ($configurationValues as $configPath => $configValue) { + foreach ($configValue as $scopeType => $scopeValue) { + foreach ($scopeValue as $scopeId => $value) { + if ($value === self::DELETE_CONFIG_FLAG) { + $this->configWriter->delete($configPath, $scopeType, $scopeId); + $this->getOutput()->writeln(sprintf('[%s] [%s] %s => %s', $scopeType, $scopeId, $configPath, 'DELETED')); continue; } - $this->configWriter->save( - $configPath, - $scopeConfigValue['value'], - $scopeConfigValue['scope'], - $this->scopeConverter->convert($scopeConfigValue['scope_id'], $scopeConfigValue['scope']) - ); + if ($value === self::KEEP_CONFIG_FLAG) { + $this->getOutput()->writeln(sprintf('[%s] [%s] %s => %s', $scopeType, $scopeId, $configPath, 'KEPT')); + + continue; + } + + $this->configWriter->save($configPath, $value, $scopeType, $scopeId); + $this->getOutput()->writeln(sprintf('[%s] [%s] %s => %s', $scopeType, $scopeId, $configPath, $value)); + } + } + } + } + + /** + * @param array $files + * + * @return array + */ + private function collectConfigurationValues(array $files): array + { + $buffer = []; - $this->getOutput()->writeln(sprintf('%s => %s', $configPath, $scopeConfigValue['value'])); + foreach ($files as $file) { + $valuesSet = 0; + + $configurations = $this->getConfigurationsFromFile($file); + foreach ($configurations as $configPath => $configValues) { + if (!isset($buffer[$configPath])) { + $buffer[$configPath] = []; + } + + $scopeConfigValues = $this->transformConfigToScopeConfig($configPath, $configValues); + foreach ($scopeConfigValues as $scopeConfigValue) { + $scopeType = $scopeConfigValue['scope']; + $scopeId = $this->scopeConverter->convert($scopeConfigValue['scope_id'], $scopeConfigValue['scope']); + $buffer[$configPath][$scopeType][$scopeId] = $scopeConfigValue['value']; $valuesSet++; } } - $this->getOutput()->writeln(sprintf('Processed: %s with %s value(s).', $file, $valuesSet)); + if (0 === $valuesSet) { + continue; + } + + $this->getOutput()->writeln(sprintf( + 'Collected configuration values from %s with %s %s.', + $file, + $valuesSet, + $valuesSet === 1 ? 'value' : 'values' + )); } + + $this->getOutput()->writeln(' '); // Add empty line to make output in terminal nicer. + + return $buffer; } /** diff --git a/Test/Unit/Model/Processor/ImportProcessorTest.php b/Test/Unit/Model/Processor/ImportProcessorTest.php index e1be178..c54362a 100644 --- a/Test/Unit/Model/Processor/ImportProcessorTest.php +++ b/Test/Unit/Model/Processor/ImportProcessorTest.php @@ -14,6 +14,7 @@ use Semaio\ConfigImportExport\Model\File\Reader\YamlReader; use Semaio\ConfigImportExport\Model\Processor\ImportProcessor; use Semaio\ConfigImportExport\Model\Validator\ScopeValidatorInterface; +use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class ImportProcessorTest extends TestCase @@ -67,6 +68,9 @@ public function processWithoutFiles(): void $this->expectException(InvalidArgumentException::class); $processor = new ImportProcessor($this->configWriterMock, $this->scopeValidatorMock, $this->scopeConverterMock, []); + $inputMock = $this->getMockBuilder(InputInterface::class)->getMock(); + $inputMock->method('getOption')->with('allow-empty-directories')->willReturn(false); + $processor->setInput($inputMock); $processor->setFinder($finderMock); $processor->process(); } @@ -126,6 +130,16 @@ public function process(): void 0 => '!!DELETE', ], ], + 'test/config/custom_field_to_be_keeped' => [ + 'default' => [ + 0 => 'VALUE_THAT_SHOULD_NOT_BE_PROCESSED', + ], + ], + 'test/config/custom_field_to_be_keeped' => [ + 'default' => [ + 0 => '!!KEEP', + ], + ], ]; $readerMock = $this->getMockBuilder(YamlReader::class) @@ -133,7 +147,7 @@ public function process(): void ->getMock(); $readerMock->expects($this->once())->method('parse')->willReturn($parseResult); - $this->scopeValidatorMock->expects($this->exactly(2))->method('validate')->willReturn(true); + $this->scopeValidatorMock->expects($this->exactly(3))->method('validate')->willReturn(true); $this->configWriterMock->expects($this->once())->method('save'); $this->configWriterMock->expects($this->once())->method('delete'); diff --git a/docs/config-import.md b/docs/config-import.md index 05bfd53..a54645f 100644 --- a/docs/config-import.md +++ b/docs/config-import.md @@ -97,6 +97,18 @@ vendorx/general/api_key: 0: "!!DELETE" ``` +### Keep Config + +To ensure that a specific configuration value will not be changed by the config importer, please use the following string as configuration value: + +```yaml +vendorx/general/api_key: + default: + 0: "!!KEEP" +``` + +This is helpful when you've got the same settings across different environments but want to keep one environment ( `X` env) unchanged without showing the exact value in the config file. It's a common scenario, especially when dealing with sensitive data. You really should only keep that kind of info in the environment’s database, not in your GIT repo. + ### Recursive folder setup If you choose to store your configuration files in subdirectories, e.g. per vendor, the recommended folder setup should look like this: