Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[OptionsResolver] Merged Options class into OptionsResolver #12156

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

Merged
merged 1 commit into from
Oct 22, 2014

Conversation

webmozart
Copy link
Contributor

Q A
Bug fix? yes
New feature? no
BC breaks? yes
Deprecations? yes
Tests pass? yes
Fixed tickets #4500, #9174, #10585, #10202, #11020, makes #7979+#10616 obsolete
License MIT
Doc PR symfony/symfony-docs#4159
#11716 was reverted as preparation of this PR (453882c).

The Options class was merged into OptionsResolver. This made it possible to fix several tickets, as indicated above.

Usage

See the updated documentation.

Bug Fixes

Previously, the options weren't validated before the normalizers were called. As a result, the normalizers had to perform validation again:

$resolver->setAllowedTypes(array(
    'choices' => 'array',
));
$resolver->setNormalizers(array(
    'choices' => function (Options $options, $value) {
         array_merge($options['choices'], ...);
    },
));

// fatal error
$resolver->resolve(array('choices' => 'foobar'));

This is fixed now.

BC Breaks

The array type hint was removed from setRequired(), setAllowedValues(), addAllowedValues(), setAllowedTypes() and addAllowedTypes(). If anybody implemented OptionsResolverInterface, they must adapt their code.

The Options class was turned into an interface extending ArrayAccess and Countable. Anybody instantiating Options directly should instantiate OptionsResolver instead. Anybody using any of the methods available in Options (get(), has()) should use the ArrayAccess interface instead.

