-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[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 our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
@@ -42,13 +42,13 @@ public function __construct() | |||
$this->beforeOptimizationPasses = array( | |||
100 => array( | |||
$resolveClassPass = new ResolveClassPass(), | |||
new ResolveDefinitionInheritancePass(), |
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 be after ResolveDefinitionTemplatesPass
on purpose: this allows the parent-child definitions to be resolved first. Then, the ResolveDefinitionInheritancePass
doesn't need to worry about this.
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 | ||
*/ | ||
public static function mergeDefinition(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
to
33d341c
Compare
@@ -27,7 +27,7 @@ protected function processValue($value, $isRoot = false) | |||
return parent::processValue($value, $isRoot); | |||
} | |||
|
|||
$class = $value instanceof 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 | ||
*/ | ||
public function getConfiguredParts() |
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?)
this is already doable with a minor change in the existing code base, isn't it? |
@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. |
the order needs to be |
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 less explicit 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 less specific 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; | ||
} | ||
|
||
private function trackChange($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->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
@@ -494,7 +502,13 @@ private function parseDefinition($id, $service, $file, array $defaults) | |||
throw new InvalidArgumentException(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, call setTrackChanges(!$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 to strtolower
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 ChildDefinition tl;dr Before, instanceof values would replace those explicitly set on services. Now, it acts more like a default: only setting a value on a service if that value wasn't explicitly set on that service (i.e. don't override). For things like tags and calls, they're merged intelligently and on a case-by-case basis (merging tags works different than merging calls)
… bug The 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 careful when setting attributes so that we don't trigger change tracking when there really were not any changes.
8c76a7f
to
5e39c5d
Compare
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->setClass($this->bag->resolveValue($value->getClass())); | ||
$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")
One more update:
|
The logic is that compiler passes are the only ones that truly have enough information to 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 | ||
*/ | ||
public function setTrackChanges($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: true
parent: ~
services:
Foo\Bar:
parent: parent
Then Foo\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).
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 definitions ab86457 [DI] Rework config hierarchy: defaults > instanceof > service config cbaee55 [DI] Track changes at the "Definition" level
This reworks how
defaults
,instanceof
and service config override each other. For example:(taken from
_instanceof
(service overrides
_instanceof
)security.voter
tagssecurity.voter
tags_defaults
> service config >_instanceof
_defaults
>_instanceof
> service configtl;dr;
_instanceof
now 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:
_instanceof
has its own override/merge logic. Mostly, it's easy - scalar values override. Some are more opinionated:tags
from_instanceof
and 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 neededcalls
from_instanceof
and 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)._instanceof
calls are called firstarguments
,factory
,deprecated
andabstract
from_instanceof
, and I think we could remove more. These feel like an abuse of_instanceof
when a parent class is more proper (and the behavior of how parent-child config is merged is more predictable in general)instanceof
is applied. This means the real hierarchy is:And yep, there's a test for this in
IntegrationTest
:)_instanceof
from the file where the child was configured is what we want)Cheers!