Buy

Autowiring & Public/Private Services

Head back to services.xml: there are a few really important details we need to get straight.

Best-Practice Service IDs

First, in our applications, we usually make the service id match the class name for simplicity: and that's what we've done here. But, when you create a re-usable bundle, the best practice is to use snake-case service id's. Change the key to class and add id="knpu_lorem_ipsum.knpu_ipsum".

11 lines lib/LoremIpsumBundle/src/Resources/config/services.xml
... lines 1 - 6
<services>
<service id="knpu_lorem_ipsum.knpu_ipsum" class="KnpU\LoremIpsumBundle\KnpUIpsum" />
</services>
... lines 10 - 11

Why is this the best practice? Well, the user could in theory change the class of this service to one of their own classes. And, it would be pretty weird to have a service called KnpU\LoremIpsumBundle\KnpUIpsum... when that's not actually the class of the service.

Supporting Autowiring

Anyways, this simple change, totally borks our app! Woohoo! Refresh!

It once again says that no service exists for KnpUIpsum. Remember: we're autowiring that class into our controller. And in order for autowiring to work, there must be a service whose id matches the class used in the type-hint. By changing the id from the class to that weird, snake-case string, we just broke autowiring!

No worries: we can solve this with a service alias. First, identify each service in your app that you intend to be used directly by the user. Yea, I know, we only have one service. But often, a bundle will have several services, but only some of them are meant to be accessed by the user: the others are just meant to support things internally.

For each "important" service, define an alias: <service id="" ...> and paste in the class name. Then, alias="" and type the first service's id: knpu_lorem_ipsum.knpu_ipsum.

13 lines lib/LoremIpsumBundle/src/Resources/config/services.xml
... lines 1 - 6
<services>
... lines 8 - 9
<service id="KnpU\LoremIpsumBundle\KnpUIpsum" alias="knpu_lorem_ipsum.knpu_ipsum" />
</services>
... lines 12 - 13

To see what this did, move over to your terminal and run:

php bin/console debug:container --show-private knpu

Ok, there are two services: one has the snake-case id and the other is the full class name. If you choose the second, it's just an alias to the snake-case service. But now that there is a service whose id is the class name, anyone can once again autowire using that type-hint. This fixes our page.

Yep, in ArticleController, the KnpUIpsum class is once-again autowired.

Public versus Private Services

Ok, there is one last thing you need to think about when setting up your services: whether or not each service should be public or private. In Symfony 4.0, services are private by default, which means that a user cannot fetch a service directly from the container with $container->get() and then the service's id. Instead, you need to use dependency injection, which includes autowiring.

And this is really the way people should code going forward: we really should not need services to be public. But, since some people still do fetch services directly, you may want to make your important services public. Let's do this: public="true".

13 lines lib/LoremIpsumBundle/src/Resources/config/services.xml
... lines 1 - 7
<service id="knpu_lorem_ipsum.knpu_ipsum" class="KnpU\LoremIpsumBundle\KnpUIpsum" public="true" />
... lines 9 - 13

And even though services are private by default, you should also add public="false" to the others. This will make your services also behave the same on Symfony 3, where they are public by default.

13 lines lib/LoremIpsumBundle/src/Resources/config/services.xml
... lines 1 - 9
<service id="KnpU\LoremIpsumBundle\KnpUIpsum" alias="knpu_lorem_ipsum.knpu_ipsum" public="false" />
... lines 11 - 13

This makes no difference in our app - it all still works.

Alright! With our services configured, let's talk about how we can allow the user to control the behavior of those services via configuration.

