Buy
Buy

services.yaml & the Amazing bind

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

When Symfony loads, it needs to figure out all of the services that should be in the container. Most of the services come from external bundles. But we now know that we can add our own services, like MarkdownHelper. We're unstoppable!

All of that happens in services.yaml under the services key:

... lines 1 - 4
services:
... lines 6 - 32

This is our spot to add our services. And I want to demystify what the config in this file actually does:

... lines 1 - 4
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
public: false # Allows optimizing the container by removing unused services; this also means
# fetching services directly from the container via $container->get() won't work.
# The best practice is to be explicit about your dependencies anyway.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/*'
exclude: '../src/{Entity,Migrations,Tests}'
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
... lines 25 - 32

All of this - except for the MarkdownHelper stuff we just added - comes standard with every new Symfony project.

Understanding _defaults

Let's start with _defaults:

... lines 1 - 4
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
public: false # Allows optimizing the container by removing unused services; this also means
# fetching services directly from the container via $container->get() won't work.
# The best practice is to be explicit about your dependencies anyway.
... lines 13 - 32

This is a special key that sets default config values that should be applied to all services that are registered in this file.

For example, autowire: true means that any services registered in this file should have the autowiring behavior turned on:

... lines 1 - 4
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
... lines 9 - 32

Because yea, you can actually set autowiring to false if you want. In fact, you could set autowiring to false on just one service to override these defaults:

services:
    _defaults:
        autowire: true
    # ...
    App\Service\MarkdownHelper:
        autowire: false
    # ...

The autoconfigure option is something we'll talk about during the last chapter of this course - but it's not too important:

... lines 1 - 4
services:
# default configuration for services in *this* file
_defaults:
... line 8
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
... lines 10 - 32

We'll also talk about public: false even sooner:

... lines 1 - 4
services:
# default configuration for services in *this* file
_defaults:
... lines 8 - 9
public: false # Allows optimizing the container by removing unused services; this also means
# fetching services directly from the container via $container->get() won't work.
# The best practice is to be explicit about your dependencies anyway.
... lines 13 - 32

The point is: we've established a few default values for any services that this file registers. No big deal.

Service Auto-Registration

The real magic comes down here with this App\ entry:

