diff --git a/src/Symfony/Component/OptionsResolver/NestableOptionsResolverInterface.php b/src/Symfony/Component/OptionsResolver/NestableOptionsResolverInterface.php new file mode 100644 index 0000000000000..4eea903efc6c7 --- /dev/null +++ b/src/Symfony/Component/OptionsResolver/NestableOptionsResolverInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver; + +/** + * @author Wouter J + */ +interface NestableOptionsResolverInterface extends OptionsResolverInterface +{ + /** + * Sets nested options resolvers. + * + * @param array $options A list of option names as keys and instances of + * OptionsResolverInterface as values. + */ + public function setNestedOptionsResolver(array $options); +} diff --git a/src/Symfony/Component/OptionsResolver/Options.php b/src/Symfony/Component/OptionsResolver/Options.php index 43c81f0c33403..3b2ab2deb52fd 100644 --- a/src/Symfony/Component/OptionsResolver/Options.php +++ b/src/Symfony/Component/OptionsResolver/Options.php @@ -97,10 +97,10 @@ public function set($option, $value) /** * Sets the normalizer for a given option. * - * Normalizers should be closures with the following signature: + * Normalizers should be valid callables with the following signature: * * - * function (Options $options, $value) + * function (Options $options, $value, $option) * * * This closure will be evaluated once the option is read using @@ -108,14 +108,18 @@ public function set($option, $value) * other options through the passed {@link Options} instance. * * @param string $option The name of the option. - * @param \Closure $normalizer The normalizer. + * @param callable $normalizer The normalizer. * * @throws OptionDefinitionException If options have already been read. * Once options are read, the container * becomes immutable. */ - public function setNormalizer($option, \Closure $normalizer) + public function setNormalizer($option, $normalizer) { + if (!is_callable($normalizer)) { + throw new \InvalidArgumentException('Normalizers should be a valid callable.'); + } + if ($this->reading) { throw new OptionDefinitionException('Normalizers cannot be added anymore once options have been read.'); } @@ -500,11 +504,11 @@ private function normalize($option) throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', $conflicts))); } - /** @var \Closure $normalizer */ + /** @var callable $normalizer */ $normalizer = $this->normalizers[$option]; $this->lock[$option] = true; - $this->options[$option] = $normalizer($this, array_key_exists($option, $this->options) ? $this->options[$option] : null); + $this->options[$option] = call_user_func($normalizer, $this, array_key_exists($option, $this->options) ? $this->options[$option] : null, $option); unset($this->lock[$option]); // The option is now normalized diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index 237ab8135f503..7c143283063c1 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -21,7 +21,7 @@ * @author Bernhard Schussek * @author Tobias Schultze */ -class OptionsResolver implements OptionsResolverInterface +class OptionsResolver implements NestableOptionsResolverInterface { /** * The default option values. @@ -53,6 +53,12 @@ class OptionsResolver implements OptionsResolverInterface */ private $allowedTypes = array(); + /** + * A list of the nested option resolvers. + * @var array + */ + private $optionResolvers = array(); + /** * Creates a new instance. */ @@ -181,6 +187,37 @@ public function addAllowedTypes(array $allowedTypes) return $this; } + /** + * {@inheritdoc} + */ + public function setNestedOptionsResolver(array $options) + { + $this->validateOptionsExistence($options); + + foreach ($options as $option => $resolver) { + if (!$resolver instanceof OptionsResolverInterface) { + throw new \InvalidArgumentException('Nested option resolvers have to implement OptionsResolverInterface.'); + } + $this->optionResolvers[$option] = $resolver; + $this->setNormalizers(array($option => array($this, 'nestedNormalizer'))); + } + + return $this; + } + + /** + * This normalizer will be called when an option has a nested options + * resolver. + * + * @param Options $options + * @param null|array $value + * @param string $option + */ + public function nestedNormalizer(Options $options, $value, $option) + { + return $this->optionResolvers[$option]->resolve($value ?: array()); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php index fc3b3fc5d38e3..cad144273b3f5 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php @@ -717,4 +717,54 @@ public function testClone() 'three' => '3', ), $clone->resolve()); } + + public function testNestedResolversForRequiredOption() + { + $this->resolver->setRequired(array('db')); + $this->resolver->setNestedOptionsResolver(array( + 'db' => $this->getNestedResolver(), + )); + + $this->assertEquals(array( + 'db' => array( + 'dsn' => 'sqlite:app.sqlite', + 'user' => 'root', + 'password' => '', + 'port' => 3306, + ), + ), $this->resolver->resolve(array( + 'db' => array( + 'dsn' => 'sqlite:app.sqlite', + ), + ))); + } + + /** + * @expectedException Symfony\Component\OptionsResolver\Exception\MissingOptionsException + * + * Nested options resolvers will always be executed, eventhough the option + * is missing. See {@link https://github.com/symfony/symfony/issues/9174} + */ + public function testNestedResolversForOptionalOption() + { + $this->resolver->setOptional(array('db')); + $this->resolver->setNestedOptionsResolver(array( + 'db' => $this->getNestedResolver(), + )); + + $this->assertEquals(array(), $this->resolver->resolve(array())); + } + + private function getNestedResolver() + { + $nestedResolver = new OptionsResolver(); + $nestedResolver->setDefaults(array( + 'user' => 'root', + 'password' => '', + 'port' => 3306, + )); + $nestedResolver->setRequired(array('dsn')); + + return $nestedResolver; + } }