-
-
Notifications
You must be signed in to change notification settings - Fork 319
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
Uninstantiable defaulted constructor parameter injection issue #168
Uninstantiable defaulted constructor parameter injection issue #168
Conversation
Hi, thank you for taking the time to report this and write the test! I will have a look at it as soon as possible because indeed ideally it should work. |
OK so here is a (hopefully generic) solution I was thinking about (I am using the same class names as your example): Right now, What I first thought: let's change I'm not sure this is easy to do because that would mean introducing a concept of "validating" a definition (i.e. making sure it can be resolved), and that doesn't exist today. That seems like a lot of work. But the main question is: does that even make sense for Because what do you expect if you call And the second question is: what do we validate? We can check that the class is instantiable. But do we check if all the constructors parameters are set/guessable too? Do we check then recursively for all the dependencies too? It seems a lot of work and performance impact :/ I'm thinking out loud, maybe you see a clear solution that I'm missing? Or maybe you have another opinion on all these questions? Suggestions are welcome. |
When I was trying to find a solution, and I discovered that As someone coming in with limited knowledge, I would have expected both If they can not be added, at least in certain circumstances, like perhaps, when the hint is an uninstantiable class/interface AND it is optional, I think it might solve this particular issue. But I don't have enough knowledge of PHP-DI to say whether that would break other features. @jmalloc it would be good to get your opinion here, too. |
Well it's the "autowiring" thing that is responsible of that. To be clear, there are 3 definition sources:
For autowiring and annotation, definitions are generated on the fly when So when So maybe it does actually make sense for |
To be honest, I don't know :) What would happen if when this happens:
And the autowiring sees If you could point me to where the constructor is scanned I might have another go at a fix, too. |
Constructors are scanned by "autowiring" (called Reflection in the code) here: ReflectionDefinitionSource However Here is how it goes:
edit: a better wording for scan would be "get definition" because that's how it's called in the code… |
As you can see So maybe calling
is a good idea. We just have to make |
A generic way to determine if a definition can be resolved would maybe be to edit the interface DefinitionResolver
{
public function resolve(Definition $definition, array $parameters = array());
public function canBeResolved(Definition $definition, array $parameters = array());
} Then each implementation needs to have this additional method, and can perform checks (or not if not necessary). I'm beginning to like this actually. |
Okay, if that sounds good to you, and it solves the problem, then I am happy! But... I also managed to get the tests to pass by implementing the idea I was trying to explain before, by changing --- a/src/DI/Definition/Source/ReflectionDefinitionSource.php
+++ b/src/DI/Definition/Source/ReflectionDefinitionSource.php
@@ -66,7 +66,7 @@ class ReflectionDefinitionSource implements DefinitionSource
foreach ($constructor->getParameters() as $index => $parameter) {
$parameterClass = $parameter->getClass();
- if ($parameterClass) {
+ if ($parameterClass && ($parameterClass->isInstantiable() || !$parameter->isOptional())) {
$parameters[$index] = new EntryReference($parameterClass->getName());
}
} |
I see what you suggest, but wouldn't there be problems if at some point you bind |
Okay, yeah, I see why that's not such a great solution now :) |
OK then I'll have a look at the other solution. It's going to be a bit more complex, but hopefully it will be in 4.2. If you want to give it a shot let me know, but don't feel obligated I understand this is work all over the place so it requires knowing the internals ;) |
… to an unresolvable entry Optional parameters that are type-hinted with interfaces should use the default parameter value if the interface is not mapped to a concrete class. Else that makes autowiring pretty much useless if a class takes optionally a parameter that we don't want to specify (use default value).
@ezzatron I've implemented what I was thiking about and pushed a branch: #169 I'll give it a few days to think about it a bit more. If you can review, and if others want to give their opinion too it's welcome. The main problem I have with this is that concept of "is the definition resolvable?" Which is a good concept, but here it is just used to spot uninstantiable classes. IMO "un-resolvable" should also cover every case where the object can't be created (in theory). But then if we do that, should we really fallback to the parameter's default value if the user has an error in his DI configuration? I'm afraid there will be lots of "WTF, why is it not injecting X??" (the user will not get alerted that his definitions are invalid). Also, even if we keep the "unresolvable == uninstantiable" compromise, here is a use case that can lead to WTF: return [
// Woops I mapped the interface to an abstract class but I didn't spot my mistake!
'MyInterface' => 'MyAbstractClass'
];
class Foo {
public function __construct(MyInterface $bar = null) {
}
} Here the user will get So I'm not sure that fix is right :/ |
I am beginning to wonder if it wouldn't be better for autowiring to simply ignore all optional parameters? I mean when you think about it, it makes more sense that way. For example this kind of pattern is quite common:
Optional parameters are used to customize things that would anyway work without it. So it makes not much sense for the container to want to resolve and inject it. In the end, if you want an optional parameter to be injected, then you have to specify it manually in the config (or with annotations). What do you think? |
Yeah, actually, that does make sense. Personally, it would make my life easier, so I'm all for it. Although, if you change it now, I can see it causing problems for people who currently rely on that feature. It's a tough call. |
@ezzatron FYI everything was released yesterday in 4.2! |
Awesome, thanks for all the hard work! I'm on holidays right now, but I'll be sure to check out when I get back. |
I keep encountering this issue, so I thought I'd have a crack at a fix. Unfortunately it wasn't as simple as it initially seemed, so this is only a test case to demonstrate the issue.
A common pattern we use in my workplace is to have defaulted constructor parameters. We also use interfaces for just about everything, so the class may look something like this:
So I would add definitions to our container something like:
Leaving out a definition for
BazInterface
beacuse we just want to use the default. Now when you requestFooInterface
from the container:You get a nasty exception something like:
The expected result is that
get()
returns an instance ofFoo
using the default value for$baz
.This PR includes a rough integration test that currently fails because of the issue mentioned above. It is not intended to be merged as it doesn't follow the style of your other tests, but should be good enough to aid in diagnosing the cause of the issue.
Thanks!