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 CHANGELOG
========= =========
5.3
---
* Add prototype definition for nested options
5.1.0 5.1.0
----- -----

View File

@ -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);
} }
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); $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);

View File

@ -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);
}
} }