8000 [OptionsResolver] Implemented policies for treating unknown/missing options by webmozart · Pull Request #10616 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[OptionsResolver] Implemented policies for treating unknown/missing options #10616

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

< 8000 /details>
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion UPGRADE-2.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,38 @@ Form
{
```

OptionsResolver
---------------

* A new parameter `$flags` was added to `OptionsResolverInterface::resolve()`.
The parameter takes a bitwise combination of the new flag constants in that
interface.

If you implement the interface in your code, you should add the parameter as
well.

Before:

```
public function resolve(array $options = array())
{
// ...
}
```

After:

```
public function resolve(array $options = array(), $flags = 0)
{
if ($flags & self::FORBID_UNKNOWN) {
// ...
}

// ...
}
```

Validator
---------

Expand All @@ -56,7 +88,7 @@ Validator

After:

Default email validation is now done via a simple regex which may cause invalid emails (not RFC compilant) to be
Default email validation is now done via a simple regex which may cause invalid emails (not RFC compilant) to be
valid. This is the default behaviour.

Strict email validation has to be explicitly activated in the configuration file by adding
Expand Down
9 changes: 9 additions & 0 deletions src/Symfony/Component/OptionsResolver/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CHANGELOG
=========

2.5.0
-----

* [BC BREAK] added parameter $flags to OptionsResolverInterface::resolve()
* added flags FORBID_UNKNOWN, REMOVE_UNKNOWN, IGNORE_UNKNOWN, FORBID_MISSING
and IGNORE_MISSING to OptionsResolverInterface
25 changes: 20 additions & 5 deletions src/Symfony/Component/OptionsResolver/OptionsResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -214,10 +214,25 @@ public function isRequired($option)
/**
* {@inheritdoc}
*/
public function resolve(array $options = array())
public function resolve(array $options = array(), $flags = 0)
{
$this->validateOptionsExistence($options);
$this->validateOptionsCompleteness($options);
if (!($flag 8000 s & (self::FORBID_UNKNOWN | self::REMOVE_UNKNOWN | self::IGNORE_UNKNOWN))) {
$flags |= self::FORBID_UNKNOWN;
}

if (!($flags & (self::FORBID_MISSING | self::IGNORE_MISSING))) {
$flags |= self::FORBID_MISSING;
}

if ($flags & self::FORBID_UNKNOWN) {
$this->validateOptionsExistence($options);
} elseif ($flags & self::REMOVE_UNKNOWN) {
$options = array_intersect_key($options, $this->knownOptions);
}

if ($flags & self::FORBID_MISSING) {
$this->validateOptionsCompleteness($options);
}

// Make sure this method can be called multiple times
$combinedOptions = clone $this->defaultOptions;
Expand All @@ -240,7 +255,7 @@ public function resolve(array $options = array())
* Validates that the given option names exist and throws an exception
* otherwise.
*
* @param array $options An list of option names as keys.
* @param array $options A list of option names as keys.
*
* @throws InvalidOptionsException If any of the options has not been defined.
*/
Expand All @@ -264,7 +279,7 @@ private function validateOptionsExistence(array $options)
* Validates that all required options are given and throws an exception
* otherwise.
*
* @param array $options An list of option names as keys.
* @param array $options A list of option names as keys.
*
* @throws MissingOptionsException If a required option is missing.
*/
Expand Down
31 changes: 29 additions & 2 deletions src/Symfony/Component/OptionsResolver/OptionsResolverInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,31 @@
*/
interface OptionsResolverInterface
{
/**
* Throw an exception if unknown options are passed to {@link resolve()}.
*/
const FORBID_UNKNOWN = 1;

/**
* Remove unknown options passed to {@link resolve()}.
*/
const REMOVE_UNKNOWN = 2;

/**
* Ignore unknown options passed to {@link resolve()}.
*/
const IGNORE_UNKNOWN = 4;

/**
* Throw an exception if a required option is missing in {@link resolve()}.
*/
const FORBID_MISSING = 32;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is really needed (just as there is no IGNORE_INVALID) because people can just make all options optional. Btw, setRequired() followed by setOptional() does not seem to override an option to become optional.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't really compare these two things. The code that calls resolve() is not necessarily the code that configures the resolver. That was the reason why these flags were needed in the first place.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I know. I'm just saying an alternative approach could be something like the following where getKnownOptions just returns all available options, which is not there currently.

$resolver->setOptional($resolver->getKnownOptions());
$resolver->resolve(...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Tobion Do you think it would be better to add the various get*Options() (known, required, optional, ..) methods and push this functionality into userland code? The PR currently adds overhead to every code that uses the resolve() method, regardless of whether they need the flags or not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure. I think since the resolver defines the semantics like required/optional, it makes sense to also do the validation there. How much influence has the overhead? Or do you mean to also remove the validation that was already present?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I'm just talking about this PR. We need to measure the influence with a moderately big form. Do you have time for that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I don't.


/**
* Ignore missing required options in {@link resolve()}.
*/
const IGNORE_MISSING = 64;

/**
* Sets default option values.
*
Expand Down Expand Up @@ -195,7 +220,9 @@ public function isRequired($option);
/**
* Returns the combination of the default and the passed options.
*
* @param array $options The custom option values.
* @param array $options The custom option values.
* @param integer $flags A combination of the flag constants in this
* interface.
*
* @return array A list of options and their values.
*
Expand All @@ -206,5 +233,5 @@ public function isRequired($option);
* @throws Exception\OptionDefinitionException If a cyclic dependency is detected
* between two lazy options.
*/
public function resolve(array $options = array());
public function resolve(array $options = array(), $flags = 0);
}
110 changes: 109 additions & 1 deletion src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\OptionsResolver\Tests;

use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\Options;

Expand Down Expand Up @@ -175,7 +176,7 @@ public function testResolveLazyReplaceDefaults()
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testResolveFailsIfNonExistingOption()
public function testResolveFailsIfUnknownOption()
{
$this->resolver->setDefaults(array(
'one' => '1',
Expand All @@ -194,6 +195,57 @@ public function testResolveFailsIfNonExistingOption()
));
}

public function testResolveRemovesUnknownOptionIfFlagIsSet()
{
$this->resolver->setDefaults(array(
'one' => '1',
));

$this->resolver->setRequired(array(
'two',
));

$this->resolver->setOptional(array(
'three',
));

$options = $this->resolver->resolve(array(
'foo' => 'bar',
'two' => '20',
), OptionsResolverInterface::REMOVE_UNKNOWN);

$this->assertEquals(array(
'one' => '1',
'two' => '20'
), $options);
}

public function testResolveIgnoresUnknownOptionsIfFlagIsSet()
{
$this->resolver->setDefaults(array(
'one' => '1',
));

$this->resolver->setRequired(array(
'two',
));

$this->resolver->setOptional(array(
'three',
));

$options = $this->resolver->resolve(array(
'foo' => 'bar',
'two' => '20',
), OptionsResolverInterface::IGNORE_UNKNOWN);

$this->assertEquals(array(
'one' => '1',
'foo' => 'bar',
'two' => '20',
), $options);
}

/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException
*/
Expand All @@ -212,6 +264,25 @@ public function testResolveFailsIfMissingRequiredOption()
));
}

public function testResolveIgnoresMissingRequiredOptionIfFlagIsSet()
{
$this->resolver->setRequired(array(
'one',
));

$this->resolver->setDefaults(array(
'two' => '2',
));

$options = $this->resolver->resolve(array(
'two' => '20',
), OptionsResolverInterface::IGNORE_MISSING);

$this->assertEquals(array(
'two' => '20',
), $options);
}

public function testResolveSucceedsIfOptionValue A83A Allowed()
{
$this->resolver->setDefaults(array(
Expand Down Expand Up @@ -717,4 +788,41 @@ public function testClone()
'three' => '3',
), $clone->resolve());
}

public function testRemoveUnknownOption()
{
$this->resolver->setDefaults(array(
'one' => 'default',
'two' => 'default'
));

$options = $this->resolver->resolve(array(
'one' => 'keep',
'three' => 'remove'
), OptionsResolverInterface::REMOVE_UNKNOWN);

$this->assertEquals($options, array(
'one' => 'keep',
'two' => 'default'
));
}

public function testIgnoreUnknownOption()
{
$this->resolver->setDefaults(array(
'one' => 'default',
'two' => 'default'
));

$options = $this->resolver->resolve(array(
'one' => 'keep',
'three' => 'ignored'
), OptionsResolverInterface::IGNORE_UNKNOWN);

$this->assertEquals($options, array(
'one' => 'keep',
'two' => 'default',
'three' => 'ignored'
));
}
}
0