Leave a comment!

  • 2018-04-12 Etienne Lp

    Hey weaverryan

    Thanks for the reply, yeah it makes sense !
    I just wanted your opinion on that :)

    Cheers !

  • 2018-04-11 weaverryan

    Hey Etienne Lp!

    Sorry for my late reply to such a good question! Let me start by clarifying one thing: in new bundles.... yes... probably you should have NO public services anymore. However, I'm not sure yet how if it's too soon to "force" this in the community. But, my instinct (even though I did something a bit different here) is yes, to make everything private for *new* bundles.

    This means that, indeed, it's a good idea to keep public="false" for the snake-case service. About the alias, this SHOULD be public: it's *only* purpose is to provide autowiring (which doesn't require it to be public). Of course, IF you DID want to continue making one service public, I suppose you could make this be the alias... I hadn't really thought about it. But yea, why not? :)

    Cheers!

  • 2018-04-04 Etienne Lp

    Hey,
    Thanks a lot for this tutorial !

    Just one detail, at the end of the video, could you let me know if it was intented that you put `public="true"` to the snake-case service (the first one) and `public="false"` to the alias ?

    I mean, wouldn't it be better to make the alias public instead, and let the other one private ?

    Sorry for my english, i hope you will understand.
    Have a nice day !

  • 2018-04-04 weaverryan

    Hey Adam Hazda!

    Great reply. So yea... you obviously understand the whole situation very well. There are just a FEW edge cases that makes using class/interface name service id's everywhere a problem... and so, we basically don't recommend them. But, those are for sure edge cases!

    About the autowiring point (A) from above, another way to think about it is this: if my class registers 5 services, but only 1 is meant to be used by the end-user, by using snake-case (and only creating a class name / interface name alias for the *1* service), then when a user runs bin/console debug:autowiring, they will only see the 1 service there, not all 5. I think that's important: I want debug:autowiring to be as short as possible - showing the user only the important stuff :).

    Anyways, great convo! I hadn't really ever thought through the repercussions of class/interface ids in reusable stuff before.

    Cheers!

  • 2018-03-29 Adam Hazda

    Hey Ryan, thanks for your Response! Your first answer makes complete sense! But I am still I little bit confused about the second topic. (B) I see your concernes on service name clashing. For the sake of service name uniqueness I suppose it would be just fine to use FQNs (of classes or interfaces) instead of some made up snake case names. I also suppose that every class is behind some kind of namespace that includes vendor name and bundle name that make it unique, which is the point of service id? Just a string of unique and referenceable characters carrying some hint about it's usage and vendor, I guess?. Now (A),.. I get your point, maybe I just can't imagine the real case. If a consumer of my bundle typehints some of his argument with my internal Interface I guess it's okay to have it autowired and injected inside his service. I can imagine though, that registering my internal service with some really general id like Psr\Log\LoggerInterface might invoke some unexpected behavior.
    I just really seem to hate snake case IDs you know... 😀. Class names are like constants. You can't really screw it up, even without some fancy Symfony PHPStorm plugin handling snake case IDs.

    Cheers!

    Update:

    I just realized that I didn't really take into consideration the part you said about *external library* and you're right. Maybe if I let's say,.. needed a different EventDispatcher, as you mentioned before, for my bundles internal workings then I have two possibilities I can think of: 1, I can completely reuse the class from Symfony, but I just need it to be assembled (compiled) differently, maybe with some different method calls (I need to make up some ID here) 2, I can either extend the class or decorate it, which would mean I want to add or modify some of the functionality, which would possibly lead to a new Interface I then use as an ID.
    I would still probably go for the new Interface anyway 😀.

  • 2018-03-27 weaverryan

    Yo Adam Hazda!

    Sorry for my slow reply :). I love questions! And these are good ones!

    > why is it that .xml format is the best practise for service definitions?

    A LONG time ago, we made this decision on the core team: YAML for applications as a best practice, XML for shareable stuff. Back then, there were a few edge-case things you could do in XML that you could not do in YAML, but I'm 99% sure that is not the case anymore. So.... it really doesn't matter :). The *one* edge thing I can think of is this: if you use YAML, then your bundle is dependent on the YAML component. And, in theory, some apps could be built without using YAML, and they may not love that your bundle requires it to be installed. Pretty minor reason, but there ya go.

    And PHP? Yea, you could totally just use PHP! I think most people don't think that the PHP service definition is *as* readable, but obviously, there is zero technical disadvantage to it.

    > The second question is about service IDs. Provided that our classes depend on abstractions, not other concrete classes AAAND provided that we do not use autowiring, instead we are in full control of what is really being injected into our classes (inside our Bundle), why do we need to use snake_case_ids? Wouldn't it be nice to use Interface names instead?

    This is a pretty new conversation that we're having. Snake case id's were always used *everywhere* (apps & bundles) until Symfony 3.3, when we started using class ids for applications. It's an interesting question to use interface names as service id's. I can make a pretty good case for not using the *class* names as the service ids, but interface names is definitely more interesting :). In practice, I think there's not a big problem. But, I can see *potential* issues: (A) if you have services that aren't meant to be used by end-users, then by using the interface as the id, you've enabled that service to be autowired (and it will be displayed in debug:autowiring). You definitely don't want that, so this kills the idea of interface ids for at least your "internal" services. (B) More minor, but it's possible that 2 different bundles will want to register a service for some external library (e.g. maybe a bundle wants to register their own event dispatcher to be used only by the bundle). Using the snake case allows you to give the service id a unique name to avoid clashing.

    I hope these answers are satisfying. If you've got more, let me know!

    Cheers!

  • 2018-03-23 Adam Hazda

    Hey Ryan! Thank you for this video tutorial. I would like to ask a few questions if you don't mind :). The first one is really easy, why is it that .xml format is the best practise for service definitions? I simply do not get it. I used to disagree with .yaml. But then I said ok fine,... at least it's quite readable. But if there is some format or a language i like to define services with,.. it would be PHP. You don't have to parse PHP notation in PHP,.. well,. because it's PHP. The second question is about service IDs. Provided that our classes depend on abstractions, not other concrete classes AAAND provided that we do not use autowiring, instead we are in full control of what is really being injected into our classes (inside our Bundle), why do we need to use snake_case_ids? Wouldn't it be nice to use Interface names instead?

    I am really looking forward to have them questions answered :)

    Have a great day.