Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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

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
Apr 5, 2017

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Apr 5, 2017

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 symfony/symfony-docs#7706

(patch best reviewed ignoring whitespaces.)

"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 [DI] Prevent AutowirePass from triggering irrelevant deprecations #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.

@fabpot
Copy link
Member

fabpot commented Apr 5, 2017

👍

@dunglas
Copy link
Member

dunglas commented Apr 5, 2017

It breaks the main idea of autowiring and the DX improvement it provides:

currently: create a Sluggerclass in src/, type-hint it in a (autowired) controller, it works with 0 config.
"by id": you need to register manually all classes you want to type-hint, worse DX. The psr-4 loader may help, but if it means registering automatically all classes in src/, it's worst than the current solution.

Constructing automatically the object graph is what make autowiring amazing to use.

But maybe am I missing something?

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Apr 5, 2017

The list of pros is quite sexy to me, fixes lots of code smell (eg #22282 being the last one.)

On the DX side, it feels really good: Yes, "by-id" autowiring will require you to add explicit aliases/services for the services in your namespace. But most bundles will ship with default aliases that will just work (as already done by SF core).

For those services in your namespace, the DX is really improved vs full manual wiring:

  • you start by declaring some service (eg via psr4 loading).
  • compilation fails because a required type-hint misses a corresponding service (DX improvement 1. vs manual wiring)
  • you read the exception message that you just got and you learn that some existing service(s) already ship(s) the FooInterface that you hinted for (DX improvement 2. vs manual wiring)
  • you decide to alias some service for this interface by yourself (DX improvement 3. vs "by-type" autowiring: you take full responsibility for wiring)
  • iterate until compilation succeeds
  • done

So, DX wise, "by-id" autowiring is "compiler-assisted explicit wiring". I already experienced it, and I want it everywhere :)

It breaks the main idea of autowiring and the DX improvement it provides

To me, the most important promise of autowiring is: tell me you need a LoggerInterface, I'll ship you one.
"By-id" autowiring does just that. So it's still a huge DX improvement over manual wiring. But freed from "by-type" downsides.

create a Sluggerclass in src/, type-hint it in a (autowired) controller, it works

It feels it works, but it can also break, because of ambiguities, which are waiting around the corner. Then DX is badly hurt.

the psr-4 loader may help, but it means registering automatically all classes in src/

That would certainly be a bad idea indeed!

@dunglas
Copy link
Member

dunglas commented Apr 5, 2017

To me, the most important promise of autowiring is: tell me you need a LoggerInterface, I'll ship you one.
"By-id" autowiring does just that. So it's still a huge DX improvment over manual wiring. But freed from downsides.

I'm not arguing against the by-id feature. I'll definitely use it in some cases. But it's not the same thing than the current autowiring, and IMO it should complement it, not replace it.

What I want when using autowiring is to only write PHP. It's basically a tool for prototyping and RAD. What I want is creating a Symfony app as fast possible without having to write config files. The steps you describe are really interesting in some cases, but it breaks this "config-less" experience we already have (when using ActionBundle fi). IMO it's a regression. Having both modes is better because it lets the developer choose how he want to code its app instead of forcing it to do some work that is currently not necessary.

It feels it works, but it can also break, because of ambiguities, which are waiting at the corner.

Yes, and it's intended when using the standard autowiring. It cannot breaks at runtime, only at compile time, and when it breaks, it's time to move to explicit (or - now - by id wiring).

Btw, I would suggest to change autowire: by-id/autowire: by-type by wire: auto and wire: by-id because the by-id mode is not really automatic anymore.

@ro0NL
Copy link
Contributor

ro0NL commented Apr 5, 2017

Big 👍 for no-questions-asked config, i.e. autowire: true|false. I personally dont want to make any choice here in terms of which "mode" should i pick; just autowire the service, or tell me if you cant. The only alternative should be explicit/manual wiring, IMO :)

Given that.. we could still improve a single type match right, creating the alias automatically when This type-hint could be aliased to the existing "x" service. occurs?

@stof
Copy link
Member

stof commented Apr 5, 2017

@nicolas-grekas if our typehint is a class rather than an interface, would it still require registering a service explicitly ? This is what makes the experience harder for simple cases like the Slugger above. All your explanations about by-id wiring always talk abut interface typehints.

