1212namespace Symfony \Component \OptionsResolver ;
1313
1414use Symfony \Component \OptionsResolver \Exception \AccessException ;
15+ use Symfony \Component \OptionsResolver \Exception \ExceptionInterface ;
1516use Symfony \Component \OptionsResolver \Exception \InvalidOptionsException ;
1617use Symfony \Component \OptionsResolver \Exception \MissingOptionsException ;
1718use Symfony \Component \OptionsResolver \Exception \NoSuchOptionException ;
@@ -40,6 +41,13 @@ class OptionsResolver implements Options
4041 */
4142private $ 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)
167188if (isset ($ params [0 ]) &&null !== ($ class =$ params [0 ]->getClass ()) && Options::class ===$ class ->name ) {
168189// Initialize the option if no previous value exists
169190if (!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)
236269return 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 ($ defaultsas $ 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)
443552throw 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+
446560if (!isset ($ this ->defined [$ option ])) {
447561throw new UndefinedOptionsException (sprintf (
448562'The option "%s" does not exist. Defined options are: "%s". ' ,
@@ -488,6 +602,11 @@ public function addAllowedValues($option, $allowedValues)
488602throw 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+
491610if (!isset ($ this ->defined [$ option ])) {
492611throw new UndefinedOptionsException (sprintf (
493612'The option "%s" does not exist. Defined options are: "%s". ' ,
@@ -533,6 +652,11 @@ public function setAllowedTypes($option, $allowedTypes)
533652throw 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+
536660if (!isset ($ this ->defined [$ option ])) {
537661throw new UndefinedOptionsException (sprintf (
538662'The option "%s" does not exist. Defined options are: "%s". ' ,
@@ -572,6 +696,11 @@ public function addAllowedTypes($option, $allowedTypes)
572696throw 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+
575704if (!isset ($ this ->defined [$ option ])) {
576705throw new UndefinedOptionsException (sprintf (
577706'The option "%s" does not exist. Defined options are: "%s". ' ,
@@ -610,8 +739,9 @@ public function remove($optionNames)
610739 }
611740
612741foreach ((array )$ optionNamesas $ 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
617747return $ 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
693824foreach ($ optionsas $ 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
793938if (isset ($ this ->allowedTypes [$ option ])) {
794939$ valid =false ;
@@ -955,6 +1100,13 @@ public function count()
9551100return 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 *