diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index aa15a2de1b827..89e651400b4c4 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -360,12 +360,12 @@ public function getDefinedOptions() * Instead of passing the message, you may also pass a closure with the * following signature: * - * function ($value) { + * function (Options $options, $value): string { * // ... * } * * The closure receives the value as argument and should return a string. - * Returns an empty string to ignore the option deprecation. + * Return an empty string to ignore the option deprecation. * * The closure is invoked when {@link resolve()} is called. The parameter * passed to the closure is the value of the option after validating it @@ -860,8 +860,20 @@ public function offsetGet($option) if (isset($this->deprecated[$option])) { $deprecationMessage = $this->deprecated[$option]; - if ($deprecationMessage instanceof \Closure && !\is_string($deprecationMessage = $deprecationMessage($value))) { - throw new InvalidOptionsException(sprintf('Invalid type for deprecation message, expected string but got "%s", returns an empty string to ignore.', \gettype($deprecationMessage))); + if ($deprecationMessage instanceof \Closure) { + // If the closure is already being called, we have a cyclic dependency + if (isset($this->calling[$option])) { + throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', array_keys($this->calling)))); + } + + $this->calling[$option] = true; + try { + if (!\is_string($deprecationMessage = $deprecationMessage($this, $value))) { + throw new InvalidOptionsException(sprintf('Invalid type for deprecation message, expected string but got "%s", return an empty string to ignore.', \gettype($deprecationMessage))); + } + } finally { + unset($this->calling[$option]); + } } if ('' !== $deprecationMessage) { diff --git a/src/Symfony/Component/OptionsResolver/Tests/Debug/OptionsResolverIntrospectorTest.php b/src/Symfony/Component/OptionsResolver/Tests/Debug/OptionsResolverIntrospectorTest.php index b146d28d6624e..4bdce6f807a07 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/Debug/OptionsResolverIntrospectorTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/Debug/OptionsResolverIntrospectorTest.php @@ -215,7 +215,7 @@ public function testGetClosureDeprecationMessage() { $resolver = new OptionsResolver(); $resolver->setDefined('foo'); - $resolver->setDeprecated('foo', $closure = function ($value) {}); + $resolver->setDeprecated('foo', $closure = function (Options $options, $value) {}); $debug = new OptionsResolverIntrospector($resolver); $this->assertSame($closure, $debug->getDeprecationMessage('foo')); diff --git a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php index 2ed6face53378..d94169ad7184d 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php @@ -486,19 +486,38 @@ public function testSetDeprecatedFailsIfInvalidDeprecationMessageType() /** * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidArgumentException - * @expectedExceptionMessage Invalid type for deprecation message, expected string but got "boolean", returns an empty string to ignore. + * @expectedExceptionMessage Invalid type for deprecation message, expected string but got "boolean", return an empty string to ignore. */ public function testLazyDeprecationFailsIfInvalidDeprecationMessageType() { $this->resolver ->setDefault('foo', true) - ->setDeprecated('foo', function ($value) { + ->setDeprecated('foo', function (Options $options, $value) { return false; }) ; $this->resolver->resolve(); } + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + * @expectedExceptionMessage The options "foo", "bar" have a cyclic dependency. + */ + public function testFailsIfCyclicDependencyBetweenDeprecation() + { + $this->resolver + ->setDefault('foo', null) + ->setDefault('bar', null) + ->setDeprecated('foo', function (Options $options, $value) { + $options['bar']; + }) + ->setDeprecated('bar', function (Options $options, $value) { + $options['foo']; + }) + ; + $this->resolver->resolve(); + } + public function testIsDeprecated() { $this->resolver @@ -590,7 +609,7 @@ function (OptionsResolver $resolver) { $resolver ->setDefault('foo', null) ->setAllowedTypes('foo', array('null', 'string', \stdClass::class)) - ->setDeprecated('foo', function ($value) { + ->setDeprecated('foo', function (Options $options, $value) { if ($value instanceof \stdClass) { return sprintf('Passing an instance of "%s" to option "foo" is deprecated, pass its FQCN instead.', \stdClass::class); } @@ -621,7 +640,7 @@ function (OptionsResolver $resolver) { function (OptionsResolver $resolver) { $resolver ->setDefault('foo', null) - ->setDeprecated('foo', function ($value) { + ->setDeprecated('foo', function (Options $options, $value) { return ''; }) ; @@ -629,6 +648,27 @@ function (OptionsResolver $resolver) { array('foo' => Bar::class), null, ); + + yield 'It deprecates value depending on other option value' => array( + function (OptionsResolver $resolver) { + $resolver + ->setDefault('widget', null) + ->setDefault('date_format', null) + ->setDeprecated('date_format', function (Options $options, $dateFormat) { + if (null !== $dateFormat && 'single_text' === $options['widget']) { + return 'Using the "date_format" option when the "widget" option is set to "single_text" is deprecated.'; + } + + return ''; + }) + ; + }, + array('widget' => 'single_text', 'date_format' => 2), + array( + 'type' => E_USER_DEPRECATED, + 'message' => 'Using the "date_format" option when the "widget" option is set to "single_text" is deprecated.', + ), + ); } /**
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: