From cf41254223e076e9ae89017c13514f6441de7083 Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Tue, 19 Feb 2019 15:58:35 -0500 Subject: [PATCH] [OptionsResolver] Add a new method addNormalizer and normalization hierarchy --- .../Component/OptionsResolver/CHANGELOG.md | 5 ++ .../Debug/OptionsResolverIntrospector.php | 8 +++ .../OptionsResolver/OptionsResolver.php | 59 +++++++++++++++++-- .../Debug/OptionsResolverIntrospectorTest.php | 36 +++++++++++ .../Tests/OptionsResolverTest.php | 57 ++++++++++++++++++ 5 files changed, 160 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/OptionsResolver/CHANGELOG.md b/src/Symfony/Component/OptionsResolver/CHANGELOG.md index a6f8f0ff47c03..60ea43f7ec20c 100644 --- a/src/Symfony/Component/OptionsResolver/CHANGELOG.md +++ b/src/Symfony/Component/OptionsResolver/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +4.3.0 +----- + + * added `OptionsResolver::addNormalizer` method + 4.2.0 ----- diff --git a/src/Symfony/Component/OptionsResolver/Debug/OptionsResolverIntrospector.php b/src/Symfony/Component/OptionsResolver/Debug/OptionsResolverIntrospector.php index ccce082fed29d..9ce5263334e15 100644 --- a/src/Symfony/Component/OptionsResolver/Debug/OptionsResolverIntrospector.php +++ b/src/Symfony/Component/OptionsResolver/Debug/OptionsResolverIntrospector.php @@ -84,6 +84,14 @@ public function getAllowedValues(string $option): array * @throws NoConfigurationException on no configured normalizer */ public function getNormalizer(string $option): \Closure + { + return current($this->getNormalizers($option)); + } + + /** + * @throws NoConfigurationException when no normalizer is configured + */ + public function getNormalizers(string $option): array { return ($this->get)('normalizers', $option, sprintf('No normalizer was set for the "%s" option.', $option)); } diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index eb0a6c2480601..0e68e75ff83ce 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -57,7 +57,7 @@ class OptionsResolver implements Options /** * A list of normalizer closures. * - * @var \Closure[] + * @var \Closure[][] */ private $normalizers = []; @@ -484,7 +484,56 @@ public function setNormalizer($option, \Closure $normalizer) throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined)))); } - $this->normalizers[$option] = $normalizer; + $this->normalizers[$option] = [$normalizer]; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Adds a normalizer for an option. + * + * The normalizer should be a closure with the following signature: + * + * function (Options $options, $value): mixed { + * // ... + * } + * + * The closure is invoked when {@link resolve()} is called. The closure + * has access to the resolved values of other options through the passed + * {@link Options} instance. + * + * The second parameter passed to the closure is the value of + * the option. + * + * The resolved option value is set to the return value of the closure. + * + * @param string $option The option name + * @param \Closure $normalizer The normalizer + * @param bool $forcePrepend If set to true, prepend instead of appending + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function addNormalizer(string $option, \Closure $normalizer, bool $forcePrepend = false): self + { + if ($this->locked) { + throw new AccessException('Normalizers cannot be set from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined)))); + } + + if ($forcePrepend) { + array_unshift($this->normalizers[$option], $normalizer); + } else { + $this->normalizers[$option][] = $normalizer; + } // Make sure the option is processed unset($this->resolved[$option]); @@ -966,15 +1015,15 @@ public function offsetGet($option/*, bool $triggerDeprecation = true*/) throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', array_keys($this->calling)))); } - $normalizer = $this->normalizers[$option]; - // The following section must be protected from cyclic // calls. Set $calling for the current $option to detect a cyclic // dependency // BEGIN $this->calling[$option] = true; try { - $value = $normalizer($this, $value); + foreach ($this->normalizers[$option] as $normalizer) { + $value = $normalizer($this, $value); + } } finally { unset($this->calling[$option]); } diff --git a/src/Symfony/Component/OptionsResolver/Tests/Debug/OptionsResolverIntrospectorTest.php b/src/Symfony/Component/OptionsResolver/Tests/Debug/OptionsResolverIntrospectorTest.php index c6615573cd881..64a1ead1fe014 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/Debug/OptionsResolverIntrospectorTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/Debug/OptionsResolverIntrospectorTest.php @@ -201,6 +201,42 @@ public function testGetNormalizerThrowsOnNotDefinedOption() $this->assertSame('bar', $debug->getNormalizer('foo')); } + public function testGetNormalizers() + { + $resolver = new OptionsResolver(); + $resolver->setDefined('foo'); + $resolver->addNormalizer('foo', $normalizer1 = function () {}); + $resolver->addNormalizer('foo', $normalizer2 = function () {}); + + $debug = new OptionsResolverIntrospector($resolver); + $this->assertSame([$normalizer1, $normalizer2], $debug->getNormalizers('foo')); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\NoConfigurationException + * @expectedExceptionMessage No normalizer was set for the "foo" option. + */ + public function testGetNormalizersThrowsOnNoConfiguredValue() + { + $resolver = new OptionsResolver(); + $resolver->setDefined('foo'); + + $debug = new OptionsResolverIntrospector($resolver); + $debug->getNormalizers('foo'); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + * @expectedExceptionMessage The option "foo" does not exist. + */ + public function testGetNormalizersThrowsOnNotDefinedOption() + { + $resolver = new OptionsResolver(); + + $debug = new OptionsResolverIntrospector($resolver); + $debug->getNormalizers('foo'); + } + public function testGetDeprecationMessage() { $resolver = new OptionsResolver(); diff --git a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php index 886a88ebfa66c..edf53cd2e2565 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php @@ -1554,6 +1554,63 @@ public function testNormalizerNotCalledForUnsetOptions() $this->assertEmpty($this->resolver->resolve()); } + public function testAddNormalizerReturnsThis() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame($this->resolver, $this->resolver->addNormalizer('foo', function () {})); + } + + public function testAddNormalizerClosure() + { + // defined by superclass + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setNormalizer('foo', function (Options $options, $value) { + return '1st-normalized-'.$value; + }); + // defined by subclass + $this->resolver->addNormalizer('foo', function (Options $options, $value) { + return '2nd-normalized-'.$value; + }); + + $this->assertEquals(['foo' => '2nd-normalized-1st-normalized-bar'], $this->resolver->resolve()); + } + + public function testForcePrependNormalizerClosure() + { + // defined by superclass + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setNormalizer('foo', function (Options $options, $value) { + return '2nd-normalized-'.$value; + }); + // defined by subclass + $this->resolver->addNormalizer('foo', function (Options $options, $value) { + return '1st-normalized-'.$value; + }, true); + + $this->assertEquals(['foo' => '2nd-normalized-1st-normalized-bar'], $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testAddNormalizerFailsIfUnknownOption() + { + $this->resolver->addNormalizer('foo', function () {}); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfAddNormalizerFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->addNormalizer('foo', function () {}); + }); + + $this->resolver->resolve(); + } + public function testSetDefaultsReturnsThis() { $this->assertSame($this->resolver, $this->resolver->setDefaults(['foo', 'bar']));