diff --git a/core/command/config/system/deleteconfig.php b/core/command/config/system/deleteconfig.php index e64ff32ac7..374f5ac69b 100644 --- a/core/command/config/system/deleteconfig.php +++ b/core/command/config/system/deleteconfig.php @@ -48,8 +48,8 @@ class DeleteConfig extends Base { ->setDescription('Delete a system config value') ->addArgument( 'name', - InputArgument::REQUIRED, - 'Name of the config to delete' + InputArgument::REQUIRED | InputArgument::IS_ARRAY, + 'Name of the config to delete, specify multiple for array parameter' ) ->addOption( 'error-if-not-exists', @@ -61,15 +61,57 @@ class DeleteConfig extends Base { } protected function execute(InputInterface $input, OutputInterface $output) { - $configName = $input->getArgument('name'); + $configNames = $input->getArgument('name'); + $configName = $configNames[0]; - if ($input->hasParameterOption('--error-if-not-exists') && !in_array($configName, $this->systemConfig->getKeys())) { - $output->writeln('System config ' . $configName . ' could not be deleted because it did not exist'); - return 1; + if (sizeof($configNames) > 1) { + if ($input->hasParameterOption('--error-if-not-exists') && !in_array($configName, $this->systemConfig->getKeys())) { + $output->writeln('System config ' . implode(' => ', $configNames) . ' could not be deleted because it did not exist'); + return 1; + } + + $value = $this->systemConfig->getValue($configName); + + try { + $value = $this->removeSubValue(array_slice($configNames, 1), $value, $input->hasParameterOption('--error-if-not-exists')); + } + catch (\UnexpectedValueException $e) { + $output->writeln('System config ' . implode(' => ', $configNames) . ' could not be deleted because it did not exist'); + return 1; + } + + $this->systemConfig->setValue($configName, $value); + $output->writeln('System config value ' . implode(' => ', $configNames) . ' deleted'); + return 0; + } else { + if ($input->hasParameterOption('--error-if-not-exists') && !in_array($configName, $this->systemConfig->getKeys())) { + $output->writeln('System config ' . $configName . ' could not be deleted because it did not exist'); + return 1; + } + + $this->systemConfig->deleteValue($configName); + $output->writeln('System config value ' . $configName . ' deleted'); + return 0; + } + } + + protected function removeSubValue($keys, $currentValue, $throwError) { + $nextKey = array_shift($keys); + + if (is_array($currentValue)) { + if (isset($currentValue[$nextKey])) { + if (empty($keys)) { + unset($currentValue[$nextKey]); + } else { + $currentValue[$nextKey] = $this->removeSubValue($keys, $currentValue[$nextKey], $throwError); + } + } else if ($throwError) { + throw new \UnexpectedValueException('Config parameter does not exist'); + } + } else if ($throwError) { + throw new \UnexpectedValueException('Config parameter does not exist'); } - $this->systemConfig->deleteValue($configName); - $output->writeln('System config value ' . $configName . ' deleted'); - return 0; + return $currentValue; } } diff --git a/core/command/config/system/getconfig.php b/core/command/config/system/getconfig.php index 80b41a6852..b76474112a 100644 --- a/core/command/config/system/getconfig.php +++ b/core/command/config/system/getconfig.php @@ -48,8 +48,8 @@ class GetConfig extends Base { ->setDescription('Get a system config value') ->addArgument( 'name', - InputArgument::REQUIRED, - 'Name of the config to get' + InputArgument::REQUIRED | InputArgument::IS_ARRAY, + 'Name of the config to get, specify multiple for array parameter' ) ->addOption( 'default-value', @@ -68,7 +68,8 @@ class GetConfig extends Base { * @return null|int null or 0 if everything went fine, or an error code */ protected function execute(InputInterface $input, OutputInterface $output) { - $configName = $input->getArgument('name'); + $configNames = $input->getArgument('name'); + $configName = array_shift($configNames); $defaultValue = $input->getOption('default-value'); if (!in_array($configName, $this->systemConfig->getKeys()) && !$input->hasParameterOption('--default-value')) { @@ -79,6 +80,18 @@ class GetConfig extends Base { $configValue = $defaultValue; } else { $configValue = $this->systemConfig->getValue($configName); + if (!empty($configNames)) { + foreach ($configNames as $configName) { + if (isset($configValue[$configName])) { + $configValue = $configValue[$configName]; + } else if (!$input->hasParameterOption('--default-value')) { + return 1; + } else { + $configValue = $defaultValue; + break; + } + } + } } $this->writeMixedInOutputFormat($input, $output, $configValue); diff --git a/core/command/config/system/setconfig.php b/core/command/config/system/setconfig.php index 0100ad2dfb..45ba01e928 100644 --- a/core/command/config/system/setconfig.php +++ b/core/command/config/system/setconfig.php @@ -48,8 +48,15 @@ class SetConfig extends Base { ->setDescription('Set a system config value') ->addArgument( 'name', - InputArgument::REQUIRED, - 'Name of the config to set' + InputArgument::REQUIRED | InputArgument::IS_ARRAY, + 'Name of the config parameter, specify multiple for array parameter' + ) + ->addOption( + 'type', + null, + InputOption::VALUE_REQUIRED, + 'Value type [string, integer, double, boolean]', + 'string' ) ->addOption( 'value', @@ -67,16 +74,124 @@ class SetConfig extends Base { } protected function execute(InputInterface $input, OutputInterface $output) { - $configName = $input->getArgument('name'); + $configNames = $input->getArgument('name'); + $configName = $configNames[0]; + $configValue = $this->castValue($input->getOption('value'), $input->getOption('type')); + $updateOnly = $input->getOption('update-only'); - if (!in_array($configName, $this->systemConfig->getKeys()) && $input->hasParameterOption('--update-only')) { - $output->writeln('Config value ' . $configName . ' not updated, as it has not been set before.'); - return 1; + if (sizeof($configNames) > 1) { + $existingValue = $this->systemConfig->getValue($configName); + + $newValue = $this->mergeArrayValue( + array_slice($configNames, 1), $existingValue, $configValue['value'], $updateOnly + ); + + $this->systemConfig->setValue($configName, $newValue); + } else { + if ($updateOnly && !in_array($configName, $this->systemConfig->getKeys(), true)) { + throw new \UnexpectedValueException('Config parameter does not exist'); + } + + $this->systemConfig->setValue($configName, $configValue['value']); } - $configValue = $input->getOption('value'); - $this->systemConfig->setValue($configName, $configValue); - $output->writeln('System config value ' . $configName . ' set to ' . $configValue . ''); + $output->writeln('System config value ' . implode(' => ', $configNames) . ' set to ' . $configValue['readable-value'] . ''); return 0; } + + /** + * @param string $value + * @param string $type + * @return mixed + * @throws \InvalidArgumentException + */ + protected function castValue($value, $type) { + switch ($type) { + case 'integer': + case 'int': + if (!is_numeric($value)) { + throw new \InvalidArgumentException('Non-numeric value specified'); + } + return [ + 'value' => (int) $value, + 'readable-value' => 'integer ' . (int) $value, + ]; + + case 'double': + case 'float': + if (!is_numeric($value)) { + throw new \InvalidArgumentException('Non-numeric value specified'); + } + return [ + 'value' => (double) $value, + 'readable-value' => 'double ' . (double) $value, + ]; + + case 'boolean': + case 'bool': + $value = strtolower($value); + switch ($value) { + case 'true': + return [ + 'value' => true, + 'readable-value' => 'boolean ' . $value, + ]; + + case 'false': + return [ + 'value' => false, + 'readable-value' => 'boolean ' . $value, + ]; + + default: + throw new \InvalidArgumentException('Unable to parse value as boolean'); + } + + case 'null': + return [ + 'value' => null, + 'readable-value' => 'null', + ]; + + case 'string': + $value = (string) $value; + return [ + 'value' => $value, + 'readable-value' => ($value === '') ? 'empty string' : 'string ' . $value, + ]; + + default: + throw new \InvalidArgumentException('Invalid type'); + } + } + + /** + * @param array $configNames + * @param mixed $existingValues + * @param mixed $value + * @param bool $updateOnly + * @return array merged value + * @throws \UnexpectedValueException + */ + protected function mergeArrayValue(array $configNames, $existingValues, $value, $updateOnly) { + $configName = array_shift($configNames); + if (!is_array($existingValues)) { + $existingValues = []; + } + if (!empty($configNames)) { + if (isset($existingValues[$configName])) { + $existingValue = $existingValues[$configName]; + } else { + $existingValue = []; + } + $existingValues[$configName] = $this->mergeArrayValue($configNames, $existingValue, $value, $updateOnly); + } else { + if (!isset($existingValues[$configName]) && $updateOnly) { + throw new \UnexpectedValueException('Config parameter does not exist'); + } + $existingValues[$configName] = $value; + } + return $existingValues; + } + } diff --git a/tests/core/command/config/system/deleteconfigtest.php b/tests/core/command/config/system/deleteconfigtest.php index aa1f93274c..11bfb6ae7a 100644 --- a/tests/core/command/config/system/deleteconfigtest.php +++ b/tests/core/command/config/system/deleteconfigtest.php @@ -50,32 +50,31 @@ class DeleteConfigTest extends TestCase { $this->command = new DeleteConfig($systemConfig); } - public function deleteData() { return [ [ - 'name', + 'name1', true, true, 0, 'info', ], [ - 'name', + 'name2', true, false, 0, 'info', ], [ - 'name', + 'name3', false, false, 0, 'info', ], [ - 'name', + 'name4', false, true, 1, @@ -105,7 +104,7 @@ class DeleteConfigTest extends TestCase { $this->consoleInput->expects($this->once()) ->method('getArgument') ->with('name') - ->willReturn($configName); + ->willReturn([$configName]); $this->consoleInput->expects($this->any()) ->method('hasParameterOption') ->with('--error-if-not-exists') @@ -117,4 +116,103 @@ class DeleteConfigTest extends TestCase { $this->assertSame($expectedReturn, $this->invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput])); } + + public function deleteArrayData() { + return [ + [ + ['name', 'sub'], + true, + false, + true, + true, + 0, + 'info', + ], + [ + ['name', 'sub', '2sub'], + true, + false, + ['sub' => ['2sub' => 1], 'sub2' => false], + ['sub' => [], 'sub2' => false], + 0, + 'info', + ], + [ + ['name', 'sub3'], + true, + false, + ['sub' => ['2sub' => 1], 'sub2' => false], + ['sub' => ['2sub' => 1], 'sub2' => false], + 0, + 'info', + ], + [ + ['name', 'sub'], + false, + true, + true, + true, + 1, + 'error', + ], + [ + ['name', 'sub'], + true, + true, + true, + true, + 1, + 'error', + ], + [ + ['name', 'sub3'], + true, + true, + ['sub' => ['2sub' => 1], 'sub2' => false], + ['sub' => ['2sub' => 1], 'sub2' => false], + 1, + 'error', + ], + ]; + } + + /** + * @dataProvider deleteArrayData + * + * @param string[] $configNames + * @param bool $configKeyExists + * @param bool $checkIfKeyExists + * @param mixed $configValue + * @param mixed $updateValue + * @param int $expectedReturn + * @param string $expectedMessage + */ + public function testArrayDelete(array $configNames, $configKeyExists, $checkIfKeyExists, $configValue, $updateValue, $expectedReturn, $expectedMessage) { + $this->systemConfig->expects(($checkIfKeyExists) ? $this->once() : $this->never()) + ->method('getKeys') + ->willReturn($configKeyExists ? [$configNames[0]] : []); + + $this->systemConfig->expects(($configKeyExists) ? $this->once() : $this->never()) + ->method('getValue') + ->willReturn($configValue); + + $this->systemConfig->expects(($expectedReturn === 0) ? $this->once() : $this->never()) + ->method('setValue') + ->with($configNames[0], $updateValue); + + $this->consoleInput->expects($this->once()) + ->method('getArgument') + ->with('name') + ->willReturn($configNames); + $this->consoleInput->expects($this->any()) + ->method('hasParameterOption') + ->with('--error-if-not-exists') + ->willReturn($checkIfKeyExists); + + $this->consoleOutput->expects($this->any()) + ->method('writeln') + ->with($this->stringContains($expectedMessage)); + + $this->assertSame($expectedReturn, $this->invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput])); + } } diff --git a/tests/core/command/config/system/getconfigtest.php b/tests/core/command/config/system/getconfigtest.php index 54dcdd434a..ebbea634cd 100644 --- a/tests/core/command/config/system/getconfigtest.php +++ b/tests/core/command/config/system/getconfigtest.php @@ -90,13 +90,19 @@ class GetConfigTest extends TestCase { ['name', ['a' => 1, 'b' => 2], true, null, false, 'json', 0, json_encode(['a' => 1, 'b' => 2])], ['name', ['a' => 1, 'b' => 2], true, null, false, 'plain', 0, "a: 1\nb: 2"], + // Nested depth + [['name', 'a'], ['a' => 1, 'b' => 2], true, null, false, 'json', 0, json_encode(1)], + [['name', 'a'], ['a' => 1, 'b' => 2], true, null, false, 'plain', 0, '1'], + [['name', 'c'], ['a' => 1, 'b' => 2], true, true, true, 'json', 0, json_encode(true)], + [['name', 'c'], ['a' => 1, 'b' => 2], true, true, false, 'json', 1, null], + ]; } /** * @dataProvider getData * - * @param string $configName + * @param string[] $configNames * @param mixed $value * @param bool $configExists * @param mixed $defaultValue @@ -105,7 +111,13 @@ class GetConfigTest extends TestCase { * @param int $expectedReturn * @param string $expectedMessage */ - public function testGet($configName, $value, $configExists, $defaultValue, $hasDefault, $outputFormat, $expectedReturn, $expectedMessage) { + public function testGet($configNames, $value, $configExists, $defaultValue, $hasDefault, $outputFormat, $expectedReturn, $expectedMessage) { + if (is_array($configNames)) { + $configName = $configNames[0]; + } else { + $configName = $configNames; + $configNames = [$configName]; + } $this->systemConfig->expects($this->atLeastOnce()) ->method('getKeys') ->willReturn($configExists ? [$configName] : []); @@ -122,7 +134,7 @@ class GetConfigTest extends TestCase { $this->consoleInput->expects($this->once()) ->method('getArgument') ->with('name') - ->willReturn($configName); + ->willReturn($configNames); $this->consoleInput->expects($this->any()) ->method('getOption') ->willReturnMap([ diff --git a/tests/core/command/config/system/setconfigtest.php b/tests/core/command/config/system/setconfigtest.php index bbabee9b18..c0b664d752 100644 --- a/tests/core/command/config/system/setconfigtest.php +++ b/tests/core/command/config/system/setconfigtest.php @@ -53,63 +53,123 @@ class SetConfigTest extends TestCase { public function setData() { return [ - [ - 'name', - 'newvalue', - true, - true, - true, - 'info', - ], - [ - 'name', - 'newvalue', - false, - true, - false, - 'comment', - ], + [['name'], 'newvalue', null, 'newvalue'], + [['a', 'b', 'c'], 'foobar', null, ['b' => ['c' => 'foobar']]], + [['a', 'b', 'c'], 'foobar', ['b' => ['d' => 'barfoo']], ['b' => ['d' => 'barfoo', 'c' => 'foobar']]], ]; } /** * @dataProvider setData * - * @param string $configName - * @param mixed $newValue - * @param bool $configExists - * @param bool $updateOnly - * @param bool $updated - * @param string $expectedMessage + * @param array $configNames + * @param string $newValue + * @param mixed $existingData + * @param mixed $expectedValue */ - public function testSet($configName, $newValue, $configExists, $updateOnly, $updated, $expectedMessage) { + public function testSet($configNames, $newValue, $existingData, $expectedValue) { $this->systemConfig->expects($this->once()) - ->method('getKeys') - ->willReturn($configExists ? [$configName] : []); - - if ($updated) { - $this->systemConfig->expects($this->once()) - ->method('setValue') - ->with($configName, $newValue); - } + ->method('setValue') + ->with($configNames[0], $expectedValue); + $this->systemConfig->method('getValue') + ->with($configNames[0]) + ->willReturn($existingData); $this->consoleInput->expects($this->once()) ->method('getArgument') ->with('name') - ->willReturn($configName); - $this->consoleInput->expects($this->any()) - ->method('getOption') - ->with('value') - ->willReturn($newValue); - $this->consoleInput->expects($this->any()) - ->method('hasParameterOption') - ->with('--update-only') - ->willReturn($updateOnly); - - $this->consoleOutput->expects($this->any()) - ->method('writeln') - ->with($this->stringContains($expectedMessage)); + ->willReturn($configNames); + $this->consoleInput->method('getOption') + ->will($this->returnValueMap([ + ['value', $newValue], + ['type', 'string'], + ])); $this->invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); } + + public function setUpdateOnlyProvider() { + return [ + [['name'], null], + [['a', 'b', 'c'], null], + [['a', 'b', 'c'], ['b' => 'foobar']], + [['a', 'b', 'c'], ['b' => ['d' => 'foobar']]], + ]; + } + + /** + * @dataProvider setUpdateOnlyProvider + * @expectedException \UnexpectedValueException + */ + public function testSetUpdateOnly($configNames, $existingData) { + $this->systemConfig->expects($this->never()) + ->method('setValue'); + $this->systemConfig->method('getValue') + ->with($configNames[0]) + ->willReturn($existingData); + $this->systemConfig->method('getKeys') + ->willReturn($existingData ? $configNames[0] : []); + + $this->consoleInput->expects($this->once()) + ->method('getArgument') + ->with('name') + ->willReturn($configNames); + $this->consoleInput->method('getOption') + ->will($this->returnValueMap([ + ['value', 'foobar'], + ['type', 'string'], + ['update-only', true], + ])); + + $this->invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); + } + + public function castValueProvider() { + return [ + [null, 'string', ['value' => '', 'readable-value' => 'empty string']], + + ['abc', 'string', ['value' => 'abc', 'readable-value' => 'string abc']], + + ['123', 'integer', ['value' => 123, 'readable-value' => 'integer 123']], + ['456', 'int', ['value' => 456, 'readable-value' => 'integer 456']], + + ['2.25', 'double', ['value' => 2.25, 'readable-value' => 'double 2.25']], + ['0.5', 'float', ['value' => 0.5, 'readable-value' => 'double 0.5']], + + ['', 'null', ['value' => null, 'readable-value' => 'null']], + + ['true', 'boolean', ['value' => true, 'readable-value' => 'boolean true']], + ['false', 'bool', ['value' => false, 'readable-value' => 'boolean false']], + ]; + } + + /** + * @dataProvider castValueProvider + */ + public function testCastValue($value, $type, $expectedValue) { + $this->assertSame($expectedValue, + $this->invokePrivate($this->command, 'castValue', [$value, $type]) + ); + } + + public function castValueInvalidProvider() { + return [ + ['123', 'foobar'], + + [null, 'integer'], + ['abc', 'integer'], + ['76ggg', 'double'], + ['true', 'float'], + ['foobar', 'boolean'], + ]; + } + + /** + * @dataProvider castValueInvalidProvider + * @expectedException \InvalidArgumentException + */ + public function testCastValueInvalid($value, $type) { + $this->invokePrivate($this->command, 'castValue', [$value, $type]); + } + }