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.', + ), + ); } /**