Skip to content

[DependencyInjection] Autowiring: add setter injection support #17608

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

Merged
merged 1 commit into from
Jul 1, 2016

Conversation

dunglas
Copy link
Member

@dunglas dunglas commented Jan 30, 2016

Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets n/a
License MIT
Doc PR todo

Add support for setter injection in the Dependency Injection Component.

Setter injection should be avoided when possible. However, there is some legitimate use cases for it. This PR follows a proposal of @weaverryan to ease using DunglasActionBundle with #16863. The basic idea is to include a setter for the required service in the trait and let the DependencyInjection Component autowire the dependency using the setter.

This way, a newcomer can use the trait without having to create or modify the constructor of the action.

/cc @derrabus

*/
private function autowireConstructor($id, Definition $definition, \ReflectionClass $reflectionClass)
{
if (!($constructor = $reflectionClass->getConstructor())) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

() around the var def should be removed.

@fabpot
Copy link
Member

fabpot commented Jan 31, 2016

This feature is indeed a must have to better support the uses cases described in the linked issue.

@sstok
Copy link
Contributor

sstok commented Jan 31, 2016

What if someone wants to use autowiring but only wants to use constructor autowire and not setter autowire? Or not for all methods (in that case setter autowiring should not be used).

@dunglas
Copy link
Member Author

dunglas commented Jan 31, 2016

@sstok If a call is explicitly defined for a method, the autowiring system will ignore this method.

Supporting only constructor autowiring or both setter and constructor using a new flag in Definition should be possible but it looks overkill too me. If you have such edge case for a service, the clean way is just to not use autowiring at all for this service. What do you think?

@dunglas
Copy link
Member Author

dunglas commented Feb 5, 2016

@fabpot support for function setDependencies(RouterInterface $router, Twig $twig) style added.

@weaverryan
Copy link
Member

@dunglas I love it! I just tried the code, here are the issues / concerns I found:

  1. The fact that would we start auto-wiring setters is a behavior change - it could be a BC break (setters are suddenly called that weren't before). Question: is this a BC break?

  2. If I create a setter with no arguments, the setter is called. I don't think it should: in this case, there's nothing to inject.

  3. If a setter does have an argument, but its optional and we can't autowire it, : I think we should do nothing, instead of calling the setter with null.

  4. If a setter does have an argument and it's required, but we can't auto-wire it (e.g. no type-hint), I think we should do nothing (don't call the method, but don't throw an exception - the user may manually add a calls for this).

Thanks!

@fabpot
Copy link
Member

fabpot commented Feb 18, 2016

@dunglas any feedback on @weaverryan comments?

@dunglas
Copy link
Member Author

dunglas commented Feb 18, 2016

They are all relevants, I need to find some time to work on this.

@dunglas
Copy link
Member Author

dunglas commented Feb 18, 2016

For the BC break, as @sstok has the same concern, I'll introduce a new aurowireSetter property in Definition that will be false by default and set it to true in action services created by DunglasActionBundle.

@weaverryan
Copy link
Member

@dunglas That will fix BC, but I'm concerned about usability - e.g.:

services:
    spring_weather_manager:
        class: ...
        autowire: true
        autowire_setter: true

If it weren't for the BC problem, my opinion would be: always do setter injection - after all, if it causes you any issues, don't use it. For that reason, I'd prefer a global flag to turn this on/off that goes away in 4.0.

@dunglas
Copy link
Member Author

dunglas commented Feb 18, 2016

On the other hand, the BC break is very limited: it will only impact people using autowiring (just a few), having classes with setters for services (unlikely, because it wasn't supported in the first version of the autowiring system), and having a setter that broke the class when triggered (very very unlikely). Maybe can we say that this "BC break" is acceptable, as it's more an edge case than a BC break.

@weaverryan
Copy link
Member

Honestly, I'm inclined to agree. This is a "magic" feature you're opting into, it's only 1 version it's been available, it won't affect many people, and if it causes an issue, it'll happen at build time (very easy to debug).

@fabpot
Copy link
Member

fabpot commented Feb 18, 2016

👍 for the small BC break instead of yet another configuration setting.

@stof
Copy link
Member

stof commented Feb 18, 2016

I agree with you about accepting this small BC break (but we need to document it clearly in changelog files)

@weaverryan
Copy link
Member

Ok, so BC break is ok. This leaves items 2-4 #17608 (comment), when you have some time :)

@fabpot
Copy link
Member

fabpot commented Mar 2, 2016

@dunglas Any idea when you will be able to finish this one?

@dunglas
Copy link
Member Author

dunglas commented Mar 2, 2016

Before the end of this week.

@dunglas
Copy link
Member Author

dunglas commented Mar 3, 2016

All issues fixed. Can you review @weaverryan?

@weaverryan
Copy link
Member

Hi Dunglas!

I need a few more days to get back a full report - I will make a very clear explanation of the functionality so that everyone is clear how it works (a few cases are almost subjective, so I want it to be clear to everyone).

The only issue I've found is that if we cannot autowire because the type-hint is for a non-existent class, it simply doesn't call the method. That's different than the constructor where it throws an exception.

Also, I think we (probably me, as I've had some recent PR's merged) may have introduced a bug into the master branch. I'm too short on time to look further right this moment, but I'm getting weird results if I type-hint something like DataCollector or EventSusbcribterInterface. These are items that should not be autowireable (as there are multiple services for them), but on master locally right now, they are autowiring - with one of the matching services. This does not happen on 3.0.1. If someone else can verify this, awesome. Otherwise, I'll look more soon.

I think we're getting quite close to mastering the exact behavior we want. We need to get it right for 3.1, because whatever we end up with will probably stay with us due to BC.

*
* @throws RuntimeException
*/
private function autowireMethod($id, Definition $definition, \ReflectionMethod $reflectionMethod, $constructor)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about renaming $constructor to $isConstructor to be clearer?

@weaverryan
Copy link
Member

I think this PR is ready. The question is: do we want it? It's opinionated (but so is autowiring in general, and it's opt-in). Some of the new autowiring features/uses (like those in DunglasActionBundle) are quite radical, and I think they need to be tested before considering moving further forward (in the core) in that direction. However, if this were merged, it allows more of these ideas to be tested out in the wild - like using traits e.g. RouterTrait) to automatically setter-inject services+shortcuts into your services.

I'm 👍, but I don't feel as confidently about this as I have other things in the past.

@symfony/deciders

@xabbuh
Copy link
Member

xabbuh commented Apr 5, 2016

👍 LGTM

@dunglas dunglas force-pushed the autowiring-setter branch from 056612c to 5470db2 Compare April 18, 2016 19:08
@dunglas
Copy link
Member Author

dunglas commented Apr 18, 2016

Rebased. Can be merged.

@dunglas
Copy link
Member Author

dunglas commented Jun 7, 2016

@symfony/deciders what do we do of this PR? Should I close it or merge it?

@fabpot
Copy link
Member

fabpot commented Jul 1, 2016

Re-read everything here. @weaverryan and @dunglas did a great job at defining exactly what we should do and what is out of scope. I agree with everything @weaverryan said and as this is what @dunglas implemented, I'm 👍 (I would not advocate its usage for everything but as this is an opt-in feature anyway, this looks good to me).

@fabpot
Copy link
Member

fabpot commented Jul 1, 2016

@dunglas Looks like it needs a rebase

@dunglas dunglas force-pushed the autowiring-setter branch from 5511bc4 to 8c34d6d Compare July 1, 2016 08:36
@dunglas
Copy link
Member Author

dunglas commented Jul 1, 2016

Rebased.

@fabpot
Copy link
Member

fabpot commented Jul 1, 2016

@dunglas I didn't notice but history is really ugly but as there are 2 contribs, gh won't be able to squash commits. Can you do that for me?

@dunglas
Copy link
Member Author

dunglas commented Jul 1, 2016

Should I merge all commits in 1 or should I keep the commit of @weaverryan?

@fabpot
Copy link
Member

fabpot commented Jul 1, 2016

I think you can merge everything into 1 commit as @weaverryan contrib is not huge.

@dunglas dunglas force-pushed the autowiring-setter branch from 786a915 to a0d7cbe Compare July 1, 2016 08:58
@dunglas
Copy link
Member Author

dunglas commented Jul 1, 2016

Done.

@fabpot
Copy link
Member

fabpot commented Jul 1, 2016

Thank you @dunglas.

@fabpot
Copy link
Member

fabpot commented Jul 1, 2016

@dunglas Can you take care of the docs?

@fabpot fabpot merged commit a0d7cbe into symfony:master Jul 1, 2016
fabpot added a commit that referenced this pull request Jul 1, 2016
… support (dunglas)

This PR was merged into the 3.2-dev branch.

Discussion
----------

[DependencyInjection] Autowiring: add setter injection support

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | n/a
| License       | MIT
| Doc PR        | todo

Add support for setter injection in the Dependency Injection Component.

Setter injection should be avoided when possible. However, there is some legitimate use cases for it. This PR follows a proposal of @weaverryan to ease using [DunglasActionBundle](https://github.com/dunglas/DunglasActionBundle) with #16863. The basic idea is to include a setter for the required service in the trait and let the DependencyInjection Component autowire the dependency using the setter.

This way, a newcomer can use the trait without having to create or modify the constructor of the action.

/cc @derrabus

Commits
-------

a0d7cbe [DependencyInjection] Autowiring: add setter injection support
@dunglas dunglas deleted the autowiring-setter branch July 1, 2016 09:02
@dunglas
Copy link
Member Author

dunglas commented Jul 1, 2016

Yes, I've a lot of docs to write...

nicolas-grekas added a commit to nicolas-grekas/symfony that referenced this pull request Nov 2, 2016
…etter injection support (dunglas)"

This reverts commit 7eab6b9, reversing
changes made to 35f201f.
dunglas added a commit that referenced this pull request Nov 2, 2016
… add setter injection support (dunglas)" (nicolas-grekas)

This PR was merged into the 3.2-dev branch.

Discussion
----------

Revert "feature #17608 [DependencyInjection] Autowiring: add setter injection support (dunglas)"

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

This reverts commit 7eab6b9, reversing
changes made to 35f201f.

As discussed in #20167

Commits
-------

bf91eda Revert "feature #17608 [DependencyInjection] Autowiring: add setter injection support (dunglas)"
fabpot added a commit that referenced this pull request Mar 2, 2017
…t (dunglas)

This PR was squashed before being merged into the 3.3-dev branch (closes #18193).

Discussion
----------

[FrameworkBundle] Introduce autowirable ControllerTrait

| Q | A |
| --- | --- |
| Branch | master |
| Bug fix? | no |
| New feature? | yes |
| BC breaks? | no |
| Deprecations? | no |
| Tests pass? | yes |
| Fixed tickets | #16863 |
| License | MIT |
| Doc PR | todo |

This is the missing part of the new controller system and it's fully BC with the old one.

Used together with the existing autowiring system, #17608 and [DunglasActionBundle](https://github.com/dunglas/DunglasActionBundle) it permits to inject explicit dependencies in controllers with 0 line of config. It's a great DX improvement for Symfony.
It also has a lot of another advantages including enabling to reuse controller accros frameworks and make them easier to test. See https://dunglas.fr/2016/01/dunglasactionbundle-symfony-controllers-redesigned/ for all arguments.

Magic methods of old controllers are still available if you use this new trait in actions.

For instance, the [`BlogController::newAction`](https://github.com/symfony/symfony-demo/blob/master/src/AppBundle/Controller/Admin/BlogController.php#L70) form the `symfony-demo` can now looks like:

``` php
namespace AppBundle\Action\Admin;

use AppBundle\Form\PostType;
use AppBundle\Utils\Slugger;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;

class NewAction {
    use ControllerTrait;

    private $slugger;

    public function __construct(Slugger $slugger)
    {
        $this->slugger = $slugger;
    }

    /**
     * @route("/new", name="admin_post_new")
     */
    public function __invoke(Request $request)
    {
        $post = new Post();
        $post->setAuthorEmail($this->getUser()->getEmail());

        $form = $this->createForm(PostType::class, $post)->add('saveAndCreateNew', SubmitType::class);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $post->setSlug($this->slugger->slugify($post->getTitle()));
            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($post);
            $entityManager->flush();

            $this->addFlash('success', 'post.created_successfully');
            if ($form->get('saveAndCreateNew')->isClicked()) {
                return $this->redirectToRoute('admin_post_new');
            }

            return $this->redirectToRoute('admin_post_index');
        }

        return $this->render('admin/blog/new.html.twig', array(
            'post' => $post,
            'form' => $form->createView(),
        ));
    }
}
```

As you can see, there is no need to register the `slugger` service in `services.yml` anymore and the dependency is explicitly injected. In fact the container is not injected in controllers anymore.

Convenience methods still work if the `ControllerTrait` is used (of course it's not mandatory). Here I've made the choice to use an invokable class but this is 100% optional, a class can still contain several actions if wanted.

Annotations like `@Route` still work too. The old `abstract` controller isn't deprecated. There is no valid reason to deprecate it IMO. People liking using the "old" way still can.

Unless in #16863, there is only one trait. This trait/class is basically a bunch of proxy methods to help newcomers. If you want to use only some methods, or want explicit dependencies (better), just inject the service you need in the constructor and don't use the trait.

I'll create open a PR on the standard edition soon to include ActionBundle and provide a dev version of the standard edition to be able to play with this new system.

I'll also backport tests added to the old controller test in 2.3+.

**Edit:** It now uses getter injection to benefit from lazy service loading by default.

Commits
-------

1f2521e [FrameworkBundle] Introduce autowirable ControllerTrait
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants