Skip to content

Commit 5029d04

Browse files
committed
Add prototype definition support for nested options
1 parent 5d0f633 commit 5029d04

File tree

3 files changed

+150
-1
lines changed

3 files changed

+150
-1
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

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

OptionsResolver.php

Lines changed: 58 additions & 1 deletion
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;
137+
138+
/**
139+
* The prototype array's index that is being read.
140+
*/
141+
private $prototypeIndex;
142+
133143
/**
134144
* Sets the default value of a given option.
135145
*
@@ -789,6 +799,33 @@ 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, a normalizer or a root definition
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+
if (null === $this->prototype && $prototype) {
816+
throw new AccessException('The prototype property cannot be set from a root definition.');
817+
}
818+
819+
$this->prototype = $prototype;
820+
821+
return $this;
822+
}
823+
824+
public function isPrototype(): bool
825+
{
826+
return $this->prototype ?? false;
827+
}
828+
792829
/**
793830
* Removes the option with the given name.
794831
*
@@ -970,13 +1007,29 @@ public function offsetGet($option, bool $triggerDeprecation = true)
9701007
$this->calling[$option] = true;
9711008
try {
9721009
$resolver = new self();
1010+
$resolver->prototype = false;
9731011
$resolver->parentsOptions = $this->parentsOptions;
9741012
$resolver->parentsOptions[] = $option;
9751013
foreach ($this->nested[$option] as $closure) {
9761014
$closure($resolver, $this);
9771015
}
978-
$value = $resolver->resolve($value);
1016+
1017+
if ($resolver->prototype) {
1018+
$values = [];
1019+
foreach ($value as $index => $prototypeValue) {
1020+
if (!\is_array($prototypeValue)) {
1021+
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)));
1022+
}
1023+
1024+
$resolver->prototypeIndex = $index;
1025+
$values[$index] = $resolver->resolve($prototypeValue);
1026+
}
1027+
$value = $values;
1028+
} else {
1029+
$value = $resolver->resolve($value);
1030+
}
9791031
} finally {
1032+
$resolver->prototypeIndex = null;
9801033
unset($this->calling[$option]);
9811034
}
9821035
}
@@ -1286,6 +1339,10 @@ private function formatOptions(array $options): string
12861339
$prefix .= sprintf('[%s]', implode('][', $this->parentsOptions));
12871340
}
12881341

1342+
if ($this->prototype && null !== $this->prototypeIndex) {
1343+
$prefix .= sprintf('[%s]', $this->prototypeIndex);
1344+
}
1345+
12891346
$options = array_map(static function (string $option) use ($prefix): string {
12901347
return sprintf('%s[%s]', $prefix, $option);
12911348
}, $options);

Tests/OptionsResolverTest.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2504,4 +2504,91 @@ public function testSetDeprecatedWithoutPackageAndVersion()
25042504
->setDeprecated('foo')
25052505
;
25062506
}
2507+
2508+
public function testInvalidValueForPrototypeDefinition()
2509+
{
2510+
$this->expectException(InvalidOptionsException::class);
2511+
$this->expectExceptionMessage('The value of the option "connections" is expected to be of type array of array, but is of type array of "string".');
2512+
2513+
$this->resolver
2514+
->setDefault('connections', static function (OptionsResolver $resolver) {
2515+
$resolver
2516+
->setPrototype(true)
2517+
->setDefined(['table', 'user', 'password'])
2518+
;
2519+
})
2520+
;
2521+
2522+
$this->resolver->resolve(['connections' => ['foo']]);
2523+
}
2524+
2525+
public function testMissingOptionForPrototypeDefinition()
2526+
{
2527+
$this->expectException(MissingOptionsException::class);
2528+
$this->expectExceptionMessage('The required option "connections[1][table]" is missing.');
2529+
2530+
$this->resolver
2531+
->setDefault('connections', static function (OptionsResolver $resolver) {
2532+
$resolver
2533+
->setPrototype(true)
2534+
->setRequired('table')
2535+
;
2536+
})
2537+
;
2538+
2539+
$this->resolver->resolve(['connections' => [
2540+
['table' => 'default'],
2541+
[], // <- missing required option "table"
2542+
]]);
2543+
}
2544+
2545+
public function testAccessExceptionOnPrototypeDefinition()
2546+
{
2547+
$this->expectException(AccessException::class);
2548+
$this->expectExceptionMessage('The prototype property cannot be set from a root definition.');
2549+
2550+
$this->resolver->setPrototype(true);
2551+
}
2552+
2553+
public function testPrototypeDefinition()
2554+
{
2555+
$this->resolver
2556+
->setDefault('connections', static function (OptionsResolver $resolver) {
2557+
$resolver
2558+
->setPrototype(true)
2559+
->setRequired('table')
2560+
->setDefaults(['user' => 'root', 'password' => null])
2561+
;
2562+
})
2563+
;
2564+
2565+
$actualOptions = $this->resolver->resolve([
2566+
'connections' => [
2567+
'default' => [
2568+
'table' => 'default',
2569+
],
2570+
'custom' => [
2571+
'user' => 'foo',
2572+
'password' => 'pa$$',
2573+
'table' => 'symfony',
2574+
],
2575+
],
2576+
]);
2577+
$expectedOptions = [
2578+
'connections' => [
2579+
'default' => [
2580+
'user' => 'root',
2581+
'password' => null,
2582+
'table' => 'default',
2583+
],
2584+
'custom' => [
2585+
'user' => 'foo',
2586+
'password' => 'pa$$',
2587+
'table' => 'symfony',
2588+
],
2589+
],
2590+
];
2591+
2592+
$this->assertSame($expectedOptions, $actualOptions);
2593+
}
25072594
}

0 commit comments

Comments
 (0)