Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit c2944ac

Browse filesBrowse files
committed
[OptionsResolver] resolve nested options
closes #4833. Allow to resolve nested options. * add `OptionsResolver::nested` * add `OptionsResolver::setNested()` * add `OptionsResolver::isNested()` * add `OptionsResolver::getNested()`
1 parent bb2727a commit c2944ac
Copy full SHA for c2944ac

File tree

2 files changed

+220
-5
lines changed
Filter options

2 files changed

+220
-5
lines changed

‎src/Symfony/Component/OptionsResolver/OptionsResolver.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/OptionsResolver/OptionsResolver.php
+157-5Lines changed: 157 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\OptionsResolver;
1313

1414
use Symfony\Component\OptionsResolver\Exception\AccessException;
15+
use Symfony\Component\OptionsResolver\Exception\ExceptionInterface;
1516
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
1617
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
1718
use Symfony\Component\OptionsResolver\Exception\NoSuchOptionException;
@@ -40,6 +41,13 @@ class OptionsResolver implements Options
4041
*/
4142
private $defaults = array();
4243

44+
/**
45+
* The nested options.
46+
*
47+
* @var OptionsResolver[]
48+
*/
49+
private $nested = array();
50+
4351
/**
4452
* The names of required options.
4553
*
@@ -142,10 +150,23 @@ class OptionsResolver implements Options
142150
* is spread across different locations of your code, such as base and
143151
* sub-classes.
144152
*
153+
* If you set default values of nested options, this method will return the
154+
* nested instance for the same convenience as above.
155+
*
156+
* // Master class
157+
* $options->setNested('connexion', array('port' => '80'));
158+
*
159+
* // Sub class inheriting $options
160+
* $nestedOptions = $options->setDefault('connexion', array(
161+
* 'port' => '443', // overrides default
162+
* ));
163+
*
164+
* $nestedOptions->setRequired('type');
165+
*
145166
* @param string $option The name of the option
146167
* @param mixed $value The default value of the option
147168
*
148-
* @return OptionsResolver This instance
169+
* @return OptionsResolver This instance or the nested instance
149170
*
150171
* @throws AccessException If called from a lazy option or normalizer
151172
*/
@@ -167,7 +188,7 @@ public function setDefault($option, $value)
167188
if (isset($params[0]) && null !== ($class = $params[0]->getClass()) && Options::class === $class->name) {
168189
// Initialize the option if no previous value exists
169190
if (!isset($this->defaults[$option])) {
170-
$this->defaults[$option] = null;
191+
$this->defaults[$option] = $this->isNested($option) ? array() : null;
171192
}
172193

173194
// Ignore previous lazy options if the closure has no second parameter
@@ -189,6 +210,18 @@ public function setDefault($option, $value)
189210
// This option is not lazy anymore
190211
unset($this->lazy[$option]);
191212

213+
if ($this->isNested($option)) {
214+
$defaults = isset($this->defaults[$option]) ? $this->defaults[$option] : array();
215+
$this->defaults[$option] = array_replace($defaults, $value);
216+
$this->defined[$option] = true;
217+
// Make sure the nested options are processed
218+
unset($this->resolved[$option]);
219+
220+
// Returning the nested options here is convenient when we need to
221+
// override them from a sub class
222+
return $this->nested[$option];
223+
}
224+
192225
// Yet undefined options can be marked as resolved, because we only need
193226
// to resolve options with lazy closures, normalizers or validation
194227
// rules, none of which can exist for undefined options
@@ -236,6 +269,82 @@ public function hasDefault($option)
236269
return array_key_exists($option, $this->defaults);
237270
}
238271

