Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork9.7k
[DI] Rework config hierarchy: defaults > instanceof > service config#22294
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Conversation
| $this->beforeOptimizationPasses =array( | ||
| 100 =>array( | ||
| $resolveClassPass =newResolveClassPass(), | ||
| newResolveDefinitionInheritancePass(), |
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.
I moved this down to beafterResolveDefinitionTemplatesPass on purpose: this allows the parent-child definitions to be resolved first. Then, theResolveDefinitionInheritancePass doesn't need to worry about this.
nicolas-grekasApr 7, 2017 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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.
Thinking about that, this should be kept here: all following compiler passes need to always win. Moving the pass after all DI extension passes is what makes change-tracking a nightmare.
unless I missed something, the full target order proposed in this PR should be:
defaults > instanceof > parents > service > passes
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.
(vs defaults > parents > service > instanceof > passes as implemented today)
| * @internal | ||
| */ | ||
| publicstaticfunctionmergeDefinition(Definition$def,ChildDefinition$definition) | ||
| { |
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 class was simply changed to look like it did before#21530 (adjusted for changes since then)
5279a96 to33d341cCompare| } | ||
| $class =$valueinstanceof ChildDefinition ?$this->resolveDefinition($value) :$value->getClass(); | ||
| $class =$value->getClass(); |
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 entire class/file could/should be renamed (ResolveInstanceofPass) - I didn't do it to keep the diff more manageable
| * | ||
| * @return array An array of configured parts for this Definition | ||
| */ | ||
| publicfunctiongetConfiguredParts() |
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.
should this replace getChanges on ChildDefinition? (or have getChanges migrate here instead?)
nicolas-grekas commentedApr 5, 2017 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
this is already doable with a minor change in the existing code base, isn't it? |
stof commentedApr 5, 2017
@weaverryan We still must have instanceof winning over defaults. Otherwise, it removes the possibility to say "I want all services in this file to be private, except for commands because they are required to be public". Service-level config should win over instanceof, but not service defaults. |
stof commentedApr 5, 2017
the order needs to be |
nicolas-grekas left a comment• edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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 PR implements another merging logic than the current one. If we agree that this is the most intuitive behavior, then I'm fine with it :)
For the reader, the current behavior (pre this PR) is defaults -> service -> instanceof,
my reasoning doing so was from lessexplicit to most explicit:
- less explicit: I don't know what I'm wiring => defaults
- a bit more explicit: I know which class I'm wiring => service
- most explicit: I know the type hierarchy of my class => instanceof
This matches the ordering of the configuration loading (since the code discovers things in this order also - this PR can't change that).
The logic in this PR is from lessspecific to most specific:
- less specific: all services in that file => defaults
- a bit more specific: some services in that file => instanceof
- most specific:this service in that file => service
| $properties =$def->getProperties(); | ||
| foreach ($instanceofDefinition->getProperties()as$k =>$v) { | ||
| // don't override properties set explicitly on the service | ||
| if (!isset($properties[$k])) { |
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.
should use array_key_exists, or the+ operator (would replace the foreach)
| } | ||
| // append method calls | ||
| if ($calls =$instanceofDefinition->getMethodCalls()) { | ||
| $def->setMethodCalls(array_merge($def->getMethodCalls(),$calls)); |
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.
same-name existing calls should not be added IMHO
| return$this; | ||
| } | ||
| privatefunctiontrackChange($type) |
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.
could be called "touch" (first read did tell me what it did - ie could have been the setter also, with boolean arg)
| $definition->setTrackChanges(false); | ||
| } | ||
| $definition->setAutowired($autowire); | ||
| if ($autowireDefaultsUsed) { |
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.
"if" not needed
| thrownewInvalidArgumentException(sprintf('Invalid autowire attribute: "by_type", "by_id", true or false expected, "%s" given in "%s".',is_string($autowire) ?$autowire :gettype($autowire),$file)); | ||
| } | ||
| if ($autowireDefaultsUsed) { |
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.
"if" not needed, callsetTrackChanges(!$autowireDefaultsUsed)
| } | ||
| // merge method calls | ||
| if ($instanceofCalls =$instanceofDefinition->getMethodCalls()) { | ||
| $currentCallMethods =array_map(function($call) { |
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.
array_column to the rescue
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.
Ah, but now I also need tostrtolower for the case insensitive matching later :)
| $uniqueInstanceofCalls =array_filter($instanceofCalls,function($instanceofCall)use ($currentCallMethods) { | ||
| // don't add an instanceof call if it was overridden on the service | ||
| return !in_array($instanceofCall[0],$currentCallMethods); |
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.
should be case insensitive
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.
+1! Got it + test update
…ke a ChildDefinitiontl;dr Before, instanceof values would replace those explicitly set on services. Now, itacts more like a default: only setting a value on a service if that value wasn't explicitlyset on that service (i.e. don't override). For things like tags and calls, they're mergedintelligently and on a case-by-case basis (merging tags works different than merging calls)
… bugThe bug is where _defaults was overriding _instanceof config. To handle that,we ignore change tracking when setting from _defaults
…after resolving children
This uncovered a bug, where in ResolveDefinitionTemplatePass, we need to be more carefulwhen setting attributes so that we don't trigger change tracking when there really werenot any changes.
8c76a7f to5e39c5dCompareweaverryan commentedApr 6, 2017
This is ready to go once again. I updated the description quite a bit to be as clear as possible. Other than general code review, the question simply comes down to this: how do we want the override logic to work with Cheers! |
…e" positives to the Definition object
| $value->setFile($this->bag->resolveValue($value->getFile())); | ||
| $value->setProperties($this->bag->resolveValue($value->getProperties())); | ||
| $value->setMethodCalls($this->bag->resolveValue($value->getMethodCalls())); | ||
| $value->setTrackChanges(true); |
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 is a side-effect of the change tracking... changes. This wasn't actually needed to get the new behavior working correctly, it was just needed to get some tests to pass (i.e. the tests were compiling the container and then comparing Definition objects together... one of the Definition objects now had a few extra "changes")
weaverryan commentedApr 6, 2017 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
One more update:
|
The logic is that compiler passes are the only ones that truly have enough informationto know if one tag should replace another, or if both are needed.
| } | ||
| $definition->setAutowired($autowire) | ||
| ->setTrackChanges(true) | ||
| ; |
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.
What about updating this to:
if (isset($service['autowire'])) {$definition->setAutowired($service['autowire']); }elseif (isset($defaults['autowire'])) {$definition ->setTrackChanges(false) ->setAutowired($defaults['autowire']) ->setTrackChanges(true); }
| * | ||
| * @return $this | ||
| */ | ||
| publicfunctionsetTrackChanges($trackChanges) |
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.
What about marking this as@internal ? I don't think we should bother about untracked changes due to a choice of the user.
Maybe we could even use a bound callable where necessary.
| foreach ($definition->getAutowiringTypes(false)as$autowiringType) { | ||
| $def->addAutowiringType($autowiringType); | ||
| $parentChanges =$parentDef->getChanges(); | ||
| if (isset($parentChanges['factory'])) { |
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.
I see one fragility here.
What if the parent uses defaults?
services:_defaults:autowire:trueparent:~
services:Foo\Bar:parent:parent
ThenFoo\Bar won't be autowired, right?
I think the defaults should always be tracked but we should allow to differentiate changes from defaults (maybe just internally, I see no need to do this in userland):
if (isset($defaults['autowire'])) {$definition->setTrackChanges('defaults') ->setAutowired($defaults['autowire']) ->setTrackChanges(true);}
and then use something like the following in the passes needing it:
$definition->getChanges($withoutDefaults =true);
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.
We could also fix#22345 using this by tracking the detected class name differently (or maybe even by not tracking it).
weaverryan commentedApr 11, 2017
Closing in favor of#22356 |
…service config (weaverryan, nicolas-grekas)This PR was merged into the 3.3-dev branch.Discussion----------[DI] Rework config hierarchy: defaults > instanceof > service config| Q | A| ------------- | ---| Branch? | 3.3| Bug fix? | yes| New feature? | no| BC breaks? | no| Deprecations? | no| Tests pass? | yes| Fixed tickets | -| License | MIT| Doc PR | -Replaces#22294.The main change is that ChildDefinition does not have "instanceof" applied anymore. All the complexity of the pass came from the merging logic required to deal with them. But in fact, we'd better not have any such logic and just not apply "instanceof" to ChildDefinition (but have them inherit from their parents with the usual logic).Commits-------6d6116b Adding an integration test for the hirarchy of defaults, instanceof, child, parent definitionsab86457 [DI] Rework config hierarchy: defaults > instanceof > service configcbaee55 [DI] Track changes at the "Definition" level
Uh oh!
There was an error while loading.Please reload this page.
This reworks how
defaults,instanceofand service config override each other. For example:(taken from
_instanceof(service overrides
_instanceof)security.votertagssecurity.votertags_defaults> service config >_instanceof_defaults>_instanceof> service configtl;dr;
_instanceofnow acts like_defaults, but for specific instances. As a user, this is how I expected it to work originally.NOTE Much of the config merging behavior in this PR - i.e.
tags,calls- could be accomplished without such a big diff. But any scalar config - i.e.public,autowire,configurator, etc -requires all of the changes in this PR.Important Notes:
_instanceofhas its own override/merge logic. Mostly, it's easy - scalar values override. Some are more opinionated:tagsfrom_instanceofand service are merged - all tags are kept. It's up to the compiler pass to decide of 2 tags should be merged into 1 or if both are neededcallsfrom_instanceofand service are merged, unless the method name of the call match, then only the one from the service wins (i.e. ability to override a call)._instanceofcalls are calledfirstarguments,factory,deprecatedandabstractfrom_instanceof, and I think we could remove more. These feel like an abuse of_instanceofwhen a parent class is more proper (and the behavior of how parent-child config is merged is more predictable in general)instanceofis applied. This means the real hierarchy is:And yep, there's a test for this in
IntegrationTest:)_instanceoffrom the file where the child was configured is what we want)Cheers!