Add prototype definition support for nested options
This commit is contained in:
parent
926f87ff51
commit
29d41b1970
@ -1,6 +1,11 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
5.3
|
||||
---
|
||||
|
||||
* Add prototype definition for nested options
|
||||
|
||||
5.1.0
|
||||
-----
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user