Skip to content

Commit

Permalink
Merge branch 'semaio:main' into feature/issue-64-include-scope-id
Browse files Browse the repository at this point in the history
  • Loading branch information
wilfriedwolf authored Feb 1, 2025
2 parents 7e9bb51 + 12e0c5d commit b61d2ed
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 29 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* ...

## [4.4.0] - 2024-08-29

### Changed

* Update symfony/yaml dependency to include ^7.0 (see [#79](https://github.com/semaio/Magento2-ConfigImportExport/pull/79)) by [@mp-sb](https://github.com/mp-sb)

## [4.3.0] - 2024-04-18

### Added

* Support import for encrypted configuration values (see [#74](https://github.com/semaio/Magento2-ConfigImportExport/pull/74)) by [@Maksold](https://github.com/Maksold)
* Add support for keeping and not overwriting configuration values in specific environments (see [#75](https://github.com/semaio/Magento2-ConfigImportExport/pull/75)) by [@vpodorozh](https://github.com/vpodorozh)

## [4.2.0] - 2023-09-29

### Added
Expand Down
97 changes: 70 additions & 27 deletions Model/Processor/ImportProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
class ImportProcessor extends AbstractProcessor implements ImportProcessorInterface
{
private const DELETE_CONFIG_FLAG = '!!DELETE';
private const KEEP_CONFIG_FLAG = '!!KEEP';

/**
* @var WriterInterface
*/
Expand Down Expand Up @@ -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('<comment>%s => %s</comment>', $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('<comment>[%s] [%s] %s => %s</comment>', $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('<comment>[%s] [%s] %s => %s</comment>', $scopeType, $scopeId, $configPath, 'KEPT'));

continue;
}

$this->configWriter->save($configPath, $value, $scopeType, $scopeId);
$this->getOutput()->writeln(sprintf('<comment>[%s] [%s] %s => %s</comment>', $scopeType, $scopeId, $configPath, $value));
}
}
}
}

/**
* @param array $files
*
* @return array
*/
private function collectConfigurationValues(array $files): array
{
$buffer = [];

$this->getOutput()->writeln(sprintf('<comment>%s => %s</comment>', $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('<info>Processed: %s with %s value(s).</info>', $file, $valuesSet));
if (0 === $valuesSet) {
continue;
}

$this->getOutput()->writeln(sprintf(
'<info>Collected configuration values from %s with %s %s.</info>',
$file,
$valuesSet,
$valuesSet === 1 ? 'value' : 'values'
));
}

$this->getOutput()->writeln(' '); // Add empty line to make output in terminal nicer.

return $buffer;
}

/**
Expand Down
64 changes: 64 additions & 0 deletions Model/Resolver/EncryptResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php
/**
* Copyright © semaio GmbH. All rights reserved.
* See LICENSE.md bundled with this module for license details.
*/

namespace Semaio\ConfigImportExport\Model\Resolver;

use Magento\Framework\Encryption\EncryptorInterface;
use Semaio\ConfigImportExport\Exception\UnresolveableValueException;
use function strlen;

class EncryptResolver extends AbstractResolver
{
/**
* @var EncryptorInterface
*/
private $encryptor;

public function __construct(EncryptorInterface $encryptor)
{
$this->encryptor = $encryptor;
}

/**
* Resolve the config value if wrapped with '%encrypt(value)%', this method encrypts the value.
*
* @param string|null $value
* @param string|null $configPath
*
* @return string|null
*
* @throws UnresolveableValueException
*/
public function resolve($value, $configPath = null)
{
if ($value === null) {
return null;
}

$value = (string)$value;
if ($value === '%encrypt()%') {
throw new UnresolveableValueException('Please specify a valid value to encrypt.');
}

$valueToEncrypt = preg_replace_callback(
'/\%encrypt\(([^)]+)\)\%/',
function ($matches) {
return $matches[1];
},
$value
);

return $this->encryptor->encrypt($valueToEncrypt);
}

/**
* @inheritDoc
*/
public function supports($value, $configPath = null): bool
{
return 0 === strncmp((string)$value, '%encrypt', strlen('%encrypt'));
}
}
16 changes: 15 additions & 1 deletion Test/Unit/Model/Processor/ImportProcessorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -126,14 +130,24 @@ 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)
->onlyMethods(['parse'])
->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');

Expand Down
112 changes: 112 additions & 0 deletions Test/Unit/Model/Resolver/EncryptResolverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php
/**
* Copyright © semaio GmbH. All rights reserved.
* See LICENSE.md bundled with this module for license details.
*/

namespace Semaio\ConfigImportExport\Test\Unit\Model\Validator;

use Generator;
use Magento\Framework\Encryption\EncryptorInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Semaio\ConfigImportExport\Exception\UnresolveableValueException;
use Semaio\ConfigImportExport\Model\Resolver\EncryptResolver;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class EncryptResolverTest extends TestCase
{
/**
* @var InputInterface
*/
private $input;

/**
* @var OutputInterface
*/
private $output;

/**
* @var QuestionHelper
*/
private $questionHelper;

/**
* @var MockObject|EncryptorInterface
*/
private $encryptor;

/**
* Set up test class
*/
protected function setUp(): void
{
parent::setUp();

$this->input = $this->createMock(InputInterface::class);
$this->output = $this->createMock(OutputInterface::class);
$this->questionHelper = $this->createMock(QuestionHelper::class);
$this->encryptor = $this->createMock(EncryptorInterface::class);
}

/**
* @test
*
* @dataProvider resolveDataProvider
*/
public function validate($value, $expectedResult): void
{
$this->encryptor->expects($this->any())
->method('encrypt')
->with($expectedResult)
->willReturn($expectedResult);

$this->assertEquals($this->getEncryptResolver()->resolve($value), $expectedResult);
}

public function resolveDataProvider(): Generator
{
yield [
'test_without_data_to_encrypt',
'test_without_data_to_encrypt',
];
yield [
'%encrypt(data_to_encrypt)%',
'data_to_encrypt',
];
yield [
null,
'',
];
yield [
false,
'',
];
yield [
true,
'1',
];
}

public function testItWillRaiseErrorIfEncryptValueIsEmpty(): void
{
$this->expectException(UnresolveableValueException::class);

$this->getEncryptResolver()->resolve('%encrypt()%');
}

/**
* @return EncryptResolver
*/
private function getEncryptResolver()
{
$resolver = new EncryptResolver($this->encryptor);
$resolver->setInput($this->input);
$resolver->setOutput($this->output);
$resolver->setQuestionHelper($this->questionHelper);

return $resolver;
}
}
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "Import/Export core_config_data values in Magento 2",
"require": {
"php": "^7.2|^8.1",
"symfony/yaml": "^3.4|^4.0|^5.0|^6.0",
"symfony/yaml": "^3.4|^4.0|^5.0|^6.0|^7.0",
"magento/module-config": "*",
"magento/module-store": "*",
"magento/framework": "*"
Expand Down
Loading

0 comments on commit b61d2ed

Please sign in to comment.