-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[DependencyInjection] Support local binding #22187
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
d159e0d
to
f7539be
Compare
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.
The target looks good to me, named args should be handled to make the feature complete to me.
Here are some friendly comments to help move forward.
*/ | ||
protected function processValue($value, $isRoot = false) | ||
{ | ||
if (!$value instanceof Definition || empty($value->getBindings())) { |
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.
> empty($value->getBindings())
!$bindings = $value->getBindings()
$class = $parameterBag->resolveValue($class); | ||
} | ||
|
||
$bindings = $value->getBindings(); |
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.
to be removed, see above
$parameterBag = $this->container->getParameterBag(); | ||
|
||
if ($class = $value->getClass()) { | ||
$class = $parameterBag->resolveValue($class); |
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 removed (also from ResolveNamedArgumentsPass): params have already been resolved at this stage.
|
||
foreach ($calls as $i => $call) { | ||
list($method, $arguments) = $call; | ||
$method = $parameterBag->resolveValue($method); |
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 comment (and same cleanup to do in ResolveNamedArgumentsPass)
ksort($arguments); | ||
$calls[$i][1] = $arguments; | ||
} | ||
} |
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.
$bindings should be allowed to contain $named args, so that it can be used with scalar/array params also.
$bindings could also be validated: what about throwing when a key doesn't match any existing class/interface? That would help people spot typos easily.
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.
$bindings should be allowed to contain $named args, so that it can be used with scalar/array params also.
Yes I have to do that :)
$bindings could also be validated: what about throwing when a key doesn't match any existing class/interface? That would help people spot typos easily.
What if the class/interface doesn't exist/is not loaded yet? Is this an unsupported case?
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 if the class/interface doesn't exist
unsupported yes, DX is way more important than supporting funky edge cases
|
||
foreach ($defaults['bindings'] as $binding) { | ||
if (!$binding->hasAttribute('id')) { | ||
throw new InvalidArgumentException(sprintf('The binding service id for tag "<defaults>" in %s must be defined.', $file)); |
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 allow $named args
@@ -333,6 +341,22 @@ private function parseDefinition(\DOMElement $service, $file, array $defaults = | |||
$definition->addAutowiringType($type->textContent); | |||
} | |||
|
|||
$bindingNodes = $this->getChildren($service, 'binding'); | |||
if (empty($bindingNodes) && !empty($defaults['bindings'])) { |
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.
~empty()~~
$bindings = array(); | ||
foreach ($bindingNodes as $binding) { | ||
if (!$binding->hasAttribute('id')) { | ||
throw new InvalidArgumentException(sprintf('The "id" attribute for binding type "%s" for service "%s" in %s must be defined.', $type, (string) $service->getAttribute('id'), $file)); |
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.
$named args
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.
which means we need a type="service/collection"
attribute on xml
throw new InvalidArgumentException(sprintf('The "id" attribute for binding type "%s" for service "%s" in %s must be defined.', $type, (string) $service->getAttribute('id'), $file)); | ||
} | ||
|
||
$bindings[$binding->getAttribute('type')] = new Reference($binding->getAttribute('id')); |
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.
type should renamed to eg "key" (to allow "type" to be used as suggested previously)
|
||
$definition->setBindings($bindings); | ||
} elseif (isset($defaults['bindings'])) { | ||
$definition->setBindings($defaults['bindings']); |
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'd expect default bindings to be merged with service ones, don't you?
ec233a9
to
a4706d9
Compare
@nicolas-grekas thanks for you comments, they should all be fixed now 😃 |
} | ||
|
||
foreach ($bindings as $key => $binding) { | ||
if ($key && '$' === $key[0]) { |
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.
$key can be a number at this stage (as bogus as it would be) - then $key[0]
would break
!isset($key[0]) || '$' === $key[0]
instead?
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.
A number should not be supported so I'd say isset($key[0]) && '$' === $key[0]
, thanks!
continue; | ||
} | ||
|
||
if (null !== $binding && !$binding instanceof Reference) { |
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 allowing Definition
also?
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.
👍
throw new InvalidArgumentException(sprintf('Invalid value for binding type "%s" for service "%s": expected null, an instance of %s or an instance of %s, %s given.', $key, (string) $this->currentId, Reference::class, Definition::class, gettype($binding))); | ||
} | ||
|
||
if (!class_exists($key) && !interface_exists($key)) { |
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.
missing false
as 2nd arg of interface_exists
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.
but thinking more about this, you told me you would prefer throwing when a binding is never used.
we can do that: we just need to move the deep clone that is done in FileLoader to ResolveDefinitionInheritancePass!
I'd prefer this over this class_exists check myself (because it makes things context-sensitive)
} | ||
|
||
if (null !== $binding && !$binding instanceof Reference && !$binding instanceof Definition) { | ||
throw new InvalidArgumentException(sprintf('Invalid value for binding type "%s" for service "%s": expected null, an instance of %s or an instance of %s, %s given.', $key, (string) $this->currentId, Reference::class, Definition::class, gettype($binding))); |
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.
for binding "key" (reusing vocabulary from XmlLoader)
} | ||
|
||
if (!class_exists($key) && !interface_exists($key)) { | ||
throw new InvalidArgumentException(sprintf('Invalid binding type "%s" for service "%s": this class/interface does not exist.', $key, $this->currentId)); |
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.
s/type/key
this class or interface
} | ||
|
||
foreach ($reflectionMethod->getParameters() as $key => $parameter) { | ||
if (isset($arguments[$key])) { |
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_key_exists
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.
in fact, AutowirePass has a trick here, which we should account for: null is not autowired, but an empty string with a non-internal type-hint triggers autowiring - we should do the binding here also in this case
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.
updated
@@ -50,21 +44,39 @@ protected function processValue($value, $isRoot = false) | |||
$resolvedArguments[] = $argument; | |||
continue; | |||
} | |||
if ('' === $key || '$' !== $key[0]) { | |||
|
|||
if ('' === $key || ('$' !== $key[0] && !class_exists($key) && !interface_exists($key))) { |
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.
here we don't need the class_exists check: we should just throw when a binding is not used (as done for $named args already), this will make things context-free again
$resolvedArguments[$j] = $argument; | ||
|
||
continue 2; | ||
} | ||
} | ||
|
||
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s::%s" has no argument named "%s". Check your service definition.', $this->currentId, $class, $method, $key)); | ||
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s::%s" has no argument type hinted as "%s". Check your service definition.', $this->currentId, $value->getClass(), $method, $key)); |
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.
type-hinted (with a dash)
} | ||
|
||
$calls = $value->getMethodCalls(); | ||
$calls[] = array(null, $value->getArguments()); // constructor |
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.
instead of null
: $class->hasConstructor() ? $class->getConstructor()->name : '__construct'
which implies moving the getReflectionClass
call outside of the getReflectionMethod
call, which is nice because it is currently in a loop for no reason.
Same on ResolveNamedArgsPass of course.
+1, how much is left to do? can't await it! |
For me it's ready. |
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.
services: Foo: arguments: BarInterface: '@bar'
Not sure if BarInterface
has a special meaning here? given only integer or $named arguments are allowed.
edit: ah i see the change in ResolveNamedArguments
now 👍
continue; | ||
} | ||
|
||
if (null !== $binding && !$binding instanceof Reference && !$binding instanceof 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.
shouldnt this check go first?
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.
No, any value is allowed for named arguments.
Though i have to add it in the other pass.
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 is true 👍
$calls = $value->getMethodCalls(); | ||
|
||
$constructor = $reflectionClass->getConstructor(); | ||
$calls[] = array(null !== $constructor ? $constructor->name : '__construct', $value->getArguments()); // constructor |
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.
comment not needed imo. this is obvious.
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.
here are some comments for another iteration.
can you look at descriptors also please?
} | ||
|
||
if (!class_exists($key) && !interface_exists($key, false)) { | ||
throw new InvalidArgumentException(sprintf('Invalid binding key "%s" for service "%s": this class/interface does not exist.', $key, $this->currentId)); |
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.
class or interface (instead of the slash)
|
||
$class = $value->getClass(); | ||
if (!$class) { | ||
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": the class is not set.', $this->currentId)); |
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.
no need for this block, autowire pass does:
if (!$reflectionClass = $this->container->getReflectionClass($definition->getClass(), true)) {
which is enough
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": the class is not set.', $this->currentId)); | ||
} | ||
|
||
if (!$reflectionClass = $this->container->getReflectionClass($class)) { |
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.
see previous comment
} | ||
|
||
if (array_key_exists('$'.$parameter->name, $bindings)) { | ||
$arguments[$key] = $bindings['$'.$parameter->name]; |
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.
to help spot typos in named args, what about keeping a pass-wide property of all named args and their usages, and throw for unused ones at the end of the pass?
|
||
if (null === $parameters) { | ||
if (!$class) { | ||
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": the class is not set.', $this->currentId)); |
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.
not needed, already checked just below
3101fb3
to
94d6da9
Compare
As for named arguments the updated arguments are shown by the descriptors, so I agree with you, I don't think we need to support the bindings in the descriptors.
I found a way to throw an error when a binding is used nowhere so no need to do this anymore. |
…g (nicolas-grekas) This PR was merged into the 3.3-dev branch. Discussion ---------- [DI] Add "factory" support to named args and autowiring | Q | A | ------------- | --- | Branch? | 3.3 | Bug fix? | yes | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - To me, factories are expected to work named arguments. Doing so also unlocks supporting them when autowiring, and will benefit #22187 soon. Commits ------- 27470de [DI] Add "factory" support to named args and autowiring
} | ||
} | ||
|
||
if ($value->getFactory() || null !== $constructor) { |
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.
missing logic update?
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.
yes 👍
|
||
if (null === $parameters) { | ||
$r = $this->getReflectionMethod($value, $method); | ||
$class = $r instanceof \ReflectionMethod ? $r->class : $this->currentId; | ||
$parameters = $r->getParameters(); | ||
} | ||
|
||
if ('$' === $key[0]) { |
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.
breaks when $key === ''
?
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.
yes, added a isset
check
The build failure is because this is not yet in master. |
this PR description could be enhanced to emphasis the use of mapping arguments to parameters, because that's a common need, see #23718 |
Can we improve the DX of this feature? If this comment by @GuilhemN is correct, this is how the feature works now: services:
_defaults:
autowire: true
autoconfigure: true
public: false
bind:
$argument1: '%kernel.project_dir%'
$argument2: '%app.container_param%'
$argument3: '%app.another_param%'
# ... This is my proposal: services:
_defaults:
autowire: true
autoconfigure: true
public: false
arguments:
$argument1: '%kernel.project_dir%'
$argument2: '%app.container_param%'
$argument3: '%app.another_param%'
# ... My reasoning:
|
@javiereguiluz arguments are not the same, they can only be used in the constructor while bindings are meant to be used in the constructor, method calls and in action arguments.
That's just one use case :) It can also be used in libraries as a totally predictable autowiring, to deal with multiple instance of the same class, to have a nicer syntax to replace services in action arguments and so on. |
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.
Small remaining comments.
This should be merged now, we already identified several cases where this feature is useful and would be a nice addition.
If we don't merge, we cannot improve it later if needed - nor experience it for real.
*/ | ||
protected function processValue($value, $isRoot = false) | ||
{ | ||
if ($value instanceof TypedReference) { |
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.
&& $value->getType() === (string) $value
because typed reference can be configured with an explicit id (as done in autowiring pass L330 in getAutowiredReference)
needs a test case
return parent::processValue($value, $isRoot); | ||
} | ||
|
||
foreach ($value->getBindings() as $key => $binding) { |
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.
foreach ($bindings as ...
$bindings = $this->getArgumentsAsPhp($service, 'bind', $file); | ||
if (isset($defaults['bind'])) { | ||
// deep clone, to avoid multiple process of the same instance in the | ||
// 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.
comment should be on one line
// deep clone, to avoid processing the same definitions several times in compiler passes
@@ -519,6 +534,22 @@ private function parseDefinition($id, $service, $file, array $defaults) | |||
} | |||
} | |||
|
|||
if (isset($defaults['bind']) || isset($service['bind'])) { | |||
// deep clone, to avoid multiple process of the same instance in the |
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.
see above
@javiereguiluz here is a typical example: services:
_defaults:
bind:
$projectDir: '%kernel.project_dir%' This means that any arguments (constructor or setter) that are named
Do we get your vote? :) |
@nicolas-grekas comments fixed :) |
Thank you @GuilhemN. |
Named parameters aren't supported yet.Edit:
Current behavior is throwing an exception when a binding is not used at all, in no services of a file if it was inherited from
_defaults
or in no services created from a prototype.It will pass if the bindings are all used in at least one service.