diff --git a/src/Symfony/Component/OptionsResolver/Options.php b/src/Symfony/Component/OptionsResolver/Options.php index d444ec4230d51..d18374cb91f6b 100644 --- a/src/Symfony/Component/OptionsResolver/Options.php +++ b/src/Symfony/Component/OptionsResolver/Options.php @@ -16,6 +16,8 @@ * * @author Bernhard Schussek * @author Tobias Schultze + * + * @method mixed offsetGet(string $option, bool $triggerDeprecation = true) */ interface Options extends \ArrayAccess, \Countable { diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index c9a26d2469131..2a9daa9bbc5fd 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -88,6 +88,11 @@ class OptionsResolver implements Options */ private $deprecated = array(); + /** + * The list of options provided by the user. + */ + private $given = array(); + /** * Whether the instance is locked for reading. * @@ -744,6 +749,7 @@ public function resolve(array $options = array()) // Override options set by the user foreach ($options as $option => $value) { + $clone->given[$option] = true; $clone->defaults[$option] = $value; unset($clone->resolved[$option], $clone->lazy[$option]); } @@ -772,7 +778,8 @@ public function resolve(array $options = array()) /** * Returns the resolved value of an option. * - * @param string $option The option name + * @param string $option The option name + * @param bool $triggerDeprecation Whether to trigger the deprecation or not (true by default) * * @return mixed The option value * @@ -784,14 +791,20 @@ public function resolve(array $options = array()) * @throws OptionDefinitionException If there is a cyclic dependency between * lazy options and/or normalizers */ - public function offsetGet($option) + public function offsetGet($option/*, bool $triggerDeprecation = true*/) { if (!$this->locked) { throw new AccessException('Array access is only supported within closures of lazy options and normalizers.'); } + $triggerDeprecation = 1 === \func_num_args() || \func_get_arg(1); + // Shortcut for resolved options if (array_key_exists($option, $this->resolved)) { + if ($triggerDeprecation && isset($this->deprecated[$option]) && \is_string($this->deprecated[$option])) { + @trigger_error(strtr($this->deprecated[$option], array('%name%' => $option)), E_USER_DEPRECATED); + } + return $this->resolved[$option]; } @@ -920,7 +933,8 @@ public function offsetGet($option) } // Check whether the option is deprecated - if (isset($this->deprecated[$option])) { + // and it is provided by the user or is being called from a lazy evaluation + if ($triggerDeprecation && isset($this->deprecated[$option]) && (isset($this->given[$option]) || ($this->calling && \is_string($this->deprecated[$option])))) { $deprecationMessage = $this->deprecated[$option]; if ($deprecationMessage instanceof \Closure) { diff --git a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php index b47158936f0fd..a3494ed185fc1 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php @@ -491,12 +491,12 @@ public function testSetDeprecatedFailsIfInvalidDeprecationMessageType() public function testLazyDeprecationFailsIfInvalidDeprecationMessageType() { $this->resolver - ->setDefault('foo', true) + ->setDefined('foo') ->setDeprecated('foo', function (Options $options, $value) { return false; }) ; - $this->resolver->resolve(); + $this->resolver->resolve(array('foo' => null)); } /** @@ -506,8 +506,7 @@ public function testLazyDeprecationFailsIfInvalidDeprecationMessageType() public function testFailsIfCyclicDependencyBetweenDeprecation() { $this->resolver - ->setDefault('foo', null) - ->setDefault('bar', null) + ->setDefined(array('foo', 'bar')) ->setDeprecated('foo', function (Options $options, $value) { $options['bar']; }) @@ -515,7 +514,7 @@ public function testFailsIfCyclicDependencyBetweenDeprecation() $options['foo']; }) ; - $this->resolver->resolve(); + $this->resolver->resolve(array('foo' => null, 'bar' => null)); } public function testIsDeprecated() @@ -539,10 +538,15 @@ public function testIsNotDeprecatedIfEmptyString() /** * @dataProvider provideDeprecationData */ - public function testDeprecationMessages(\Closure $configureOptions, array $options, ?array $expectedError) + public function testDeprecationMessages(\Closure $configureOptions, array $options, ?array $expectedError, int $expectedCount) { + $count = 0; error_clear_last(); - set_error_handler(function () { return false; }); + set_error_handler(function () use (&$count) { + ++$count; + + return false; + }); $e = error_reporting(0); $configureOptions($this->resolver); @@ -555,6 +559,7 @@ public function testDeprecationMessages(\Closure $configureOptions, array $optio unset($lastError['file'], $lastError['line']); $this->assertSame($expectedError, $lastError); + $this->assertSame($expectedCount, $count); } public function provideDeprecationData() @@ -571,6 +576,7 @@ function (OptionsResolver $resolver) { 'type' => E_USER_DEPRECATED, 'message' => 'The option "foo" is deprecated.', ), + 1, ); yield 'It deprecates an option with custom message' => array( @@ -588,20 +594,27 @@ function (OptionsResolver $resolver) { 'type' => E_USER_DEPRECATED, 'message' => 'The option "foo" is deprecated, use "bar" option instead.', ), + 2, ); - yield 'It deprecates a missing option with default value' => array( + yield 'It deprecates an option evaluated in another definition' => array( function (OptionsResolver $resolver) { + // defined by superclass $resolver - ->setDefaults(array('foo' => null, 'bar' => null)) + ->setDefault('foo', null) ->setDeprecated('foo') ; + // defined by subclass + $resolver->setDefault('bar', function (Options $options) { + return $options['foo']; // It triggers a deprecation + }); }, - array('bar' => 'baz'), + array(), array( 'type' => E_USER_DEPRECATED, 'message' => 'The option "foo" is deprecated.', ), + 1, ); yield 'It deprecates allowed type and value' => array( @@ -623,20 +636,46 @@ function (OptionsResolver $resolver) { 'type' => E_USER_DEPRECATED, 'message' => 'Passing an instance of "stdClass" to option "foo" is deprecated, pass its FQCN instead.', ), + 1, ); - yield 'It ignores deprecation for missing option without default value' => array( + yield 'It triggers a deprecation based on the value only if option is provided by the user' => array( function (OptionsResolver $resolver) { $resolver - ->setDefined(array('foo', 'bar')) - ->setDeprecated('foo') + ->setDefined('foo') + ->setAllowedTypes('foo', array('null', 'bool')) + ->setDeprecated('foo', function (Options $options, $value) { + if (!\is_bool($value)) { + return 'Passing a value different than true or false is deprecated.'; + } + + return ''; + }) + ->setDefault('baz', null) + ->setAllowedTypes('baz', array('null', 'int')) + ->setDeprecated('baz', function (Options $options, $value) { + if (!\is_int($value)) { + return 'Not passing an integer is deprecated.'; + } + + return ''; + }) + ->setDefault('bar', function (Options $options) { + $options['baz']; // It does not triggers a deprecation + + return $options['foo']; // It does not triggers a deprecation + }) ; }, - array('bar' => 'baz'), - null, + array('foo' => null), // It triggers a deprecation + array( + 'type' => E_USER_DEPRECATED, + 'message' => 'Passing a value different than true or false is deprecated.', + ), + 1, ); - yield 'It ignores deprecation if closure returns an empty string' => array( + yield 'It ignores a deprecation if closure returns an empty string' => array( function (OptionsResolver $resolver) { $resolver ->setDefault('foo', null) @@ -647,6 +686,7 @@ function (OptionsResolver $resolver) { }, array('foo' => Bar::class), null, + 0, ); yield 'It deprecates value depending on other option value' => array( @@ -668,6 +708,62 @@ function (OptionsResolver $resolver) { 'type' => E_USER_DEPRECATED, 'message' => 'Using the "date_format" option when the "widget" option is set to "single_text" is deprecated.', ), + 1, + ); + + yield 'It triggers a deprecation for each evaluation' => array( + function (OptionsResolver $resolver) { + $resolver + // defined by superclass + ->setDefined('foo') + ->setDeprecated('foo') + // defined by subclass + ->setDefault('bar', function (Options $options) { + return $options['foo']; // It triggers a deprecation + }) + ->setNormalizer('bar', function (Options $options, $value) { + $options['foo']; // It triggers a deprecation + $options['foo']; // It triggers a deprecation + + return $value; + }) + ; + }, + array('foo' => 'baz'), // It triggers a deprecation + array( + 'type' => E_USER_DEPRECATED, + 'message' => 'The option "foo" is deprecated.', + ), + 4, + ); + + yield 'It ignores a deprecation if no option is provided by the user' => array( + function (OptionsResolver $resolver) { + $resolver + ->setDefined('foo') + ->setDefault('bar', null) + ->setDeprecated('foo') + ->setDeprecated('bar') + ; + }, + array(), + null, + 0, + ); + + yield 'It explicitly ignores a depreciation' => array( + function (OptionsResolver $resolver) { + $resolver + ->setDefault('foo', null) + ->setDeprecated('foo') + ->setDefault('bar', function (Options $options) { + return $options->offsetGet('foo', false); + }) + ; + }, + array(), + null, + 0, ); }