@ro0NL creating the alias automatically when there is no ambiguity is what happens in the by-type matching (among other things). And this is what breaks often: as soon as you register a second service matching the type (for a totally unrelated feature), the automatically-added alias is not added anymore and things blow up. And this second service might come from elsewhere than your own code (adding a new bundle, etc...).

@ro0NL
Copy link
Contributor

ro0NL commented Apr 5, 2017

the automatically-added alias is not added anymore and things blow up.

Yes; in case we fallback to the process described by @nicolas-grekas #22295 (comment)

I understand it sort of brings back the original issue; but on the other hand, it's truly convenient :) most project src dirs i've seen practically contain nothing but unique type services :)

@dunglas
Copy link
Member

dunglas commented Apr 5, 2017

@ro0NL it's why I'm against dropping the original feature :)

@ro0NL
Copy link
Contributor

ro0NL commented Apr 5, 2017

Then again... Some\FQCN: ['@arg'] will probably become a very common notation for those cases... fixing it out-of-the-box as well.

Guess letting the user updating some service id to a FQCN is a better approach, compared to SF creating an alias for your "FQCN to id convention".. which i've also seen thousands of :) causing very cluttered configs.

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Apr 5, 2017

if our typehint is a class rather than an interface, would it still require registering a service explicitly

yes, that's what I mean by being side-effect free (and call "auto-registration" this "by-type" behavior).

take this Slugger example: if no service has that name, then auto-registering this class as a Slugger service is a maybe decision. The service could very well exist under the "slugger" id. No-go.

We could implement something in between if this is the corner stone of the discussion.
We could have this logic:

  • when Slugger is not found
  • then look at all existing services
  • if none exists with that class in its type hierarchy, then auto-register Slugger
  • otherwise (if at least one exists), throw with a suggestion (as already done in "by-id")

That would not be a strict as I'd like it to be - but at least that would fix most of the issues we have with "by-type" (but it would still be "magic" by requiring some container-wide introspection - meaning will lead to unwanted side-effects at some point).

@dunglas
Copy link
Member

dunglas commented Apr 5, 2017

We could implement something in between if this is the corner stone of the discussion.
We could have this logic:

when Slugger is not found
then look at all existing services
if none exists with that class in its type hierarchy, then auto-register Slugger
otherwise (if at least one exists), throw with a suggestion (as already done in "by-id")

Give me some time to double-think about it, but it looks like a nice compromise. Will this behavior be recursive (e.g, will FooController -> MyApiClient -> MyRestClient works with no config if there is only 1 type available in the hierarchy for each class)?

@nicolas-grekas
Copy link
Member Author

As I see it, this would be recursive yes, same as today (ie these would private + autowired).

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Apr 5, 2017

Oh, I see just one difference that I'd like to put in the compromise: as soon as a type is optional, we should not auto-register.

@chalasr
Copy link
Member

chalasr commented Apr 5, 2017

I'd always use explicit binding aka current by-id or "define things once and profit while keeping control", #22282 was really painful to debug and e.g. #21351 would have been really hard to achieve using the current by-type, that's where time winning ends to me and I think end users shouldn't be bothered by such issues.
Using the current by-type as a last resort looks like a good compromise indeed, that's how laravel behaves I believe: if no explicit binding exists then try to guess it. IMHO there should be no choice to do, only 1 behavior performing some different strategies to achieve one final goal: wiring the service, which is the only thing I need as an user.

@dunglas
Copy link
Member

dunglas commented Apr 5, 2017

Oh, I see just one difference that I'd like to put in the compromise: as soon as a type is optional, we should not auto-register.

Ok for me, if there is no alias defined, ?Foo should be wired but ?Foo = null should not.

Using the current by-type as a last resort looks like a good compromise indeed, that's how laravel behaves I believe: if no explicit binding exists then try to guess it. IMHO there should be no choice to do, only 1 behavior performing some different strategies to achieve one final goal: wiring the service, which is the only thing I need as an user.

I agree, and we need to make it clear in the doc that autowiring is for simple cases only. For advanced uses, you must use explicit wiring (this feature as always been designed with this in mind).

@nicolas-grekas
Copy link
Member Author

PR updated, autoregistration is back, with the following logic:

  • when a Slugger type-hint has no corresponding service
  • when also the related argument is required
  • then look at all existing services
  • if none exists with that class in its type hierarchy, then auto-register Slugger
  • otherwise (if at least one exists), throw with a suggestion

@robfrawley
Copy link
Contributor

Personally, I only use "by-id" for two of the reasons @nicolas-grekas already pointed out, and therefore have no issue with the removal of "by-type" completely:

  • 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)

The middle-ground approach recently discussed is also fine, but again, I will only ever use "by-id" myself. ;-)

@fabpot
Copy link
Member

fabpot commented Apr 5, 2017

Thank you @nicolas-grekas.

@fabpot fabpot merged commit cc5e582 into symfony:master Apr 5, 2017
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
@antanas-arvasevicius
Copy link
Contributor

Yes I know that I can fix this by explicitly adding FQCN services, but if we'll need to do that everytime then we create a a non autowired service and just need to mark that with FQCN service to let that service be autowired in the future, it's just as hard as wiring everything manually, don't see much benefit from doing so.
I understand if it will be required only for bundles or found ambiguities, but for regular use case it just too verbose

@nicolas-grekas
Copy link
Member Author

@dunglas that would only "fix" autoregistered services, but would not change anything to the session_manager example, which requires an alias yes.

@nicolas-grekas
Copy link
Member Author

@antanas-arvasevicius the verbosity can be removed by renaming the service to the FQCN - and get rid of any alias.

@antanas-arvasevicius
Copy link
Contributor

@nicolas-grekas so for every manual registered service, as "session_manager" we'll need to add FQCN alias service to let that service be autowired to any?

@antanas-arvasevicius
Copy link
Contributor

yes, the point that "session_manager" is sometimes used not in autowired schema, so using everythere FQCN looks overhead.

@antanas-arvasevicius
Copy link
Contributor

as I understand the pattern for everything is as such:

  1. define a new service id X, with class: Y
  2. create new service with a name Y, alias: X

to let autowire for these services

@dunglas
Copy link
Member

dunglas commented Apr 10, 2017

@antanas-arvasevicius yes

@nicolas-grekas
Copy link
Member Author

or, using the class<>id convention in place in 3.3

  1. create a service with id FQCN-X, no class

class will be set to "id"

@antanas-arvasevicius
Copy link
Contributor

Understood, but then it's hard to use good old style manual service registration with proper nice ID with new autowire logic where you must create (or rename) FQCN-X service names or alias with your manually created services to get autowire working.
I think service with id FQCN-X is a best way for now, just manual wiring will not look so good with @long-FQCN-strings
Will change our sources to your suggested variants, but still not clear what's the benefits for local private source codes with these restrictions then 1:1 mapping can be resolved without additional metainfo.

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Apr 10, 2017

@antanas-arvasevicius yep, FQCN-ids are longer. But now that autowiring is predictible, you can almost always enable it for all services, and let that boring work be done by the framework (ie use _defaults)

@antanas-arvasevicius
Copy link
Contributor

Hello,
some notes on this, it's problematic with JmsDiExtraBundle then services are registered using @Service annotation, now we need annotated with full FQCN instead of just @Service()
or manually add alias into services.yml

I've created small compiler pass to workaround these restrictions.
You just need to register this compiler pass to your AppBundle and specify namespaces which is "whitelisted" (e.g. your internal/private/trusted namespaces)
code can be found here: https://pastebin.com/UqjjMVdZ

What's your thoughts about this "workaround"? Maybe that should be added to Symfony core?

@nicolas-grekas
Copy link
Member Author

@antanas-arvasevicius this autoalias pass basically reverts to the behavior we'd like to get rid of (is reintroduces the issue with ambiguities). Maybe you should open a PR on JmsDiExtraBundle so that the default name of services created by @Service annotation matches the FQCN of the class?

@mvrhov
Copy link

mvrhov commented Apr 11, 2017

@nicolas-grekas: This is exactly the same thing I've been fighting against for when service_id==class was implemented. Everybody was assuring me that nothing prevents me from using my own service ids. And now there is a feature that forces me to use exactly this. Where everything I want to have aoutwired is mostly controllers

@antanas-arvasevicius
Copy link
Contributor

@nicolas-grekas no, I think "by-id" autowire is great, but it's great only for "bundles" and library vendors, our own internal, non publishable source code won't have these "side-effects" as have had a "bundles" with previous "by-type" implementation. So I think "whitelisting" namespaces which by default would work "by-type" without strict restriction would be OK.

@nicolas-grekas
Copy link
Member Author