272+
/**
273+
* Defines an option as a new self.
274+
*
275+
* Returns a new OptionsResolver instance to configure nested options.
276+
*
277+
* $nestedOptions = $options->setNested('connexion', array(
278+
* 'host' => 'localhost',
279+
* 'port' => 80,
280+
* );
281+
*
282+
* $nestedOptions->setRequired('user');
283+
* $nestedOptions->setDefault('password', function (Options $nested) {
284+
* return isset($nested['user']) ? '' : null;
285+
* });
286+
* $nestedOptions->setDefined(array('secure'));
287+
* $nestedOptions->setNormalizer('secure', function (Options $nested, $secure) {
288+
* return 443 === $nested['port'] ?: $secure;
289+
* });
290+
* $nestedOptions->setAllowedTypes('port', 'int');
291+
*
292+
* @param string $option The option name
293+
* @param array $defaults The default nested options
294+
*
295+
* @return OptionsResolver The nested options resolver
296+
*
297+
* @throws AccessException If called from a lazy option or normalizer
298+
*/
299+
public function setNested($option, array $defaults = array())
300+
{
301+
if ($this->locked) {
302+
throw new AccessException('Options cannot be made nested from a lazy option or normalizer.');
303+
}
304+
305+
$nestedOptions = new self();
306+
307+
foreach ($defaults as $name => $default) {
308+
$nestedOptions->setDefault($name, $default);
309+
}
310+
311+
// Keep a raw copy of defaults until nested options are resolved allowing to
312+
// easily override them, even using lazy definition with {@link setDefault()}
313+
$this->defaults[$option] = $defaults;
314+
$this->defined[$option] = true;
315+
316+
// Make sure the nested options are processed
317+
unset($this->resolved[$option]);
318+
319+
return $this->nested[$option] = $nestedOptions;
320+
}
321+
322+
/**
323+
* Returns whether an option is nested.
324+
*
325+
* An option is nested if it was passed to {@link setNested()}.
326+
*
327+
* @param string $option The name of the option
328+
*
329+
* @return bool Whether the option is nested
330+
*/
331+
public function isNested($option)
332+
{
333+
return isset($this->nested[$option]);
334+
}
335+
336+
/**
337+
* Returns the names of all nested options.
338+
*
339+
* @return string[] The names of the nested options
340+
*
341+
* @see isNested()
342+
*/
343+
public function getNestedOptions()
344+
{
345+
return array_keys($this->nested);
346+
}
347+
239348
/**
240349
* Marks one or more options as required.
241350
*
@@ -443,6 +552,11 @@ public function setAllowedValues($option, $allowedValues)
443552
throw new AccessException('Allowed values cannot be set from a lazy option or normalizer.');
444553
}
445554

555+
// Not supported for nested options
556+
if ($this->isNested($option)) {
557+
return $this;
558+
}
559+
446560
if (!isset($this->defined[$option])) {
447561
throw new UndefinedOptionsException(sprintf(
448562
'The option "%s" does not exist. Defined options are: "%s".',
@@ -488,6 +602,11 @@ public function addAllowedValues($option, $allowedValues)
488602
throw new AccessException('Allowed values cannot be added from a lazy option or normalizer.');
489603
}
490604

605+
// Not supported for nested options
606+
if ($this->isNested($option)) {
607+
return $this;
608+
}
609+
491610
if (!isset($this->defined[$option])) {
492611
throw new UndefinedOptionsException(sprintf(
493612
'The option "%s" does not exist. Defined options are: "%s".',
@@ -533,6 +652,11 @@ public function setAllowedTypes($option, $allowedTypes)
533652
throw new AccessException('Allowed types cannot be set from a lazy option or normalizer.');
534653
}
535654

655+
// Not supported for nested options
656+
if ($this->isNested($option)) {
657+
return $this;
658+
}
659+
536660
if (!isset($this->defined[$option])) {
537661
throw new UndefinedOptionsException(sprintf(
538662
'The option "%s" does not exist. Defined options are: "%s".',
@@ -572,6 +696,11 @@ public function addAllowedTypes($option, $allowedTypes)
572696
throw new AccessException('Allowed types cannot be added from a lazy option or normalizer.');
573697
}
574698

699+
// Not supported for nested options
700+
if ($this->isNested($option)) {
701+
return $this;
702+
}
703+
575704
if (!isset($this->defined[$option])) {
576705
throw new UndefinedOptionsException(sprintf(
577706
'The option "%s" does not exist. Defined options are: "%s".',
@@ -610,8 +739,9 @@ public function remove($optionNames)
610739
}
611740

612741
foreach ((array) $optionNames as $option) {
613-
unset($this->defined[$option], $this->defaults[$option], $this->required[$option], $this->resolved[$option]);
614-
unset($this->lazy[$option], $this->normalizers[$option], $this->allowedTypes[$option], $this->allowedValues[$option]);
742+
unset($this->defined[$option], $this->defaults[$option], $this->nested[$option]);
743+
unset($this->required[$option], $this->resolved[$option], $this->lazy[$option]);
744+
unset($this->normalizers[$option], $this->allowedTypes[$option], $this->allowedValues[$option]);
615745
}
616746

617747
return $this;
@@ -632,6 +762,7 @@ public function clear()
632762

633763
$this->defined = array();
634764
$this->defaults = array();
765+
$this->nested = array();
635766
$this->required = array();
636767
$this->resolved = array();
637768
$this->lazy = array();
@@ -691,7 +822,13 @@ public function resolve(array $options = array())
691822

692823
// Override options set by the user
693824
foreach ($options as $option => $value) {
694-
$clone->defaults[$option] = $value;
825+
if ($clone->isNested($option)) {
826+
$defaults = isset($clone->defaults[$option]) ? $clone->defaults[$option] : array();
827+
$clone->defaults[$option] = array_replace($defaults, $value);
828+
} else {
829+
$clone->defaults[$option] = $value;
830+
}
831+
695832
unset($clone->resolved[$option], $clone->lazy[$option]);
696833
}
697834

@@ -789,6 +926,14 @@ public function offsetGet($option)
789926
// END
790927
}
791928

929+
if ($this->isNested($option)) {
930+
try {
931+
$value = $this->nested[$option]->resolve($value);
932+
} catch (ExceptionInterface $e) {
933+
throw new InvalidOptionsException(sprintf('The nested options in the option "%s" could not be resolved.', $option), 0, $e);
934+
}
935+
}
936+
792937
// Validate the type of the resolved option
793938
if (isset($this->allowedTypes[$option])) {
794939
$valid = false;
@@ -955,6 +1100,13 @@ public function count()
9551100
return count($this->defaults);
9561101
}
9571102

1103+
public function __clone()
1104+
{
1105+
foreach ($this->nested as $name => $options) {
1106+
$this->nested[$name] = clone $options;
1107+
}
1108+
}
1109+
9581110
/**
9591111
* Returns a string representation of the type of the value.
9601112
*

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php
+63Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,53 @@ public function testInvokeEachLazyOptionOnlyOnce()
230230
$this->assertSame(2, $calls);
231231
}
232232

233+
////////////////////////////////////////////////////////////////////////////
234+
// setNested()/isNested()/getNestedOptions()
235+
////////////////////////////////////////////////////////////////////////////
236+
237+
public function testSetNestedReturnsNewResolver()
238+
{
239+
$this->assertNotSame($this->resolver, $this->resolver->setNested('foo'));
240+
$this->assertInstanceOf('Symfony\Component\OptionsResolver\OptionsResolver', $this->resolver->setNested('bar'));
241+
}
242+
243+
public function testSetNested()
244+
{
245+
$this->resolver->setNested('one', array('un' => '1'));
246+
$this->resolver->setNested('two', array('deux' => '2', 'vingt' => 20));
247+
248+
$this->assertEquals(array(
249+
'one' => array('un' => '1'),
250+
'two' => array('deux' => '2', 'vingt' => 20),
251+
), $this->resolver->resolve());
252+
}
253+
254+
/**
255+
* @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException
256+
*/
257+
public function testFailIfSetNestedFromLazyOption()
258+
{
259+
$this->resolver->setDefault('lazy', function (Options $options) {
260+
$options->setNested('nested', array('number' => 42));
261+
});
262+
263+
$this->resolver->resolve();
264+
}
265+
266+
public function testIsNested()
267+
{
268+
$this->assertFalse($this->resolver->isNested('foo'));
269+
$this->resolver->setNested('foo', array('number' => 42));
270+
$this->assertTrue($this->resolver->isNested('foo'));
271+
}
272+
273+
public function testIsNestedWithNoValue()
274+
{
275+
$this->assertFalse($this->resolver->isNested('foo'));
276+
$this->resolver->setNested('foo');
277+
$this->assertTrue($this->resolver->isNested('foo'));
278+
}
279+
233280
////////////////////////////////////////////////////////////////////////////
234281
// setRequired()/isRequired()/getRequiredOptions()
235282
////////////////////////////////////////////////////////////////////////////
@@ -1547,4 +1594,20 @@ public function testCountFailsOutsideResolve()
15471594

15481595
count($this->resolver);
15491596
}
1597+
1598+
////////////////////////////////////////////////////////////////////////////
1599+
// Nested options
1600+
////////////////////////////////////////////////////////////////////////////
1601+
1602+
/**
1603+
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
1604+
* @expectedExceptionMessage The nested options in the option "z" could not be resolved.
1605+
*/
1606+
public function testResolveFailsIfNonExistingNestedOption()
1607+
{
1608+
$this->resolver->setNested('z', array('one' => '1'));
1609+
$this->resolver->setNested('a', array('two' => '2'));
1610+
1611+
$this->resolver->resolve(array('z' => array('foo' => 'bar')));
1612+
}
15501613
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.