Skip to content

Commit

Permalink
Add prototype definition support for nested options
Browse files Browse the repository at this point in the history
  • Loading branch information
yceruto committed Apr 18, 2021
1 parent 5d0f633 commit 5029d04
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 1 deletion.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

5.3
---

* Add prototype definition for nested options

5.1.0
-----

Expand Down
59 changes: 58 additions & 1 deletion OptionsResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -789,6 +799,33 @@ public function getInfo(string $option): ?string
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.
*
Expand Down Expand Up @@ -970,13 +1007,29 @@ public function offsetGet($option, bool $triggerDeprecation = true)
$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);
}
$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 {
$resolver->prototypeIndex = null;
unset($this->calling[$option]);
}
}
Expand Down Expand Up @@ -1286,6 +1339,10 @@ private function formatOptions(array $options): string
$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);
Expand Down
87 changes: 87 additions & 0 deletions Tests/OptionsResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2504,4 +2504,91 @@ public function testSetDeprecatedWithoutPackageAndVersion()
->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);
}
}

0 comments on commit 5029d04

Please sign in to comment.