... lines 1 - 4
services:
... lines 6 - 13
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/*'
exclude: '../src/{Entity,Migrations,Tests}'
... lines 19 - 32

This says:

Make all classes inside src/ available as services in the container.

You can see this in real life! Run:

php bin/console debug:autowiring

At the top, yep! Our controller and MarkdownHelper appear in this list. And any future classes will also show up here, automatically.

But wait! Does that mean that all of our classes are instantiated on every single request? Because, that would be super wasteful!

Sadly... yes! Bah, I'm kidding! Come on - Symfony kicks way more but than that! No: this line simply tells the container to be aware of these classes. But services are never instantiated until - and unless - someone asks for them. So, if we didn't ask for our MarkdownHelper, it would never be instantiated on that request. Winning!

Services are only Instantiated Once

Oh, and one important thing: each service in the container is instantiated a maximum of once per request. If multiple parts of our code ask for the MarkdownHelper, it will be created just once, and the same instance will be passed each time. That's awesome for performance: we don't need multiple markdown helpers... even if we need to call parse() multiple times.

The Services exclude Key

... lines 1 - 4
services:
... lines 6 - 13
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
... line 17
exclude: '../src/{Entity,Migrations,Tests}'
... lines 19 - 32

The exclude key is not too important: if you know that some classes don't need to be in the container, you can exclude them for a small performance boost in the dev environment only.

So between _defaults and this App\ line - which we have given the fancy name - "service auto-registration" - everything just... works! New classes are added to the container and autowiring handles most of the heavy-lifting!

Oh, and this last App\Controller\ part is not important:

... lines 1 - 4
services:
... lines 6 - 19
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
... lines 25 - 32

The classes in Controller\ are already registered as services thanks to the App\ section. This adds a special tag to controllers... which you just shouldn't worry about. Honestly.

Finally, at the bottom, if you need to configure one service, this is where you do it: put the class name, then the config below:

... lines 1 - 4
services:
... lines 6 - 25
App\Service\MarkdownHelper:
arguments:
$logger: '@monolog.logger.markdown'
... lines 29 - 32

Services Ids = Class Name

And actually, this is not the class name of the service. It's really the service id... which happens to be equal to the class name. Run:

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

Most services in the container have a "snake case" service id. That's the best-practice for re-usable bundles. But thanks to service auto-registration, our service id's are equal to their class name. I just wanted to point that out.

The Amazing bind

Thanks to all of this config... well... we don't need to spend much time in this config file! We only need to configure the "special cases" - like we did for MarkdownHelper.

And actually.. there's a much cooler way to do that! Copy the service id and delete the config:

... lines 1 - 4
services:
... lines 6 - 25
App\Service\MarkdownHelper:
arguments:
$logger: '@monolog.logger.markdown'
... lines 29 - 32

If we didn't do anything else, Symfony would once-again pass us the "main" Logger object.

Now, add a new key beneath _defaults called bind. Then add $markdownLogger set to @monolog.logger.markdown:

... lines 1 - 4
services:
# default configuration for services in *this* file
_defaults:
... lines 8 - 13
# setup special, global autowiring rules
bind:
$markdownLogger: '@monolog.logger.markdown'
... lines 17 - 32

Copy that argument name, open MarkdownHelper, and rename the argument from $logger to $markdownLogger. Update it below too:

... lines 1 - 8
class MarkdownHelper
{
... lines 11 - 14
public function __construct(AdapterInterface $cache, MarkdownInterface $markdown, LoggerInterface $markdownLogger)
{
... lines 17 - 18
$this->logger = $markdownLogger;
}
... lines 21 - 35
}

Ok: markdown.log still only has one line. And... refresh! Check the file... hey! It worked!

I love bind: it says:

If you find any argument named $markdownLogger, pass this service to it.

And because we added it to _defaults, it applies to all our services. Instead of configuring our services one-by-one, we're creating project-wide conventions. Next time you need this logger? Yep, just name it $markdownLogger and keep coding.

Next! In addition to services, the container can also hold flat configuration: called parameters.

Leave a comment!

  • 2018-07-06 Nick Reincke

    You are welcome :)

  • 2018-07-04 Mehran Hadidi

    It makes sense now. Thanks for describing.

  • 2018-07-04 Nick Reincke

    The `markdown` channel, when created within a new handler (`markdown_logging` in this case), is also logged by the `main` handler because the `main` handler is by default only excluding the `event` channel, but not the new `markdown` channel.

  • 2018-07-04 Mehran Hadidi

    Thanks for reply. I also found this solution but it looks like its a bad design. Because when I am specifying a channel to log, why it should also publish it on main log file by default.

  • 2018-07-03 Juan Miguel Ardissone

    I got it

    There was the old configuration that I was using before trying with your tutorial

    App\Command\GreatCommand:
    arguments:
    $variableName2: '%param2%'
    tags:
    - { name: 'console.command' }

    I deleted it and it worked. Thanks

  • 2018-07-03 Juan Miguel Ardissone

    I have this configuration

    ~~~
    services:
    # default configuration for services in *this* file
    _defaults:
    autowire: true # Automatically injects dependencies in your services.
    autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
    public: false # Allows optimizing the container by removing unused services; this also means
    # fetching services directly from the container via $container->get() won't work.
    # The best practice is to be explicit about your dependencies anyway.

    bind:
    $variableName1: '%param1%'
    $variableName2: '%param2%'
    $variableName3: '@csa_guzzle.client.api'
    ~~~

    The 3 are in at least one constructor. The one that gives me problems is injected into a command (is a parameter). Could this be a problem?

  • 2018-07-03 weaverryan

    Hey Juan Miguel Ardissone!

    Ah yes, I can answer this! My guess is that, you have some configuration that looks like this:


    # config/services.yaml
    services:
    _defaults:
    # ...

    # actually, in your code, this may appear beneath an _instanceof config, instead of _defaults. But,
    # the situation is still the same
    bind:
    $variableName: 'someValue'

    When you use bind, to help make sure you don't have a typo, you MUST have a $variableName argument (with that name) in the constructor of *one* of your services. If you have ZERO arguments with this name, then, because this might be a typo, Symfony throws an exception. However, there is a small bug in the exception message. Really, the message should say:

    > Hey! You have a "bind" configured for an argument named $variableName. But, I didn't find this argument in any of your services. Do you maybe have a typo? Or is that bind unused?

    The bug is that, internally, Symfony "attaches" the error to one specific service - in your case some internal ".abstract.instanceof.App\Twig\AppExtension".

    So, the fix is to either (A) make sure that each bind is used on at least *one* service that it's bound to or (B) remove the bind because it's unused.

    Let me know if that helps!

    Cheers!

  • 2018-07-02 Juan Miguel Ardissone

    Hi.

    I had used bind configuration con my project. I am using Symfony 4.1 and when I add more than one I get this error:

    ```
    Unused binding "$variableName" in service ".abstract.instanceof.App\Twig\AppExtension".
    ```

    And that is right, I don't inject this variable into that twig extension because I do not use it there

    I found this question but without a correct answer: https://openclassrooms.com/...

    Did you now something about this?

    Side note: if I remove the bind variable or inject this variable into my AppExtension constructor it works. But this not make any sense

  • 2018-07-02 weaverryan

    Hey Nick Reincke!

    Ah, great work! This was on oversight on my part! And your solution is perfect.

    Cheers!

  • 2018-07-01 Nick Reincke

    Got the same issue. The `markdown_logging` handler is only logging the `markdown` channel but the `main` handler is logging anything except the `event` channel. You have to exclude the `markdown` channel from the `main` handler within the `monolog.yaml`.


    main:
    type: stream
    path: "%kernel.logs_dir%/%kernel.environment%.log"
    level: debug
    channels: ["!event", "!markdown"]
  • 2018-07-01 Mehran Hadidi

    Its logging on markdown.log and dev.log.

    Any solution?

  • 2018-05-22 Alexander Enlund

    Oh man... that was stupid of me not to notice, now I see that down in PhpSorm it shows what they're "under", well as you notice I'm new to Symfony and PHP... BUT everything is working, almost. Now I can't have the name "$markdownLogger" instead of the $logger, it says the following: Invalid service "App\Service\MarkdownHelper": method "__construct()" has no argument named "$logger". Check your service definition. AND I have changed from $logger to $markdownLogger where one should change it. (at least the bind is working, THANKS!)

  • 2018-05-21 weaverryan

    Hey Alexander Enlund!

    Ah man, that IS a weird error :). But I know the problem! Double-check your YAML indentation *very* closely - something is wrong with it. You can even copy the YAML code from the code blocks on this page to be sure. Basically, the "bind" keyword should be "under" (i.e. indented) a specific service (or beneath _defaults) so that you are applying the "bind" option to that service. But, you bind is not indented enough. So, to YAML, it appears that you are creating a new service whose id is "bind" and which has no options. I could repeat this by taking the code from the 2nd code block under this section - https://knpuniversity.com/s... - and *removing* 4 spaces before bind (so that it has the same number of spaces as _defaults).

    Let us know if this helps! And good for you for coding along - it will make a BIG difference :).

    Cheers!

  • 2018-05-21 Alexander Enlund

    I get a weird error, it says:
    (1/1) RuntimeException
    The definition for "bind" has no class. If you intend to inject this service dynamically at runtime, please mark it as synthetic=true. If this is an abstract definition solely used by child definitions, please add abstract=true, otherwise specify a class to get rid of this error.

    Btw. Great tutorial! Even though I'm trying to follow every step very closely, I get errors almost all the time...

  • 2018-03-22 gstanto

    Thank you very much for this series. This has cleared up so many things.