Add prototype definition support for nested options
This commit is contained in:
parent
926f87ff51
commit
29d41b1970
@ -1,6 +1,11 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
5.3
|
||||||
|
---
|
||||||
|
|
||||||
|
* Add prototype definition for nested options
|
||||||
|
|
||||||
5.1.0
|
5.1.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
@ -130,6 +130,16 @@ class OptionsResolver implements Options
|
|||||||
|
|
||||||
private $parentsOptions = [];
|
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.
|
* Sets the default value of a given option.
|
||||||
*
|
*
|
||||||
@ -789,6 +799,33 @@ class OptionsResolver implements Options
|
|||||||
return $this->info[$option] ?? null;
|
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.
|
* Removes the option with the given name.
|
||||||
*
|
*
|
||||||
@ -970,13 +1007,29 @@ class OptionsResolver implements Options
|
|||||||
$this->calling[$option] = true;
|
$this->calling[$option] = true;
|
||||||
try {
|
try {
|
||||||
$resolver = new self();
|
$resolver = new self();
|
||||||
|
$resolver->prototype = false;
|
||||||
$resolver->parentsOptions = $this->parentsOptions;
|
$resolver->parentsOptions = $this->parentsOptions;
|
||||||
$resolver->parentsOptions[] = $option;
|
$resolver->parentsOptions[] = $option;
|
||||||
foreach ($this->nested[$option] as $closure) {
|
foreach ($this->nested[$option] as $closure) {
|
||||||
$closure($resolver, $this);
|
$closure($resolver, $this);
|
||||||
}
|
}
|
||||||
$value = $resolver->resolve($value);
|
|
||||||
|
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 {
|
} finally {
|
||||||
|
$resolver->prototypeIndex = null;
|
||||||
unset($this->calling[$option]);
|
unset($this->calling[$option]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1286,6 +1339,10 @@ class OptionsResolver implements Options
|
|||||||
$prefix .= sprintf('[%s]', implode('][', $this->parentsOptions));
|
$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 {
|
$options = array_map(static function (string $option) use ($prefix): string {
|
||||||
return sprintf('%s[%s]', $prefix, $option);
|
return sprintf('%s[%s]', $prefix, $option);
|
||||||
}, $options);
|
}, $options);
|
||||||
|
@ -2504,4 +2504,91 @@ class OptionsResolverTest extends TestCase
|
|||||||
->setDeprecated('foo')
|
->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