I understand the expectation but I can't find a satisfactory way to circumvent the drawbacks listed in the description of this PR - even for internal code. This would be just allowing things to be fragile again - for your code only yes, but still looking like a foot gun. We should not help build such things IMHO.

@antanas-arvasevicius
Copy link
Contributor

I don't see any reasonable use case how we could fire from autowire "by-type" in our internal code. Examples:
a) if someone adds new realization of interface X which was autowired by someone then the exception about ambiguity will be thrown, we'll then just add an alias or manually wire specific services. Actually if it will be two implementations of interface X and couple of services will need different implementations we'll just need to wire it manually, aliasing won't work at all.

b) Actually in internal code all wiring between services is done with concrete classes not with an interfaces at all, create interfaces for internal code where only one specialization exists is just over-engineering with no reason.

But from "bundles" perspective I agree with "by-id" benefits, because then someone in some new bundle will introduce service which implements some (e.g. "TranslatorInterface") service and wiring logic will go apart.

@kiler129
Copy link
Contributor

kiler129 commented Apr 11, 2017

@nicolas-grekas I highly respect your work and I personally think current direction of changes in DIC is really fantastic, however such a huge BC break (and in fact functionality REMOVAL) is unacceptable.

Yesterday I created a branch of our enterprise system, very large one, which uses Symfony DependencyInjection component and I'm scarred and terrified how to approach changes made in 3.3. While I think FQCNs are great I cannot just suddenly switch since a lot of services, due to nature of legacy code, are manually fetched from container all over the code and new code gradually moves towards injection. What does it mean for me (and probably a lot of other devs)? I literally need to spend hours and create enormous clutter in config files converting something like this:

CacheTagsFactory:
    class: Foo\Bar\CacheTagsFactory
    #....

into

CacheTagsFactory:
    class: Foo\Bar\CacheTagsFactory
    #....

Foo\Bar\CacheTagsFactory:
    alias: CacheTagsFactory

Alternative solution you recommended works only in theory, since usually services do exist and thus they will not be autowired now. I will be able to swallow such change if at least adding simple alternative tag would be possible, but currently I need to create fake empty service... which creates another headache: tags.

We use tags to denote some internal relations. There's a compiler pass which looks at these tags and determines if service should be available - if not service is removed and information about that is saved. Not going into details using aliases to solve issue created by this PR will also break that part of our system, since container will be created with Foo\Bar\CacheTagsFactory service pointing to non-existing CacheTagsFactory.

I'm always positive about changes, we should move forward and make code better, but sometimes we need to take baby steps on certain changes. In my opinion this change should be made as optional in 3.3 and by-type injection should be removed in 4.0.

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Apr 11, 2017

@kiler129 I don't get the issue with tags, how does that relate to this change? Aliases do not change anything to the existing tags you have, do they?

@kiler129
Copy link
Contributor

@nicolas-grekas: No, I just mentioned that as an example where creating alias is not just an easy fix. My main point is about polluting the config and actually making things less obvious - I absolutely have no problem with how new DIC works, but with the fact old way of searching for services was simply ripped off the code with no [clean] way of grace period for migration.
I really don't want to be stuck at 3.2 only due to that change or implement weird hacks for the time until all calls get replaced with new FQ names. It's hard for me to say that, but in some systems where DIC was retrofitted it may never be done.

@nicolas-grekas
Copy link
Member Author

Proposal to replace the BC Break by a regular deprecation in #22384

fabpot added a commit that referenced this pull request Apr 12, 2017
…n (nicolas-grekas)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[DI] Replace autowiring BC break by regular deprecation

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

It happens that doing so is really easy now. And as discussed in #22295, this should be more friendly to our users.

Commits
-------

0ed087d [DI] Replace autowiring BC break by regular deprecation
@robfrawley
Copy link
Contributor

@nicolas-grekas I thought the whole point of using the new "experimental" tag was to allow for things to break without having to wait for the next major releases?

@chalasr
Copy link
Member

chalasr commented Apr 13, 2017

@robfrawley Only newly introduced features can be marked as experimental and potentially be removed in the future, that's only about future additions. Flagging existing features as experimental must not happen since we did promise to keep BC on them by introducing them without any flag.

@robfrawley
Copy link
Contributor

I thought the autowiring component was marked as experimental ATM. Must have been wrong, though.

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.

Morty Proxy This is a proxified and sanitized view of the page, visit original site.