Skip to content

Commit 9daae60

Browse files
committed
Add prototype definition support for nested options
1 parent 0a66727 commit 9daae60

File tree

3 files changed

+138
-1
lines changed

3 files changed

+138
-1
lines changed

src/Symfony/Component/OptionsResolver/CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
5.3.0
5+
-----
6+
7+
* added prototype definition for nested options
8+
49
5.1.0
510
-----
611

src/Symfony/Component/OptionsResolver/OptionsResolver.php

+53-1
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,16 @@ class OptionsResolver implements Options
130130

131131
private $parentsOptions = [];
132132

133+
/**
134+
* Whether the whole options definition is marked as array prototype.
135+
*/
136+
private $prototype = false;
137+
138+
/**
139+
* The prototype array's index that is being reading.
140+
*/
141+
private $prototypeIndex;
142+
133143
/**
134144
* Sets the default value of a given option.
135145
*
@@ -789,6 +799,29 @@ public function getInfo(string $option): ?string
789799
return $this->info[$option] ?? null;
790800
}
791801

802+
/**
803+
* Marks the whole options definition as array prototype.
804+
*
805+
* @return $this
806+
*
807+
* @throws AccessException If called from a lazy option or normalizer
808+
*/
809+
public function setPrototype(bool $prototype): self
810+
{
811+
if ($this->locked) {
812+
throw new AccessException('The prototype property cannot be set from a lazy option or normalizer.');
813+
}
814+
815+
$this->prototype = $prototype;
816+
817+
return $this;
818+
}
819+
820+
public function isPrototype(): bool
821+
{
822+
return $this->prototype;
823+
}
824+
792825
/**
793826
* Removes the option with the given name.
794827
*
@@ -975,8 +1008,23 @@ public function offsetGet($option, bool $triggerDeprecation = true)
9751008
foreach ($this->nested[$option] as $closure) {
9761009
$closure($resolver, $this);
9771010
}
978-
$value = $resolver->resolve($value);
1011+
1012+
if ($resolver->prototype) {
1013+
$values = [];
1014+
foreach ($value as $index => $prototypeValue) {
1015+
if (!\is_array($prototypeValue)) {
1016+
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)));
1017+
}
1018+
1019+
$resolver->prototypeIndex = $index;
1020+
$values[$index] = $resolver->resolve($prototypeValue);
1021+
}
1022+
$value = $values;
1023+
} else {
1024+
$value = $resolver->resolve($value);
1025+
}
9791026
} finally {
1027+
$resolver->prototypeIndex = null;
9801028
unset($this->calling[$option]);
9811029
}
9821030
}
@@ -1286,6 +1334,10 @@ private function formatOptions(array $options): string
12861334
$prefix .= sprintf('[%s]', implode('][', $this->parentsOptions));
12871335
}
12881336

1337+
if ($this->prototype && null !== $this->prototypeIndex) {
1338+
$prefix .= sprintf('[%s]', $this->prototypeIndex);
1339+
}
1340+
12891341
$options = array_map(static function (string $option) use ($prefix): string {
12901342
return sprintf('%s[%s]', $prefix, $option);
12911343
}, $options);

src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php

+80
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\OptionsResolver\Debug\OptionsResolverIntrospector;
1818
use Symfony\Component\OptionsResolver\Exception\AccessException;
1919
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
20+
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
2021
use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException;
2122
use Symfony\Component\OptionsResolver\Options;
2223
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -2500,4 +2501,83 @@ public function testSetDeprecatedWithoutPackageAndVersion()
25002501
->setDeprecated('foo')
25012502
;
25022503
}
2504+
2505+
public function testInvalidValueForPrototypeDefinition()
2506+
{
2507+
$this->expectException(InvalidOptionsException::class);
2508+
$this->expectExceptionMessage('The value of the option "connections" is expected to be of type array of array, but is of type array of "string".');
2509+
2510+
$this->resolver
2511+
->setDefault('connections', static function (OptionsResolver $resolver) {
2512+
$resolver
2513+
->setPrototype(true)
2514+
->setDefined(['table', 'user', 'password'])
2515+
;
2516+
})
2517+
;
2518+
2519+
$this->resolver->resolve(['connections' => ['foo']]);
2520+
}
2521+
2522+
public function testMissingOptionForPrototypeDefinition()
2523+
{
2524+
$this->expectException(MissingOptionsException::class);
2525+
$this->expectExceptionMessage('The required option "connections[1][table]" is missing.');
2526+
2527+
$this->resolver
2528+
->setDefault('connections', static function (OptionsResolver $resolver) {
2529+
$resolver
2530+
->setPrototype(true)
2531+
->setRequired('table')
2532+
;
2533+
})
2534+
;
2535+
2536+
$this->resolver->resolve(['connections' => [
2537+
['table' => 'default'],
2538+
[], // <- missing required option "table"
2539+
]]);
2540+
}
2541+
2542+
public function testPrototypeDefinition()
2543+
{
2544+
$this->resolver
2545+
->setDefault('connections', static function (OptionsResolver $resolver) {
2546+
$resolver
2547+
->setPrototype(true)
2548+
->setRequired('table')
2549+
->setDefaults(['user' => 'root', 'password' => null])
2550+
;
2551+
})
2552+
;
2553+
2554+
$actualOptions = $this->resolver->resolve([
2555+
'connections' => [
2556+
'default' => [
2557+
'table' => 'default',
2558+
],
2559+
'custom' => [
2560+
'user' => 'foo',
2561+
'password' => 'pa$$',
2562+
'table' => 'symfony',
2563+
],
2564+
],
2565+
]);
2566+
$expectedOptions = [
2567+
'connections' => [
2568+
'default' => [
2569+
'user' => 'root',
2570+
'password' => null,
2571+
'table' => 'default',
2572+
],
2573+
'custom' => [
2574+
'user' => 'foo',
2575+
'password' => 'pa$$',
2576+
'table' => 'symfony',
2577+
],
2578+
],
2579+
];
2580+
2581+
$this->assertSame($expectedOptions, $actualOptions);
2582+
}
25032583
}

0 commit comments

Comments
 (0)