Normalizers are not called anymore for undefined options (#9174). People need to set a default value if they want a normalizer to be executed independent of the options passed to resolve().

Deprecations

OptionsResolverInterface was deprecated and will be removed in 3.0. OptionsResolver instances are not supposed to be shared between classes, hence an interface does not make sense.

Several other methods were deprecated. See the CHANGELOG and UPGRADE-2.6 files for information.

Todo

  • Fix PHPDoc
  • Adapt CHANGELOG/UPGRADE
  • Adapt documentation
  • Deprecate OptionsResolver[Interface]

@linaori
Copy link
Contributor

linaori commented Oct 6, 2014

I'm not really happy seeing this being static, what's the argument against $options->resolve($defaults)?

@stof
Copy link
Member

stof commented Oct 6, 2014

@iltar having to instantiate a resolver to resolver simple defaults makes it much slower, and also more complex to use. The static call is an enhanced version of options = array_merge($defaults, $options), allowing to validate options instead of just bring defaults in. AFAIK, nobody has ever complained that merging the defaults in your methods is using procedural code generally.

And as you can see, the usage of an Options instance allows to provide extension points for the definition of the defaults (which is what the component aims to solve for forms)

@linaori
Copy link
Contributor

linaori commented Oct 6, 2014

@stof in both examples provided, the argument passed to Options::resolve() is an instance of Options. Following the examples, I prefer the api listed below as it's less thinking and makes it more intuitive .

// Basic option resolving
$options = $options->resolve(array(
    'choices' = array(),
));

// More advanced option configuration
$defaults = new Options();
$defaults['choices'] = array();
$defaults->setRequired('em');

$options = $options->resolve($defaults);

// Form component example (3.0)
public function setDefaultOptions(Options $options) {
    $options['choices'] = array();

    $options->setRequired('em');
}

@stof
Copy link
Member

stof commented Oct 6, 2014

@iltar the basic usage in @webmozart's comment does not require you to build an object. And $options is the array of values provided by the user of your code

@linaori
Copy link
Contributor

linaori commented Oct 6, 2014

@stof so if I understand it correctly, the idea is to also allow an array. In that case, this is a good solution. I'm still not in favor of static methods in my code, but I supposed this will be behind the scenes. What will be the case when someone implements their own Options implementation?

@sstok
Copy link
Contributor

sstok commented Oct 7, 2014

So does this also make it possible to use nested options as described in #4833? :)

@webmozart
Copy link
Contributor Author

@iltar As @stof said, Options::resolve($options, $defaults) is an enhanced version of array_replace($defaults, $options). This is very low-level code - a static call really shouldn't matter there. Being that low-level, own Options implementations are not anticipated nor supported.

@sstok Not yet.

@webmozart
Copy link
Contributor Author

I finished the PR now.

@webmozart
Copy link
Contributor Author

I'm currently adapting the PR so that we can close #10616. I'm also preparing the implementation of #4833 in 2.7+ without breaking BC. I need some feedback to complete that.

In 2.6, I changed Options to allow for two different resolving syntaxes:

The simple syntax without any objects, designed for high performance (array_replace() on steroids):

public function __construct(array $options = array())
{
    Options::validateRequired($options, 'em');
    Options::validateTypes($options, array(
        'em' => 'Doctrine\ORM\EntityManager',
    ));

    $this->options = Options::resolve($options, array(
        'class' => null,
        'query_builder' => null,
    ));
}

And the object-based syntax with the flexibility that we know of the OptionsResolver (lazy options, normalization...):

public function __construct(array $options = array())
{
    $defaults = new Options();
    $defaults['class'] = null;
    $defaults['query_builder'] = null;
    $defaults->setRequired('em');
    $defaults->setAllowedTypes(array(
        'em' => 'Doctrine\ORM\EntityManager',
    ));

    $this->options = Options::resolve($options, $defaults);
}

My question to you is: Would any of you actually use the first mentioned, static API? Or should we remove it and make resolve() an instance method usable like this:

public function __construct(array $options = array())
{
    $defaults = new Options();
    // ...

    $this->options = $defaults->resolve($options);
}

@fabpot
Copy link
Member

fabpot commented Oct 14, 2014

The static method syntax was introduced in 2.6 so it can be removed now (or at any time before the first RC version.)

@fabpot
Copy link
Member

fabpot commented Oct 14, 2014

By the way, I'm all for removing the static API.

@jameshalsall
Copy link
Contributor

👍 for removing static API

@docteurklein
Copy link
Contributor

same, +1 for removing static api

@linaori
Copy link
Contributor

linaori commented Oct 14, 2014

Less is more, and I'm usually against static. I would definitely use the last example.

@webmozart
Copy link
Contributor Author

Good, thanks for the feedback!

@webmozart webmozart force-pushed the issue4500 branch 2 times, most recently from 891b387 to c9b0471 Compare October 14, 2014 14:27
@webmozart
Copy link
Contributor Author

I updated the PR and the above description now.

static::validateNames($options, $defaults->options, true);
// If an option is a closure that should be evaluated lazily, store it
// in the "lazy" property.
if (is_callable($value)) {
Copy link

Choose a reason for hiding this comment

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

I see issue here:

php -r "var_dump(is_callable('array_map'));"
// bool(true)

Besides it is not possible to have closure as an option.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The callable/closure set to the option is not evaluated if its first parameter is not type hinted with Options. Look inside the if statement.

Copy link

Choose a reason for hiding this comment

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

Oops, I missed that. Sorry for noise.

@webmozart
Copy link
Contributor Author

@Tobion
Copy link
Contributor

Tobion commented Oct 14, 2014

With the new methods introduced, it would probably be a good time to fix #10585. So its not wrong again.

@Tobion
Copy link
Contributor

Tobion commented Oct 14, 2014

Also what I always find irritating is the magic of defining lazy options.
There is a huge difference between

$options->set('name', function (Options $options, $previousValue) {
});

and

$options->set('name', function ($options, $previousValue) {
});

which is non-obvious. Maybe there should be different methods for defining plain options and lazy avaluated options?

@Tobion
Copy link
Contributor

Tobion commented Oct 14, 2014

Also I think Options:resolve should not clone. The OptionsResolver should only clone the options when calling resolve. The semantics are different. It makes sense that one can use one resolver to call resolve multiple times. But for options it makes things so conplicated and confusing. E.g. it's not possible to call the following which is strange from a users perspective. Or am I missing something?

$options = new Options();
$options['foo'] = 'bar';
$options->resolve(array());
echo $options['foo'];

@webmozart
Copy link
Contributor Author

@Tobion The "problem" about using a separate method for lazy options is that it then isn't obvious anymore how set() and - let's call it - setLazy() interact:

$options['foo'] = 'bar';
$options->setLazy('foo', function (Options $options) {
    return 'dynamic';
});
$options['foo'] = 'baz';

I personally don't find it very clear what the value of the 'foo' option is at the different points in time. With:

$options['foo'] = 'bar';
$options['foo'] = function (Options $options) {
    return 'dynamic';
};
$options['foo'] = 'baz';

it's clear that we are always performing the same set operation. Obviously I'm biased here.

About resolve(): The point of that method is not to change the state of the object, but to resolve the passed options:

$options = new Options();
// ...
$resolvedOptions = $options->resolve($passedOptions);
echo $resolvedOptions['foo'];

From the user POV, it seems confusing to me if resolve() on the one hand turns the input into some output, and on the other hand modifies the internal state of the instance, making it unusable for further resolve() calls.

I'll look into fixing #10585 tomorrow, thanks for mentioning that.

@Tobion
Copy link
Contributor

Tobion commented Oct 21, 2014

Why should it be a bc break if you do Options extends OptionsInterface and just deprecate Options?

@Tobion
Copy link
Contributor

Tobion commented Oct 21, 2014

They can be called (BC), but it's true that they are not part of the interface.

It basically forbids these use-cases. But I'm not sure if there are use-cases for options that depend on the definition.

@webmozart
Copy link
Contributor Author

Why deprecate OptionsResolverInterface?

OptionsResolver is a low-level utility which is instantiated in the same class that it is used in most of the time. Interfaces, on the other hand, are contracts helping different classes/components to communicate. What's the point in having an interface for a class which is not supposed to be shared?

It basically forbids these use-cases.

That's good, because these methods are deprecated. The methods can be called, but they shouldn't.

Why should it be a bc break if you do Options extends OptionsInterface and just deprecate Options?

That's a possibility. However this also makes the definition of lazy options unnecessarily longer (and that definition is the sole purpose of the interface's existence):

$resolver->setDefault('choices', function (OptionsInterface $options) {
});

I'd rather make Options an empty class if you insist on the "Interface" suffix.

@webmozart webmozart force-pushed the issue4500 branch 2 times, most recently from ec3e26f to f1db155 Compare October 21, 2014 09:59
*/
public function setOptional(array $optionNames)
{
$this->setDefined($optionNames);
Copy link
Contributor

Choose a reason for hiding this comment

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

return $this->setDefined($optionNames);?

@webmozart webmozart force-pushed the issue4500 branch 2 times, most recently from 686a170 to fb9e7e0 Compare October 21, 2014 12:04
@webmozart
Copy link
Contributor Author

The PR description, documentation, CHANGELOG and UPGRADE-2.6 files are up to date now.

@webmozart webmozart changed the title [OptionsResolver] Validate options before normalization [OptionsResolver] Merged Options class into OptionsResolver Oct 21, 2014
$options = $resolver->resolve($options);
```

Before:
Copy link
Contributor

Choose a reason for hiding this comment

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

after

@Tobion
Copy link
Contributor

Tobion commented Oct 21, 2014

Please just merge it before it gets reworked again ;)

@webmozart
Copy link
Contributor Author

@Tobion @fabpot I'd like to have your +1 first

@Tobion
Copy link
Contributor

Tobion commented Oct 21, 2014

Apart from this #12156 (comment) typo 👍

@fabpot
Copy link
Member

fabpot commented Oct 21, 2014

👍

@webmozart webmozart merged commit 642c119 into symfony:master Oct 22, 2014
webmozart added a commit that referenced this pull request Oct 22, 2014
…r (webmozart)

This PR was merged into the 2.6-dev branch.

Discussion
----------

[OptionsResolver] Merged Options class into OptionsResolver

| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | yes
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | #4500, #9174, #10585, #10202, #11020, makes #7979+#10616 obsolete
| License       | MIT
| Doc PR        | symfony/symfony-docs#4159

#11716 was reverted as preparation of this PR (453882c).

The Options class was merged into OptionsResolver. This made it possible to fix several tickets, as indicated above.

**Usage**

See [the updated documentation](https://github.com/webmozart/symfony-docs/blob/issue11705/components/options_resolver.rst).

**Bug Fixes**

Previously, the options weren't validated before the normalizers were called. As a result, the normalizers had to perform validation again:

```php
$resolver->setAllowedTypes(array(
    'choices' => 'array',
));
$resolver->setNormalizers(array(
    'choices' => function (Options $options, $value) {
         array_merge($options['choices'], ...);
    },
));

// fatal error
$resolver->resolve(array('choices' => 'foobar'));
```

This is fixed now.

**BC Breaks**

The `array` type hint was removed from `setRequired()`, `setAllowedValues()`, `addAllowedValues()`, `setAllowedTypes()` and `addAllowedTypes()`. If anybody implemented `OptionsResolverInterface`, they must adapt their code.

The Options class was turned into an interface extending ArrayAccess and Countable. Anybody instantiating Options directly should instantiate OptionsResolver instead. Anybody using any of the methods available in Options (`get()`, `has()`) should use the ArrayAccess interface instead.

Normalizers are not called anymore for undefined options (#9174). People need to set a default value if they want a normalizer to be executed independent of the options passed to `resolve()`.

**Deprecations**

OptionsResolverInterface was deprecated and will be removed in 3.0. OptionsResolver instances are not supposed to be shared between classes, hence an interface does not make sense.

Several other methods were deprecated. See the CHANGELOG and UPGRADE-2.6 files for information.

**Todo**

- [x] Fix PHPDoc
- [x] Adapt CHANGELOG/UPGRADE
- [x] Adapt documentation
- [x] Deprecate OptionsResolver[Interface]

Commits
-------

642c119 [OptionsResolver] Merged Options class into OptionsResolver
@linaori
Copy link
Contributor

linaori commented Oct 22, 2014

Just a small detail but, what will happen with FormTypeInterface and AbstractType in 2.6? Will they stay like this? public function setDefaultOptions(OptionsResolverInterface $resolver);

Based on the PR I noticed that it should be 100% BC for forms, but I think this should be well documented as BC break for 3.0.

@webmozart webmozart deleted the issue4500 branch October 22, 2014 17:21
@webmozart
Copy link
Contributor Author

Will they stay like this?

Yes.

weaverryan added a commit to symfony/symfony-docs that referenced this pull request Oct 25, 2014
…umentation to describe the 2.6 API (webmozart, peterrehm)

This PR was merged into the master branch.

Discussion
----------

[WCM][OptionsResolver] Adjusted the OptionsResolver documentation to describe the 2.6 API

| Q             | A
| ------------- | ---
| Doc fix?      | no
| New docs?     | yes (symfony/symfony#11716, symfony/symfony#12156)
| Applies to    | 2.6+
| Fixed tickets | -

Commits
-------

575c320 Updated OptionsResolver documentation: removed static methods
fab825d Merge pull request #1 from peterrehm/patch-9
f202fb8 Removed duplicate use statements
ae66893 Added missing mailer class and use statements
43ae1d8 [OptionsResolver] Adjusted the OptionsResolver documentation to describe the 2.6 API
michaelperrin pushed a commit to michaelperrin/symfony-docs that referenced this pull request Dec 1, 2014
`OptionsResolverInterface` has been deprecated in Symfony 2.6 in favor of `OptionsResolver` (see symfony/symfony#12156)
@stof
Copy link
Member

stof commented Dec 4, 2014

Why deprecate OptionsResolverInterface?

OptionsResolver is a low-level utility which is instantiated in the same class that it is used in most of the time. Interfaces, on the other hand, are contracts helping different classes/components to communicate. What's the point in having an interface for a class which is not supposed to be shared?

@webmozart Form types are a good example where you pass the option resolver around to other classes, to configure it. So maybe we should not deprecate the interface ?
On the other hand, it is true that providing a different implementation probably does not make any sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants