diff --git a/Command/ImportCommand.php b/Command/ImportCommand.php index 4146919..84c80b2 100644 --- a/Command/ImportCommand.php +++ b/Command/ImportCommand.php @@ -134,6 +134,13 @@ protected function configure() 'Allow empty directories during import.' ); + $this->addOption( + 'lock-config', + 'c', + InputOption::VALUE_NONE, + 'Additionally lock imported values in app/etc/config.php (read-only in Admin).' + ); + parent::configure(); } diff --git a/Model/Processor/ImportProcessor.php b/Model/Processor/ImportProcessor.php index 56ac276..810eb8b 100644 --- a/Model/Processor/ImportProcessor.php +++ b/Model/Processor/ImportProcessor.php @@ -6,7 +6,12 @@ namespace Semaio\ConfigImportExport\Model\Processor; +use Magento\Config\App\Config\Type\System; +use Magento\Framework\App\Config\ConfigPathResolver; use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\Framework\App\DeploymentConfig\Writer as DeploymentConfigWriter; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Stdlib\ArrayManager; use Semaio\ConfigImportExport\Exception\UnresolveableValueException; use Semaio\ConfigImportExport\Model\Converter\ScopeConverterInterface; use Semaio\ConfigImportExport\Model\File\FinderInterface; @@ -61,21 +66,45 @@ class ImportProcessor extends AbstractProcessor implements ImportProcessorInterf */ private $resolvers; + /** + * @var DeploymentConfigWriter + */ + private $deploymentConfigWriter; + + /** + * @var ConfigPathResolver + */ + private $configPathResolver; + + /** + * @var ArrayManager + */ + private $arrayManager; + /** * @param WriterInterface $configWriter * @param ScopeValidatorInterface $scopeValidator * @param ScopeConverterInterface $scopeConverter + * @param DeploymentConfigWriter $deploymentConfigWriter + * @param ConfigPathResolver $configPathResolver + * @param ArrayManager $arrayManager * @param ResolverInterface[] $resolvers */ public function __construct( WriterInterface $configWriter, ScopeValidatorInterface $scopeValidator, ScopeConverterInterface $scopeConverter, + DeploymentConfigWriter $deploymentConfigWriter, + ConfigPathResolver $configPathResolver, + ArrayManager $arrayManager, array $resolvers = [] ) { $this->configWriter = $configWriter; $this->scopeValidator = $scopeValidator; $this->scopeConverter = $scopeConverter; + $this->deploymentConfigWriter = $deploymentConfigWriter; + $this->configPathResolver = $configPathResolver; + $this->arrayManager = $arrayManager; $this->resolvers = $resolvers; } @@ -103,6 +132,9 @@ public function process() return; } + $shouldLock = $this->shouldLockConfig(); + $lockData = []; + foreach ($configurationValues as $configPath => $configValue) { foreach ($configValue as $scopeType => $scopeValue) { foreach ($scopeValue as $scopeId => $value) { @@ -110,6 +142,12 @@ public function process() $this->configWriter->delete($configPath, $scopeType, $scopeId); $this->getOutput()->writeln(sprintf('[%s] [%s] %s => %s', $scopeType, $scopeId, $configPath, 'DELETED')); + if ($shouldLock) { + $this->getOutput()->writeln( + sprintf(' Note: If this path is locked in app/etc/config.php, remove it manually.') + ); + } + continue; } @@ -121,9 +159,62 @@ public function process() $this->configWriter->save($configPath, $value, $scopeType, $scopeId); $this->getOutput()->writeln(sprintf('[%s] [%s] %s => %s', $scopeType, $scopeId, $configPath, $value)); + + if ($shouldLock) { + $lockData[] = [ + 'path' => $configPath, + 'value' => $value, + 'scope' => $scopeType, + 'scope_id' => $scopeId, + ]; + } } } } + + if ($shouldLock && !empty($lockData)) { + $this->writeToConfigPhp($lockData); + } + } + + /** + * Check if --lock-config option is enabled. + */ + private function shouldLockConfig(): bool + { + $input = $this->getInput(); + return $input && $input->hasOption('lock-config') && $input->getOption('lock-config'); + } + + /** + * Batch-write collected values to app/etc/config.php. + */ + private function writeToConfigPhp(array $lockData): void + { + $this->getOutput()->writeln(' '); + $this->getOutput()->writeln('Locking values in app/etc/config.php...'); + + $configArray = []; + foreach ($lockData as $item) { + $resolvedPath = $this->configPathResolver->resolve( + $item['path'], + $item['scope'], + $item['scope_id'], + System::CONFIG_TYPE + ); + $configArray = $this->arrayManager->set($resolvedPath, $configArray, $item['value']); + } + + $this->deploymentConfigWriter->saveConfig( + [ConfigFilePool::APP_CONFIG => $configArray], + false + ); + + $this->getOutput()->writeln(sprintf( + '%d %s locked in app/etc/config.php.', + count($lockData), + count($lockData) === 1 ? 'value' : 'values' + )); } /** diff --git a/README.md b/README.md index 6dd2387..2de55b5 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,16 @@ See [docs/file-formats.md](docs/file-formats.md) for more information and exampl See [docs/config-import.md](docs/config-import.md) for more information. +#### Lock config values + +You can lock imported values in `app/etc/config.php` to make them read-only in the Admin panel: + +```bash +php bin/magento config:data:import config/store production --lock-config +``` + +This writes values to both the database and `config.php`. Locked values appear greyed out in Admin > Stores > Configuration. See the [import docs](docs/config-import.md#lock-config) for details. + ### Export config data diff --git a/docs/config-import.md b/docs/config-import.md index 4b4e8c4..b042eb5 100644 --- a/docs/config-import.md +++ b/docs/config-import.md @@ -20,6 +20,7 @@ $ php bin/magento config:data:import --help --recursive (-r) Recursively go over subdirectories and import configs. --prompt-missing-env-vars (-p) Prompt in interactive mode when environment variables are found but not configured (Default: true) --allow-empty-directories (-e) Do not throw error if import directories are empty. + --lock-config (-c) Additionally lock imported values in app/etc/config.php (read-only in Admin). ``` :exclamation: Only use the `no-cache` option if you clear the cache afterwards, e.g. in a deployment process. Otherwise the changes will have no effect. @@ -123,6 +124,20 @@ vendorx/general/api_key: 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. +### Lock Config + +By default, imported values are written to the database (`core_config_data`) and remain editable in the Admin panel. If you want to additionally lock the values so they become **read-only in Admin**, use the `--lock-config` option: + +```bash +php bin/magento config:data:import config/store production --lock-config +``` + +This writes the imported values to both the database **and** `app/etc/config.php`. Values stored in `config.php` take precedence over database values and appear greyed out (non-editable) in Admin > Stores > Configuration. + +:exclamation: When using `--lock-config` together with `!!DELETE`, the value is removed from the database but **not** from `config.php`. If the value was previously locked, you need to remove it from `app/etc/config.php` manually. + +:exclamation: The `--lock-config` option writes to the filesystem. Make sure `app/etc/config.php` is writable by the PHP process. + ### 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: