Skip to content

[DI] Add "by-id" autowiring: a side-effect free variant of it based on the class<>id convention #22060

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
Mar 25, 2017

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Mar 19, 2017

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 PR adds a new autowiring mode, based only on the class <> id convention.
This way of autowiring is free from any conflicting behavior, which is what I was looking for to begin with.

The expected DX is a bit more involving than the current way we do autowiring. But it's worth it to me, because it's plain predictable - a lot less "magic" imho.

So in this mode, for each App\Foo type hint, a reference to an "App\Foo" service will be created. If no such service exists, an exception will be thrown. To me, this opens a nice DX: when type hinting interfaces (which is the best practice), this will tell you when you need to create the explicit interface <> id mapping that is missing - thus encourage things to be made explicit, but only when required, and gradually, in a way that will favor discoverability by devs.

Of course, this is opt-in, and BC. You'd need to do eg in yaml: autowire: by_id.
For consistency, the current mode (autowire: true) can be configured using autowire: by_type.

@theofidry
Copy link
Contributor

cool, will try it during the week/weekend!

@javiereguiluz
Copy link
Member

Could you please show a before/after comparison of what the user has to do to use this feature? Or some small but complete code example? Otherwise it's hard to follow :( Thanks!

return $this->autowired;
}

/**
* Sets autowired.
*
* @param bool $autowired
* @param int $autowired
Copy link
Contributor

Choose a reason for hiding this comment

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

int|false?

Copy link
Member Author

Choose a reason for hiding this comment

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

int only, even if bools are accepted, that's only "by cast", which is fine even on php 7.

Copy link
Contributor

Choose a reason for hiding this comment

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

Imo we should either explicitly allow false or add a new constant, it feels weird to call $definition->setAutowired(0);.

Copy link
Member Author

@nicolas-grekas nicolas-grekas Mar 20, 2017

Choose a reason for hiding this comment

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

on php 7, adding just int would be fine, no BC break (in terms of behavior, not signature of course), and even then, false would be accepted.

Copy link
Member Author

Choose a reason for hiding this comment

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

turned to bool|int

@nicolas-grekas nicolas-grekas force-pushed the di-autorequired branch 4 times, most recently from d7fd5a6 to 59a4bf7 Compare March 20, 2017 22:44
@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Mar 20, 2017

I played with this PR on symfony-demo, I really like the taste it provides :)
I found some enhancements for exception messages: these now provide suggestions when the corresponding type is not found as an id.

A typical example is this one, displayed while building an incompletely defined container:

  [Symfony\Component\DependencyInjection\Exception\RuntimeException]           
  Cannot autowire argument $manager of method AppBundle\Form\Type\TagsInputTy  
  pe::__construct() for service "AppBundle\Form\Type\TagsInputType": Service   
  "Doctrine\Common\Persistence\ObjectManager" does not exist and it cannot be  
   auto-registered. This type could be aliased to the existing "doctrine.orm.  
  default_entity_manager" service.                                             

The resolution is easy, just add the following to services.yml:

    Doctrine\Common\Persistence\ObjectManager:
        '@doctrine.orm.default_entity_manager'

Of course, this alias should be provided by default by DoctrineBundle. By doing so, we would fix "by-id" autowiring immediatly, but we would also prevent any ambiguous "by-type" autowiring - by stating that "doctrine.orm.default_entity_manager" is the service to use by default when the "Doctrine\Common\Persistence\ObjectManager" type is wired.

A possible working resulting services.yml file for symfony-demo could be the following:

services:
    _defaults:
        public: false
        autowire: by_id

    _instanceof:
        Symfony\Component\EventDispatcher\EventSubscriberInterface:
            tags: [ kernel.event_subscriber ]

        Symfony\Component\Form\AbstractType:
            tags: [ form.type ]

        Twig_Extension:
            tags: [ twig.extension ]

    AppBundle\:
        resource: ../../src/AppBundle/{EventListener,Form/Type}

    AppBundle\Controller\:
        resource: ../../src/AppBundle/Controller
        public: true

    AppBundle\Twig\AppExtension:
        arguments: { $locales: '%app_locales%' }

    AppBundle\EventListener\CommentNotificationListener:
        arguments: { $sender: '%app.notifications.email_sender%' }

    AppBundle\EventListener\RedirectToPreferredLocaleListener:
        arguments:
            $locales: '%app_locales%'
            $defaultLocale: '%locale%'

    AppBundle\Security\PostVoter:
        tags: [ security.voter ]

    Twig_Extensions_Extension_Intl:
        class: Twig_Extensions_Extension_Intl

# The following aliases are required for now when using by-id autowiring,
# until these types are aliased in each core bundle, which they should be.
# This kind of explicit aliasing is the only difference between by-id and
# by-type autowiring in this file.

    Doctrine\ORM\EntityManager:
        '@doctrine.orm.default_entity_manager'

    Doctrine\Common\Persistence\ObjectManager:
        '@doctrine.orm.default_entity_manager'

    Swift_Mailer:
        '@swiftmailer.mailer.default'

    Symfony\Component\Routing\Generator\UrlGeneratorInterface:
        '@router.default'


<xsd:simpleType name="autowire">
<xsd:restriction base="xsd:string">
<xsd:pattern value="(true|false|by-type|by-id)" />
Copy link
Member

Choose a reason for hiding this comment

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

this does not allow parameters anymore, unlike boolean (see just above), so it is a BC break.

Copy link
Member Author

@nicolas-grekas nicolas-grekas Mar 21, 2017

Choose a reason for hiding this comment

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

actually I wanted to trigger this discussion :)
so: in Definition::setAutowired, there is a cast to bool currently.
This means that parameters never worked, did they?
Adding %.+% to the regexp might fix the BC by still allowing one to pass a param,
but it won't fix the fact that this param is ignored.
Or did I miss something?
Btw, this is the case for all definition setters that "cast" - and has never been an issue really (autowiring/abstract/etc. should not be parametric.)

Copy link
Member

Choose a reason for hiding this comment

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

if it never worked, this is fine for me.

@stof
Copy link
Member

stof commented Mar 21, 2017

for the url generator, please add the alias as this has to be done in FrameworkBundle

@@ -257,9 +258,9 @@ private function autowireMethod(\ReflectionMethod $reflectionMethod, array $argu
continue;
}

$typeName = InheritanceProxyHelper::getTypeHint($reflectionMethod, $parameter, true);
$type = InheritanceProxyHelper::getTypeHint($reflectionMethod, $parameter, true);
Copy link
Member

Choose a reason for hiding this comment

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

As all these renamings are already merged as part of another PR now, can you rebase your branch to make the diff more readable ?

Copy link
Member Author

Choose a reason for hiding this comment

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

rebased

@nicolas-grekas nicolas-grekas force-pushed the di-autorequired branch 2 times, most recently from 36d76e9 to ee07c1b Compare March 21, 2017 15:23
@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Mar 21, 2017

First commit now split as separate PR, adding logging to AutowirePass, see #22095.
This made me realize that by-id autoregistration has a high chance of creating a duplicate service when an alias is missing. Better encourage writing explicit aliases.
PR updated accordingly. Still very useful to me to help devs wire things explicitly but progressively.

@nicolas-grekas nicolas-grekas changed the title [DI] Add side-effect-restricted variant for autowiring, based solely on ids [DI] Add "by-id" autowiring: a side-effect free variant of it based on the class<>id convention Mar 21, 2017
@nicolas-grekas nicolas-grekas force-pushed the di-autorequired branch 3 times, most recently from 241de74 to 7b80c02 Compare March 21, 2017 21:52
fabpot added a commit that referenced this pull request Mar 21, 2017
…nicolas-grekas)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[*Bundle] Add autowiring aliases for common services

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

As spotted while working on #22060, we're missing many aliases to prevent any autowiring ambiguities.
I also removed the "Symfony\Component\EventDispatcher\EventDispatcher" and "Symfony\Component\DependencyInjection\Container" aliases: we'd better encourage using the corresponding interfaces instead.
On ControllerTrait, we need to type hint against SessionInterface, because otherwise, when session support is disabled, autowiring still auto-registers an "autowired.Session" service, which defeats the purpose of being able to enable/disable it.

Commits
-------

08c2ee3 [*Bundle] Add autowiring aliases for common services
@nicolas-grekas
Copy link
Member Author

Exception message enhanced a bit more, with suggestions for already aliased types:

    [Symfony\Component\DependencyInjection\Exception\RuntimeException]             
    Cannot autowire argument $entityManager of method AppBundle\EventListener\C    
    heckRequirementsSubscriber::__construct() for service "AppBundle\EventListe    
    ner\CheckRequirementsSubscriber": Service "Doctrine\ORM\EntityManager" does    
     not exist. This type could be aliased to the existing "doctrine.orm.defaul    
    t_entity_manager" service. This service is already aliased to "Doctrine\ORM    
    \EntityManagerInterface".                                                      

@weaverryan
Copy link
Member

Hi guys!

Let me summarize the new mode! This mode no longer looks through the system and tries to find services that match the type-hint. Instead, it's much simpler: it only looks to see if a service id exists that matches the type-hint. Otherwise, it throws an exception: it does not try to automatically create an autowired private service for that class (if it's a class).

I like this! (but I don't like having 2 modes). It doesn't work how I expect autowiring to work (i.e. finding services matching a type-hint), but it's perfectly straightforward. Also, if you take the other mode to an extreme, you would have zero services explicitly wired (other than controllers) because the type-hints in classes would cause autowiring to auto-create services. But with this mode, we're saying: all services must be explicitly wired (while in reality for DX, the PSR-4 loader helps a lot here). That feels a bit better: I will be able to see in my services.yml file why every service exists.

Anyways, I like this... but I don't like the 2 modes. I don't mean we should remove the old one. Instead, if we accept this, it should be with the intention that this becomes the recommended mode for autowiring.

Cheers!

} elseif (isset($this->ambiguousServiceTypes[$type])) {
$message = sprintf('Service "%s" does not exist. This type could be aliased to any of these existing services: "%s"', $type, implode('", "', $this->ambiguousServiceTypes[$type]));
} elseif (isset($this->types[$type])) {
$message = sprintf('Service "%s" does not exist. This type could be aliased to the existing "%s" service', $type, $this->types[$type]);
Copy link
Member

Choose a reason for hiding this comment

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

It's perhaps outside the scope of this PR, but we could also add this "alias" recommendation to the "other" mode, when there are multiple definitions (

throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s". Multiple services exist for this %s (%s).', $type, $id, $classOrInterface, $matchingServices));
)

}
if ($aliases) {
$message .= sprintf('. This service is already aliased to "%s"', implode('", "', $aliases));
}
Copy link
Member

Choose a reason for hiding this comment

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

What case is this catching? I don't see anything in AutowirePassTest for this.

Copy link
Member Author

Choose a reason for hiding this comment

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

See my previous comment, last sentence of the block:
#22060 (comment)
This PR has no tests yet :)

Copy link
Member

Choose a reason for hiding this comment

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

Ah, thank you! What about:

. Or you can update your type hint to "%s".

(but, if we did this, we probably need some extra logic to make the sentence read correctly if there are multiple $aliases):

. Or you can update your type hint to be one of the following: "%s".

Copy link
Member Author

Choose a reason for hiding this comment

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

@theofidry
Copy link
Contributor

Wholeheartedly agree with @weaverryan 😄

@nicolas-grekas
Copy link
Member Author

Test case added, failure unrelated. Ready.

fabpot added a commit that referenced this pull request Mar 24, 2017
…icolas-grekas)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[DI] Add hints to exceptions thrown by AutowiringPass

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

As spotted while working on #22060

Commits
-------

d7557cf [DI] Add hints to exceptions thrown by AutowiringPass
@fabpot
Copy link
Member

fabpot commented Mar 25, 2017

Thank you @nicolas-grekas.

@fabpot fabpot merged commit c298f2a into symfony:master Mar 25, 2017
fabpot added a commit that referenced this pull request Mar 25, 2017
…t of it based on the class<>id convention (nicolas-grekas)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[DI] Add "by-id" autowiring: a side-effect free variant of it based on the class<>id convention

| 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 PR adds a new autowiring mode, based only on the class <> id convention.
This way of autowiring is free from any conflicting behavior, which is what I was looking for to begin with.

The expected DX is a bit more involving than the current way we do autowiring. But it's worth it to me, because it's plain predictable - a lot less "magic" imho.

So in this mode, for each `App\Foo` type hint, a reference to an "App\Foo" service will be created. If no such service exists, an exception will be thrown. To me, this opens a nice DX: when type hinting interfaces (which is the best practice), this will tell you when you need to create the explicit interface <> id mapping that is missing - thus encourage things to be made explicit, but only when required, and gradually, in a way that will favor discoverability by devs.

Of course, this is opt-in, and BC. You'd need to do eg in yaml: `autowire: by_id`.
For consistency, the current mode (`autowire: true`) can be configured using `autowire: by_type`.

Commits
-------

c298f2a [DI] Add "by-id" autowiring: a side-effect free variant of it based on the class<>id convention
@nicolas-grekas nicolas-grekas deleted the di-autorequired branch March 25, 2017 20:19
sustmi added a commit to sustmi/symfony that referenced this pull request Mar 25, 2017
…c9dc7)

This is done to backport DIC from Symfony 3.3 to Symfony 3.2.6.
It is temporary workaround as we want to use Symfony 3.2.6 but also features like "by-id" autowiring (symfony#22060).
After Symfony 3.3 will be released and we migrate to it we can get rid of this "blend-commit".
@jsamouh
Copy link
Contributor

jsamouh commented Mar 26, 2017

@nicolas-grekas , you think you update the doc soon ^^ ? need it to prevent my partner using this implementation when autowire fails causing by service definition priority.
thks for all and good work 👍

@curry684
Copy link
Contributor

curry684 commented Mar 26, 2017

http://symfony.com/blog/new-in-symfony-3-3-configurable-autowiring-setters is still live without changes, which I was trying last Friday before this PR got merged, and cost me a lot of time as autowire: [ setX ] was still accepted at the time in service config but not doing anything at all. Current master throws an error on that thankfully, but the blog announcement is still up.

edit: removed by now

@dunglas
Copy link
Member

dunglas commented Apr 3, 2017

Is this new mode still necessary now that we have #22254? IIUC, they fix almost the same issue (multiple implementations of an interface). For classes without interfaces, the default mode will less likely fail and is faster/easier to use from a dev POV.

Anyway, this new mode can also be useful in some cases (e.g. the domain layer - it's an intermediate between 100% manual wiring and the "real" autowiring mode) but IMO it should not be the default.

fabpot added a commit that referenced this pull request Apr 5, 2017
…g reflection against all existing services (nicolas-grekas)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[BC BREAK][DI] Always autowire "by id" instead of using reflection against all existing services

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

(patch best reviewed [ignoring whitespaces](https://github.com/symfony/symfony/pull/22295/files?w=1).)

"By-id" autowiring, as introduced in #22060 is free from all the issues that "by-type" autowiring has:
- it has no magic and requires explicit type<>id matching (*vs* using reflection on all services to cherry-pick *the* one that matches some type-hint *at that time*, which is fragile)
- it is free from any ambiguities (*vs* the Damocles' sword of breaking config just by enabling some unrelated bundle)
- it is easily introspected: just look at DI config files (*vs* inspecting the type-hierarchy of all services + their type-hints)
- ~~it is side-effect free, thus plain predictable (*vs* auto-registration of discovered types as services)~~
- it plays nice with deprecated services or classes (see #22282)
- *etc.*

Another consideration is that any "by-type" autowired configuration is either broken (because of future ambiguities) - or equivalent to a "by-id" configuration (because resolving ambiguities *means* adding explicit type<>id mappings.) For theoreticians, we could say that "by-id" autowiring is the asymptotic limit of "by-type" autowiring :-)

For all these reasons - and also because it reduces the complexity of the code base - I propose to change the behavior and only support "by-id" autowiring in 3.3. This will break some configurations relying on "by-type" autowiring. Yet the break will only happen at compile-time, which means this won't *silently* break any apps. For all core Symfony services, they will work out of the box thanks to #22098 *et al.* For the remaining services, fixing ones config should be pretty straightforward: just follow the suggestions provided by the exception messages. If they are fine to you, you'll end up with the exact same config in the end. And maybe you'll spot issues that were hidden previously.

Commits
-------

cc5e582 [BC BREAK][DI] Always autowire "by id" instead of using reflection against all existing services
@fabpot fabpot mentioned this pull request May 1, 2017
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.