Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit ce24e15

Browse filesBrowse files
committed
feature #30371 [OptionsResolver] Add a new method addNormalizer and normalization hierarchy (yceruto)
This PR was squashed before being merged into the 4.3-dev branch (closes #30371). Discussion ---------- [OptionsResolver] Add a new method addNormalizer and normalization hierarchy | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #30310 | License | MIT | Doc PR | symfony/symfony-docs#11103 ### 3rd-party package <details><summary>Given: (CLICK ME)</summary> <p> Generic type: ```php class FooType extends AbstractType { private $registry; public function __construct(ManagerRegistry $registry) { $this->registry = $registry; } // buildForm ... public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('class'); $resolver->setDefaults([ 'em' => null, 'query' => null, ]); $resolver->setAllowedTypes('em', ['null', 'string']); $resolver->setAllowedTypes('query', ['null', 'callable']); $resolver->setNormalizer('em', function (Options $options, $em) { if (null !== $em) { return $this->registry->getManager($em); } return $this->registry->getManagerForClass($options['class']); }); $resolver->setNormalizer('query', function (Options $options, $query) { if (\is_callable($query)) { $query = $query($options['em']->getRepository($options['class'])); if (!$query instanceof Query) { throw new UnexpectedTypeException($query, 'Doctrine\ORM\Query'); } } return $query; }); } } ``` </p> </details> ### App context <details><summary>Before (CLICK ME)</summary> <p> Normalizing the new allowed value will require to override the parent's normalizer: ```php class BarType extends AbstractType { private $registry; public function __construct(ManagerRegistry $registry) { $this->registry = $registry; } // buildForm ... public function configureOptions(OptionsResolver $resolver): void { $resolver->addAllowedTypes('em', 'Doctrine\ORM\EntityManagerInterface'); $resolver->setNormalizer('em', function (Options $options, $em) { if ($em instanceof EntityManagerInterface) { return $em; } if (null !== $em) { return $this->registry->getManager($em); } return $this->registry->getManagerForClass($options['class']); }); $resolver->addAllowedTypes('query', 'string'); $resolver->setNormalizer('query', function (Options $options, $query) { if (\is_callable($query)) { $query = $query($options['em']->getRepository($options['class'])); if (!$query instanceof Query) { throw new UnexpectedTypeException($query, 'Doctrine\ORM\Query'); } } if (\is_string($query)) { $query = $options['em']->createQuery($query); } return $query; }); } public function getParent() { return FooType::class; } } ``` </p> </details> <details><summary>After (CLICK ME)</summary> <p> The new normalizer is added to the stack and it'll receive the previously normalized value or if `forcePrepend = true` the validated value: ```php class BarType extends AbstractType { // buildForm ... public function configureOptions(OptionsResolver $resolver): void { $resolver->addAllowedTypes('em', 'Doctrine\ORM\EntityManagerInterface'); $resolver->addNormalizer('em', function (Options $options, $em) { if ($em instanceof EntityManagerInterface) { return $em; } return $em; }, true); // $forcePrepend = true (3rd argument) $resolver->addAllowedTypes('query', 'string'); $resolver->addNormalizer('query', function (Options $options, $query) { if (\is_string($query)) { $query = $options['em']->createQuery($query); } return $query; }); } public function getParent() { return FooType::class; } } ``` </p> </details> Commits ------- cf41254 [OptionsResolver] Add a new method addNormalizer and normalization hierarchy
2 parents 7554cf6 + cf41254 commit ce24e15
Copy full SHA for ce24e15

File tree

Expand file treeCollapse file tree

5 files changed

+160
-5
lines changed
Filter options
Expand file treeCollapse file tree

5 files changed

+160
-5
lines changed

‎src/Symfony/Component/OptionsResolver/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/OptionsResolver/CHANGELOG.md
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
4.3.0
5+
-----
6+
7+
* added `OptionsResolver::addNormalizer` method
8+
49
4.2.0
510
-----
611

‎src/Symfony/Component/OptionsResolver/Debug/OptionsResolverIntrospector.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/OptionsResolver/Debug/OptionsResolverIntrospector.php
+8Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ public function getAllowedValues(string $option): array
8484
* @throws NoConfigurationException on no configured normalizer
8585
*/
8686
public function getNormalizer(string $option): \Closure
87+
{
88+
return current($this->getNormalizers($option));
89+
}
90+
91+
/**
92+
* @throws NoConfigurationException when no normalizer is configured
93+
*/
94+
public function getNormalizers(string $option): array
8795
{
8896
return ($this->get)('normalizers', $option, sprintf('No normalizer was set for the "%s" option.', $option));
8997
}

‎src/Symfony/Component/OptionsResolver/OptionsResolver.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/OptionsResolver/OptionsResolver.php
+54-5Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class OptionsResolver implements Options
5757
/**
5858
* A list of normalizer closures.
5959
*
60-
* @var \Closure[]
60+
* @var \Closure[][]
6161
*/
6262
private $normalizers = [];
6363

@@ -484,7 +484,56 @@ public function setNormalizer($option, \Closure $normalizer)
484484
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
485485
}
486486

