Buy

services.yaml & the Amazing bind

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:

32 lines config/services.yaml
... 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:

32 lines config/services.yaml
... 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:

32 lines config/services.yaml
... 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:

32 lines config/services.yaml
... 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:

32 lines config/services.yaml
... 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:

32 lines config/services.yaml
... 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:

32 lines config/services.yaml
... 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

32 lines config/services.yaml
... 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:

32 lines config/services.yaml
... 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:

32 lines config/services.yaml
... 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:

32 lines config/services.yaml
... 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:

32 lines config/services.yaml
... 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:

37 lines src/Service/MarkdownHelper.php
... 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-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.