Skip to content

[DI] Support getter-based dependency injection #20657

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

Closed
nicolas-grekas opened this issue Nov 27, 2016 · 31 comments
Closed

[DI] Support getter-based dependency injection #20657

nicolas-grekas opened this issue Nov 27, 2016 · 31 comments
Labels
DependencyInjection RFC RFC = Request For Comments (proposals about features that you want to be discussed)

Comments

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Nov 27, 2016

A concept well described in e.g. this thread:
http://www.theserverside.com/news/thread.tss?thread_id=26205
and also in #835

Basically, one could then be able override the getConnection method of such a class:

class foo {
// ...
    public/protected function getConnection()
    {
        return new FooDefaultConnection();
    }
}

with a generated proxy ($this->container auto-injected by some to-be-defined mean):

class extends foo {
    private $container;

    public/protected function getConnection()
    {
        return $this->container('connection_for_foo'):
    }
}

in XML, this could defined with something like e.g.:

<service id="foo" class="foo">
<getter name="getConnection" type="service" id="connection_for_foo" />
</service>

so that $container->get('foo') returns an instance of the generated class (and by this mean an instance of foo of course).

The benefits?

  • Lazyness made simple and almost free! I may give it a try;
  • immutability (if no corresponding setter exists of course)
@nicolas-grekas nicolas-grekas added the RFC RFC = Request For Comments (proposals about features that you want to be discussed) label Nov 27, 2016
@Taluu
Copy link
Contributor

Taluu commented Nov 28, 2016

Should it support abstract methods too ? We would of course have to define the abstract methods in the service declaration

<?php

class foo {
    abstract protected/public function getBar();
}

And the proxy would be

<?php

class extends foo {
    private $container;

    protected/public function getBar()
    {
        return $this->container->get('for_foo');
    }
}

for about the same service declaration you mentionned

@nicolas-grekas
Copy link
Member Author

You should read the link I posted, you'll find exactly this example :) So yes, it will work by nature.

@stof
Copy link
Member

stof commented Nov 28, 2016

this seems interesting.

Note that the XML file should not use @connection_for_foo to create a reference though. We never rely on the @ convention in XML and PHP config files.

@nicolas-grekas
Copy link
Member Author

I just realized that getter-injection would have another big benefit: immutability! (if no corresponding setter exists of course)

@unkind
Copy link
Contributor

unkind commented Dec 17, 2016

I just realized that getter-injection would have another big benefit: immutability! (if no corresponding setter exists of course)

You compare getter injection with setter one here, but constructor injection has no such issue as well. Well, just don't use setter injection, it is always can be avoided. So is it fair to mention it as a benefit of getter injection?

Lazyness made simple and almost free! I may give it a try;

This is true, but the only difference I see with existing lazy-services is that is allows to lazy-load services with final keyword. But here is irony: your approach forces to have classes without final keyword.