487-
$this->normalizers[$option] = $normalizer;
487+
$this->normalizers[$option] = [$normalizer];
488+
489+
// Make sure the option is processed
490+
unset($this->resolved[$option]);
491+
492+
return $this;
493+
}
494+
495+
/**
496+
* Adds a normalizer for an option.
497+
*
498+
* The normalizer should be a closure with the following signature:
499+
*
500+
* function (Options $options, $value): mixed {
501+
* // ...
502+
* }
503+
*
504+
* The closure is invoked when {@link resolve()} is called. The closure
505+
* has access to the resolved values of other options through the passed
506+
* {@link Options} instance.
507+
*
508+
* The second parameter passed to the closure is the value of
509+
* the option.
510+
*
511+
* The resolved option value is set to the return value of the closure.
512+
*
513+
* @param string $option The option name
514+
* @param \Closure $normalizer The normalizer
515+
* @param bool $forcePrepend If set to true, prepend instead of appending
516+
*
517+
* @return $this
518+
*
519+
* @throws UndefinedOptionsException If the option is undefined
520+
* @throws AccessException If called from a lazy option or normalizer
521+
*/
522+
public function addNormalizer(string $option, \Closure $normalizer, bool $forcePrepend = false): self
523+
{
524+
if ($this->locked) {
525+
throw new AccessException('Normalizers cannot be set from a lazy option or normalizer.');
526+
}
527+
528+
if (!isset($this->defined[$option])) {
529+
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
530+
}
531+
532+
if ($forcePrepend) {
533+
array_unshift($this->normalizers[$option], $normalizer);
534+
} else {
535+
$this->normalizers[$option][] = $normalizer;
536+
}
488537

489538
// Make sure the option is processed
490539
unset($this->resolved[$option]);
@@ -966,15 +1015,15 @@ public function offsetGet($option/*, bool $triggerDeprecation = true*/)
9661015
throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', array_keys($this->calling))));
9671016
}
9681017

969-
$normalizer = $this->normalizers[$option];
970-
9711018
// The following section must be protected from cyclic
9721019
// calls. Set $calling for the current $option to detect a cyclic
9731020
// dependency
9741021
// BEGIN
9751022
$this->calling[$option] = true;
9761023
try {
977-
$value = $normalizer($this, $value);
1024+
foreach ($this->normalizers[$option] as $normalizer) {
1025+
$value = $normalizer($this, $value);
1026+
}
9781027
} finally {
9791028
unset($this->calling[$option]);
9801029
}

‎src/Symfony/Component/OptionsResolver/Tests/Debug/OptionsResolverIntrospectorTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/OptionsResolver/Tests/Debug/OptionsResolverIntrospectorTest.php
+36Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,42 @@ public function testGetNormalizerThrowsOnNotDefinedOption()
201201
$this->assertSame('bar', $debug->getNormalizer('foo'));
202202
}
203203

