-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[WIP] [OptionsResolver] handle nested options #18134
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
[WIP] [OptionsResolver] handle nested options #18134
Conversation
ad7cce8
to
b486c62
Compare
92e1f61
to
49588c6
Compare
@@ -488,6 +600,12 @@ public function addAllowedValues($option, $allowedValues) | |||
throw new AccessException('Allowed values cannot be added from a lazy option or normalizer.'); | |||
} | |||
|
|||
if ($this->isNested($option)) { | |||
@trigger_error(sprintf('The "%s" method should not be used with nested options. A failed attempt occurred with the option "%" for values %s', __METHOD__, $option, $this->formatValues($allowedValues)), E_USER_WARNING); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this doesn't seem to be a deprecation notice, so exceptions should be used, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That what I proposed in my comment in the concerned commit.
I'm not sure how strict we should be here, this is just a proposition.
dd55fe5
to
211a4d9
Compare
closes symfony#4833. Allow to resolve nested options. * add `OptionsResolver::nested` * add `OptionsResolver::setNested()` * add `OptionsResolver::isNested()` * add `OptionsResolver::getNested()`
211a4d9
to
e1aa1b6
Compare
closes symfony#15524. Set allowed types for many options or for a set of nested or extra options. * add `Options::NONE` * add `Options::ALL` * add `Options::DEFINED` * add `Options::NESTED` * add `Options::EXTRA` * add `OptionsResolver::allowedValuesForAll` * add `OptionsResolver::allowedTypesForAll` * add `OptionsResolver::resolveUndefined` * add `OptionsResolver::addAllowedValuesForAll()` * add `OptionsResolver::addAllowedTypesForAll()` * add `OptionsResolver::setPrototype()`
e1aa1b6
to
ccd5ce1
Compare
Pushed b51cce4 to pass the parent options to each nested option normalizer. |
e8f1172
to
b51cce4
Compare
Just an alternative approach for this feature, without caring about maintaining a tree:
|
Just some more thoughts: Your approach of maintaining a tree makes the |
@backbone87 thanks for the feedback.
It's not a bad idea. But for now I'd rather try to handle nested or extra options without adding too much overhead to the current behavior.
I don't agree with you on that, it just makes it more powerful IMHO, ref #3968. I've worked on that PR but haven't push any more updates for now, I'm waiting for more reviews on the current state. ping @stof @Tobion @webmozart |
Well that discussion was 4 years ago and the OptionsResolver grew quite a bit since then and the requested features push it more and more towards the feature set of the Config component |
But they still do not share goal or responsibility: one for loading resources and defining merging rules for domain configuration and the other for resolving class options even through inheritance. |
"option resolution through inheritance" is a specific "defined merging rule" The main benefit of the The point of view is cleary personal preference, so like you already said: lets hear some other opinions on this topic. |
I would just say I see a nuance between "domain configuration" and "component or classes configuration" which may not require external resource loading but internal definition. |
Thank you for working on this @HeahDude! I'm a bit uncomfortable about adding this functionality. First of all, nested options - so far - were not asked for by many people. Second, adding support for nested options will have a performance impact. However - just like PropertyAccess - OptionsResolver is a very low-level but frequently called component that impacts the performance of forms. We should avoid decreasing its performance for no benefit (for the majority of users). |
@webmozart thanks for the review. It was my self challenge for the hack day. It looks more like a PoC/RFC as is, and would require a nice lifting and lot of test to be safely merged right now. I actually thought about the light weight aspect, but the idea is also to help some core form types. Since it remains a specific new method call, it should not impact performance that much while being flexible for specific use cases, Also it may be more safe to allow "locked" options as tried in #17161 since some options should never been overridden in some form type extensions or children in some core form types. I guess we have the time to polish it for later and make a decision when it's properly implemented. |
Status: Needs Work |
We have an use case where we need this functionality. |
@HeahDude Any news on this one? Still wanting to work on it? |
Yes I think it's needed to support this case IMHO even in the core. I will start it again from scratch to get something simpler and be careful to not add too much overhead in the meantime if someone wants to give it a try, please do. Closing for now. |
…finition (yceruto) This PR was merged into the 4.2-dev branch. Discussion ---------- [OptionsResolver] Added support for nesting options definition | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #4833 | License | MIT | Doc PR | symfony/symfony-docs#9995 I'd like to propose an alternative to #27251 and #18134 with a different approach. It would allow you to create a nested options system with required options, validation (type, value), normalization and more. <details> <summary><strong>Short documentation</strong></summary> **To define a nested option, you can pass a closure as the default value of the option with an `OptionsResolver` argument:** ```php $resolver ->defaults([ 'connection' => 'default', 'database' => function (OptionsResolver $resolver) { $resolver ->setRequired(['dbname', 'host', ...]) ->setDefaults([ 'driver' => 'pdo_sqlite', 'port' => function (Options $options) { return 'pdo_mysql' === $options['driver'] ? 3306 : null, }, 'logging' => true, ]) ->setAllowedValues('driver', ['pdo_sqlite', 'pdo_mysql']) ->setAllowedTypes('port', 'int') ->setAllowedTypes('logging', 'bool') // ... }, ]); $resolver->resolve(array( 'database' => array( 'dbname' => 'demo', 'host' => 'localhost', 'driver' => 'pdo_mysql', ), )); // returns: array( // 'connection' => 'default', // 'database' => array( // 'dbname' => 'demo', // 'host' => 'localhost', // 'driver' => 'pdo_mysql', // 'port' => 3306, // 'logging' => true, // ), //) ``` Based on this instance, you can define the options under ``database`` and its desired default value. **If the default value of a child option depend on another option defined in parent level, adds a second ``Options`` argument to the closure:** ```php $resolver ->defaults([ 'profiling' => false, 'database' => function (OptionsResolver $resolver, Options $parent) { $resolver ->setDefault('logging', $parent['profiling']) ->setAllowedTypes('logging', 'bool'); }, ]) ; ``` **Access to nested options from lazy or normalize functions in parent level:** ```php $resolver ->defaults([ 'version' => function (Options $options) { return $options['database']['server_version']; }, 'database' => function (OptionsResolver $resolver) { $resolver ->setDefault('server_version', 3.15) ->setAllowedTypes('server_version', 'numeric') // ... }, ]) ; ``` As condition, for nested options you must to pass an array of values to resolve it on runtime, otherwise an exception will be thrown: ```php $resolver->resolve(); // OK $resolver->resolve(['database' => []]); // OK $resolver->resolve(['database' => null); // KO (Exception!) ``` </details> --- Demo app https://github.com/yceruto/nested-optionsresolver-demo Commits ------- d04e40b Added support for nested options definition
Summary
c2944ac fixes [OptionsResolver] Support nested options #4833. Handle nested options resolving
OptionsResolver::nested
a private array of nestedOptionsResolver
instances by root names,OptionsResolver::setNested($option, array $defaults)
defines an option as a new self and returns the nestedOptionsResolver
instance,OptionsResolver::isNested($option)
returns whether an option name is a root name for a nestedOptionsResolver
instance,OptionsResolver::getNested()
an array of root names representing nestedOptionsResolver
instances.*Allowed*
methods are not supported with root name of nested options since they always have to be an array, so:f2ad947 trigger warning ? throw
BadMethodCallException
? what do you think ?9e4e02a fixes [OptionsResolver] array of instance allowed type #15524. Handle extra options resolving and
addAllowed*ForAll
rules with constant flagsOptions::NONE
constant,Options::ALL
constant, matches defined and extra options,Options::DEFINED
constant,Options::NESTED
constant,Options::EXTRA
constant, matches any undefined options, helps dealing with "anonymous" options (using a numerical index)OptionsResolver::allowedValuesForAll
a private array to validate all options even undefined but not root of nested options as they must always be an array,OptionsResolver::allowedTypesForAll
a private array to validate all options (as previous),OptionsResolver::resolveUndefined
a privatebool
to allow extra options resolving,OptionsResolver::addAllowedValuesForAll($options, $allowedValues, $replace = false, $nested = Options::ALL)
, first argument may be an array of options names or any flag constant exceptOptions::NONE
, if a root name of nested options is passed as/among first argument the method will be recursive because the fourth argument default isOptions::ALL
, prevent it by passingOptions::NONE
instead,OptionsResolver::addAllowedTypesForAll($options, $allowedTypes, $replace, $nested)
, same as previous, note that passingOptions::ALL
as first argument will not apply to root of nested options as they must always be arrays and on contrary will apply on extra options, passtrue
as third argument to override previous allowed types,OptionsResolver::setPrototypes((string|string[]) $types, (mixed) $values, $options = Options::EXTRA)
, setsresolveUndefined
totrue
and validate any extra options with the any given type with its corresponding allowed values,OptionsResolver::allowExtraOptions($allow = true)
The difference is that in the second case implicitly the flag
Options::EXTRA
is passed so previous defaults will not be validate by prototypes rules, if that's not what you want passOptions::ALL
as third argument.Note that
setNested()
returns the nestedOptionsResolver
instance in this examples, you could call these methods on the$resolver
as well but this not the same:ccd5ce1 Harden dependency of nested options with an internal class
NestedOptions
class which extendsOptionsResolver
,NestedOptions::root
the parentOptionsResolver
instance,NestedOptions::rootName
the root option name,NestedOptions::resolve()
overrides to throw an exception is parentOptionsResolver
instance is not locked,OptionsResolver::isLocked()
internal public method to check fromNestedOptions
if it can be resolved.Work in Progress (RFC)
OptionsResolver::setExtraNormalizer(function (Options $options, $extraName, $extraValue) { ... })
to normalize any undefined options, basically a shortcut for the currently supportedsetNormalizer(Options::Extra, callable)
but with handling of$extraName
OptionsResolver::prototypes
private array holding allowed values by allowed types to validate extra options.