Add prototype definition support for nested options

This commit is contained in:
Yonel Ceruto 2021-01-20 18:32:38 -05:00
parent 926f87ff51
commit 29d41b1970
3 changed files with 150 additions and 1 deletions

View File

@ -1,6 +1,11 @@
CHANGELOG
=========
5.3
---
* Add prototype definition for nested options
5.1.0
-----

View File

@ -130,6 +130,16 @@ class OptionsResolver implements Options
private $parentsOptions = [];
/**
* Whether the whole options definition is marked as array prototype.
*/
private $prototype;
/**
* The prototype array's index that is being read.
*/
private $prototypeIndex;
/**
* Sets the default value of a given option.
*
@ -789,6 +799,33 @@ class OptionsResolver implements Options
return $this->info[$option] ?? null;
}
/**
* Marks the whole options definition as array prototype.
*
* @return $this
*
* @throws AccessException If called from a lazy option, a normalizer or a root definition
*/
public function setPrototype(bool $prototype): self
{
if ($this->locked) {
throw new AccessException('The prototype property cannot be set from a lazy option or normalizer.');
}
if (null === $this->prototype && $prototype) {
throw new AccessException('The prototype property cannot be set from a root definition.');
}
$this->prototype = $prototype;
return $this;
}
public function isPrototype(): bool
{
return $this->prototype ?? false;
}
/**
* Removes the option with the given name.
*
@ -970,13 +1007,29 @@ class OptionsResolver implements Options
$this->calling[$option] = true;
try {
$resolver = new self();
$resolver->prototype = false;
$resolver->parentsOptions = $this->parentsOptions;
$resolver->parentsOptions[] = $option;
foreach ($this->nested[$option] as $closure) {
$closure($resolver, $this);
}
if ($resolver->prototype) {
$values = [];
foreach ($value as $index => $prototypeValue) {
if (!\is_array($prototypeValue)) {
throw new InvalidOptionsException(sprintf('The value of the option "%s" is expected to be of type array of array, but is of type array of "%s".', $this->formatOptions([$option]), get_debug_type($prototypeValue)));
}
$resolver->prototypeIndex = $index;
$values[$index] = $resolver->resolve($prototypeValue);
}
$value = $values;
} else {
$value = $resolver->resolve($value);
}
} finally {
$resolver->prototypeIndex = null;
unset($this->calling[$option]);
}
}
@ -1286,6 +1339,10 @@ class OptionsResolver implements Options
$prefix .= sprintf('[%s]', implode('][', $this->parentsOptions));
}
if ($this->prototype && null !== $this->prototypeIndex) {
$prefix .= sprintf('[%s]', $this->prototypeIndex);
}
$options = array_map(static function (string $option) use ($prefix): string {
return sprintf('%s[%s]', $prefix, $option);
}, $options);

View File

@ -2504,4 +2504,91 @@ class OptionsResolverTest extends TestCase
->setDeprecated('foo')
;
}
public function testInvalidValueForPrototypeDefinition()
{
$this->expectException(InvalidOptionsException::class);
$this->expectExceptionMessage('The value of the option "connections" is expected to be of type array of array, but is of type array of "string".');
$this->resolver
->setDefault('connections', static function (OptionsResolver $resolver) {
$resolver
->setPrototype(true)
->setDefined(['table', 'user', 'password'])
;
})
;
$this->resolver->resolve(['connections' => ['foo']]);
}
public function testMissingOptionForPrototypeDefinition()
{
$this->expectException(MissingOptionsException::class);
$this->expectExceptionMessage('The required option "connections[1][table]" is missing.');
$this->resolver
->setDefault('connections', static function (OptionsResolver $resolver) {
$resolver
->setPrototype(true)
->setRequired('table')
;
})
;
$this->resolver->resolve(['connections' => [
['table' => 'default'],
[], // <- missing required option "table"
]]);
}
public function testAccessExceptionOnPrototypeDefinition()
{
$this->expectException(AccessException::class);
$this->expectExceptionMessage('The prototype property cannot be set from a root definition.');
$this->resolver->setPrototype(true);
}
public function testPrototypeDefinition()
{
$this->resolver
->setDefault('connections', static function (OptionsResolver $resolver) {
$resolver
->setPrototype(true)
->setRequired('table')
->setDefaults(['user' => 'root', 'password' => null])
;
})
;
$actualOptions = $this->resolver->resolve([
'connections' => [
'default' => [
'table' => 'default',
],
'custom' => [
'user' => 'foo',
'password' => 'pa$$',
'table' => 'symfony',
],
],
]);
$expectedOptions = [
'connections' => [
'default' => [
'user' => 'root',
'password' => null,
'table' => 'default',
],
'custom' => [
'user' => 'foo',
'password' => 'pa$$',
'table' => 'symfony',
],
],
];
$this->assertSame($expectedOptions, $actualOptions);
}
}