This approach, in my opinion, has big disadvantage: it hurts class API (no ability to add final keyword; additional protected/public getters; it's more tedious to unit-test abstract classes).

In which cases getter injection is better than constructor one?

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Dec 18, 2016

@unkind every approach has drawbacks and benefits. That's fair. That's why we need all of them.

@unkind
Copy link
Contributor

unkind commented Dec 18, 2016

@nicolas-grekas So what's the benefit this approach has over existing constructor injection?

@nicolas-grekas
Copy link
Member Author

Laziness in the core! In a much more reasonable way than the current proxy-manager, which is an optional dep (and should remain such).

@unkind
Copy link
Contributor

unkind commented Dec 18, 2016

In a much more reasonable way than the current proxy-manager

Magic getter which works with this DI-container only is "much more reasonable" way? Why?

which is an optional dep

What's the problem? If you don't want to load this dep, so you probably don't need lazy-services.

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Dec 18, 2016

There is nothing magic, it's plain inheritance. I didn't wait to use Symfony to "open/close" some of my classes by providing a protected method for alternative behavior injection. Symfony DI was laking a way to take advantage of this design. Here is a PR to fill the hole.
And yes, it's much more reasonable than dumping a huge proxy class full of real (and really) magic methods.

@unkind
Copy link
Contributor

unkind commented Dec 18, 2016

There is nothing magic, it's plain inheritance

ProxyManager utilizes inheritance as well.
But the difference is that ProxyManager is transparent, I can disable it and my code will work as expected, there will be no impact in my code.

I call getter injection "magic", because it doesn't simply work w/o your DI-container, I don't see dependencies in the constructor, they come from nowhere.

@sstok
Copy link
Contributor

sstok commented Dec 18, 2016

I can imagine some situations where this would make sense, the Template pattern (for abstract classes) is exactly designed for this type of usage, plus with PHP 7 return types you can still guard it provides a correct implementation 👍

Else you can have an, oh.. he 😄 default implementation which can be replaced with a lazy loading version without the need of another class! 😵 of course you need to allow this type of replacement, but that's a choice "you" make.

@unkind I share your concerns, this is not suitable for every situation but there are use-cases.
If you services are final but implement an interface they can use interface bound proxies #20656 when they must be lazy.

@Ocramius
Copy link
Contributor

I don't understand the feature, or it doesn't make sense to me.

You just described what looks like factories to me, and then you replace the factory code completely, which is a big assumption.

In my opinion, this is a big leap of faith, and you may also break internal code.

Every time I tried to push the boundaries of the ProxyManager to allow scenarios like this one, I had huge issues in functional testing, since loads and loads of edge cases arise.

So yes, dumping huge proxy classes is generally better than overriding implementations, because you only rely on the public signature, and never on the implementation.

That's why it is transparent, and why I wrote over a damn megabyte of tests to ensure complete transparency at all times: it ABSOLUTELY MUST NOT feel nor behave magically.

What can be done (and cannot be done immediately, since it needs accurate analysis first) is customising proxies to intercept only method calls to some "factory methods" to add:

  • Laziness
  • Memorisation

This is what bitExpert/disco does, so you can directly look at that codebase for examples.

Please also note that Disco doesn't alter method implementations, but only proxies the container itself, as well as the services being retrieved (much like symfony/dependency-injection currently does).

Recap: DO NOT replace method implementations. DO NOT trust that you know why a method is implemented in a certain way.

@Ocramius
Copy link
Contributor

That's why we need all of them.

Don't think this way, please: this is where maintenance nightmares (for both vendor and consumer) start.

@docteurklein
Copy link
Contributor

docteurklein commented Dec 18, 2016

Wouldn't this kind of couple the behavior with the container impl?
This pattern is not really helping people to write better code!

I mean: are you proposing people to hardcode dependencies in getters ?
and then saying "worry not, the symfony DIC will extend your class to make it actually dynamic" ?

That seems like a bad idea to me.

@docteurklein
Copy link
Contributor

docteurklein commented Dec 18, 2016

People relying on this feature will write code that simply wouldn't work without the DIC, no?

PS: by "wouldn't work", I mean wouldn't allow to change the harcoded factory.

@nicolas-grekas
Copy link
Member Author

Wow, so much strong affirmative and negative statements here. Please keep the discussion open!
I'll stick to implementing it correctly so that we can judge on actual facts and not feelings.

@Ocramius
Copy link
Contributor

@nicolas-grekas discussion is open, I'm just warning based on first hand bad experiences and failed attempts.

If I didn't misinterpret (to be seen after a generated DIC PoC) what is being suggested here, I see lots of new problems.

@ro0NL
Copy link
Contributor

ro0NL commented Dec 18, 2016

People relying on this feature will write code that simply wouldn't work without the DIC, no?

Agree. I already imagine classes with only stub methods, relying to be overridden with DI.

That said, i dont see how this relates to dependency injection, i mean we're combining it with a proxy generator thingy.. right?

Finally i would like to know when to use lazy getter overriding, opposed to lazy setter/constructor injection? Im not sure saving on a composer package justifies changing your approach..

@lisachenko
Copy link

lisachenko commented Dec 19, 2016

The more I look on this issue, the more I think about it as autowiring implementation. See my gist https://gist.github.com/lisachenko/6345683 for the idea. This magic closure binding should be placed into the one single library to keep it clear and simple. It will be a nightmare to add one more layer into the symfony DI itself, because of increasing complexity.

@nicolas-grekas maybe it's time to use AOP proxy generation for super-lazy control over the source-code? AOP has access to the protected properties and getters, can inject services and much more. It's also possible to implement fully decoupled app with super-lazy property injection. Super-lazy property injection means that service will be injected only during the real access or method call. AOP will unset getters and store information about how to inject this service, then magic getter for the class will be called that will inject our dependency. That's it )

BTW, my solution also works with final classes: engine removes final keyword from the original class, rename it and define a child with the same name and final modifier:
final class Foo {} => renamed to the class Foo_AopProxied {} and our proxy is generated instead of original class final class Foo extends Foo_AopProxies {}

@docteurklein
Copy link
Contributor

docteurklein commented Dec 19, 2016

@nicolas-grekas

Wow, so much strong affirmative and negative statements here. Please keep the discussion open!
I'll stick to implementing it correctly so that we can judge on actual facts and not feelings.

sorry, didn't want to be offensive. I totally agree with you on trying and see how it goes. I just wanted to give my 2 cents.

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Dec 19, 2016

The PR is ready, it just misses a few tests (and doc). For those who'd like to try it, the configuration is the same as for properties, except of course that you should use getter/getters where property/properties is allowed in yaml/xml. "Key" is the name of the getter, "value" the return value of the getter.

@lisachenko I agree with you that this converges with autowiring, and in fact we have some nice plan with @dunglas to take advantage of getter injection (Kevin might be working on it right now so we'll see how it shapes soon). I wouldn't go as far as doing AOP code rewriting for now. At least that doesn't look like an "easy pick", while this RFC was quite "easy" to implement. But if you see it coming closer, then that's nice :)

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Dec 19, 2016

People relying on this feature will write code that simply wouldn't work without the DIC, no?

doing this kind of overriding without any DIC is trivial: in tests just use e.g. phpunit's mock system (which basically does exactly that). Otherwise, use anonymous classes. Done in a few lines. No DIC, just plain PHP. Nothing designed "for Symfony". Decoupling is at its best (one of them).

In your comment @docteurklein, it looks like you're thinking that people design their code to fit the DIC. Obviously, that's always a very bad idea so I don't take this path. Instead I like thinking about empowering people (me being the first "people"). I'm missing getter injection because I'm used since years to design some of my classes using a dynamic getter when I know that the related dependency is heavy to instantiate but not required all the time. If you look at e.g. Doctrine DBAL, that's almost exactly what they do also with the connect method. It's not a getter, but it could be (they just save one function call by not doing exactly so).

As you see with the Doctrine example (and I'm sure you'll find more), laziness is a first class concern in some libs. Saying "use lazy: true" reads as "couple your biz requirements with the framework", because obviously that won't work if you don't have the wiring to make it work. That's why it's not a solution to anything but userland concerns @ro0NL.

So, this is using a getter as an internally managed initializer yes. That's fine when it's your responsibility to deal with laziness. Should the getter be open to extensibility? It could. This is just using inheritance to do some open/closed principle. Nothing new here.

Symfony is just missing a way to take advantage of such design. Nice!

Note that I never suggested that getter injection should rule them all. It just fits some use cases and as such, empowers the corresponding people. Nothing more.

@unkind
Copy link
Contributor

unkind commented Dec 19, 2016

Saying "use lazy: true" reads as "couple your biz requirements with the framework"

Strong statement. You can write something like

final class MyLazyConnectionWithoutAnyFramework implements Connection
{
    public function __construct(ConnectionFactory $connectionFactory, string $host, ...)
    {
        // ..
    }

    public function query(...)
    {
        if (!$this->connection) {
            $this->connection = $this->connectionFactory->...
        }

        $this->connection->query(...);
    }
}

doing this kind of overriding without any DIC is trivial:

Same I can say about lazy: true, see above. You can easily make it lazy with plain PHP if you want to go away from the framework.

It's about composition over inheritance. By some reason, you force inheritance where we can simply work with composition. I don't understand it.

This is just using inheritance to do some open/closed principle. Nothing new here.

public/protected methods are bad extensions points in most cases, that's why, for example, in Symfony you have a rule "private by default": try to find another way to extend behaviour.

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Dec 19, 2016

@unkind where is this code using lazy: true?

Symfony you have a rule "private by default"

but obviously not all of them are private...

@unkind
Copy link
Contributor

unkind commented Dec 19, 2016

@unkind where is this code using lazy: true?

Let's say you have

services:
    connection:
        alias: mysql_connection

    mysql_connection:
        class: Acme/MySqlConnection
        lazy: true
        arguments: [%mysql_host%, %mysql_user%]

Then dev is like: "I don't want to have ProxyManager dep for some weird reason, let's get rid of it!"
No problem:

services:
    connection:
        alias: lazy_connection

    lazy_mysql_connection:
        class: Acme\MyLazyConnectionWithoutAnyFramework
        arguments: [%mysql_host%, %mysql_user%]

MyLazyConnectionWithoutAnyFramework will create instance of MySqlConnection on its own.

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Dec 19, 2016

OK, that's almost exactly what I tried to illustrate with my DBAL example.

@robfrawley
Copy link
Contributor

robfrawley commented Dec 20, 2016

I read the linked article and the entire discussion here and I really only have one thing to say: everyone needs to calm down with their apocalypse-destined imaginations. The PR seems particularly harmless in terms of overhead and offers an alternate approach to OAC/DI/IOC (that has been implemented in many other frameworks/languages) that might be appropriate for different contexts. From my perspective, complaints about lack of testability are overblown as well: regardless of OAC/DI/IOC there is setup/teardown involved. Offering choice/options for the programmer isn't a bad thing, even if it isn't right for your project, and all methods have their tradeoffs. @nicolas-grekas Thanks for all your work on Symfony and this RFC/PR 👍

@ro0NL
Copy link
Contributor

ro0NL commented Dec 20, 2016

@nicolas-grekas thought about this today, and i definitely appreciate this feature (so dont get me wrong :).

What about a more generic approach, so it allows for a) implements and b) some form of method/argument forwarding (think Proxy::caller('arg') => Service::caller('arg'), where returning is optional, ie. void, forwarded return, or service instance).

But maybe im going too far with this now ;-)

@unkind
Copy link
Contributor

unkind commented Dec 20, 2016

@ro0NL are you asking for #20656?

@ro0NL
Copy link
Contributor

ro0NL commented Dec 21, 2016

Yep, totally. Missed it.. thanks!

@fabpot fabpot closed this as completed Jan 30, 2017
fabpot added a commit that referenced this issue Jan 30, 2017
This PR was merged into the 3.3-dev branch.

Discussion
----------

[DI] Add getter injection

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #20657
| License       | MIT
| Doc PR        | symfony/symfony-docs#7300

Getter overriding by the container will allow a new kind of dependency injection which enables easier laziness and more immutable classes, by not requiring any corresponding setter. See linked issue for more.

This is WIP:
- [x] wire the concept
- [x] dump anonymous classes with PhpDumper
- [x] generate at runtime in ContainerBuilder::createService
- [x] tests
- [x] make it work on PHP 5

Commits
-------

cb49858 [DI] Add getter injection
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
DependencyInjection RFC RFC = Request For Comments (proposals about features that you want to be discussed)
Projects
None yet
Development

No branches or pull requests