204+
public function testGetNormalizers()
205+
{
206+
$resolver = new OptionsResolver();
207+
$resolver->setDefined('foo');
208+
$resolver->addNormalizer('foo', $normalizer1 = function () {});
209+
$resolver->addNormalizer('foo', $normalizer2 = function () {});
210+
211+
$debug = new OptionsResolverIntrospector($resolver);
212+
$this->assertSame([$normalizer1, $normalizer2], $debug->getNormalizers('foo'));
213+
}
214+
215+
/**
216+
* @expectedException \Symfony\Component\OptionsResolver\Exception\NoConfigurationException
217+
* @expectedExceptionMessage No normalizer was set for the "foo" option.
218+
*/
219+
public function testGetNormalizersThrowsOnNoConfiguredValue()
220+
{
221+
$resolver = new OptionsResolver();
222+
$resolver->setDefined('foo');
223+
224+
$debug = new OptionsResolverIntrospector($resolver);
225+
$debug->getNormalizers('foo');
226+
}
227+
228+
/**
229+
* @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException
230+
* @expectedExceptionMessage The option "foo" does not exist.
231+
*/
232+
public function testGetNormalizersThrowsOnNotDefinedOption()
233+
{
234+
$resolver = new OptionsResolver();
235+
236+
$debug = new OptionsResolverIntrospector($resolver);
237+
$debug->getNormalizers('foo');
238+
}
239+
204240
public function testGetDeprecationMessage()
205241
{
206242
$resolver = new OptionsResolver();

‎src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php
+57Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,6 +1554,63 @@ public function testNormalizerNotCalledForUnsetOptions()
15541554
$this->assertEmpty($this->resolver->resolve());
15551555
}
15561556

1557+
public function testAddNormalizerReturnsThis()
1558+
{
1559+
$this->resolver->setDefault('foo', 'bar');
1560+
1561+
$this->assertSame($this->resolver, $this->resolver->addNormalizer('foo', function () {}));
1562+
}
1563+
1564+
public function testAddNormalizerClosure()
1565+
{
1566+
// defined by superclass
1567+
$this->resolver->setDefault('foo', 'bar');
1568+
$this->resolver->setNormalizer('foo', function (Options $options, $value) {
1569+
return '1st-normalized-'.$value;
1570+
});
1571+
// defined by subclass
1572+
$this->resolver->addNormalizer('foo', function (Options $options, $value) {
1573+
return '2nd-normalized-'.$value;
1574+
});
1575+
1576+
$this->assertEquals(['foo' => '2nd-normalized-1st-normalized-bar'], $this->resolver->resolve());
1577+
}
1578+
1579+
public function testForcePrependNormalizerClosure()
1580+
{
1581+
// defined by superclass
1582+
$this->resolver->setDefault('foo', 'bar');
1583+
$this->resolver->setNormalizer('foo', function (Options $options, $value) {
1584+
return '2nd-normalized-'.$value;
1585+
});
1586+
// defined by subclass
1587+
$this->resolver->addNormalizer('foo', function (Options $options, $value) {
1588+
return '1st-normalized-'.$value;
1589+
}, true);
1590+
1591+
$this->assertEquals(['foo' => '2nd-normalized-1st-normalized-bar'], $this->resolver->resolve());
1592+
}
1593+
1594+
/**
1595+
* @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException
1596+
*/
1597+
public function testAddNormalizerFailsIfUnknownOption()
1598+
{
1599+
$this->resolver->addNormalizer('foo', function () {});
1600+
}
1601+
1602+
/**
1603+
* @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException
1604+
*/
1605+
public function testFailIfAddNormalizerFromLazyOption()
1606+
{
1607+
$this->resolver->setDefault('foo', function (Options $options) {
1608+
$options->addNormalizer('foo', function () {});
1609+
});
1610+
1611+
$this->resolver->resolve();
1612+
}
1613+
15571614
public function testSetDefaultsReturnsThis()
15581615
{
15591616
$this->assertSame($this->resolver, $this->resolver->setDefaults(['foo', 'bar']));

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.