Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

services.yaml & the Amazing bind

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

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!

27
Login or Register to join the conversation
Default user avatar
Default user avatar Mehran Hadidi | posted 5 years ago

Its logging on markdown.log and dev.log.

Any solution?

2 Reply
NickReynke Avatar
NickReynke Avatar NickReynke | Mehran Hadidi | posted 5 years ago | edited

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"]
3 Reply

Hey NickReynke!

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

Cheers!

2 Reply
Default user avatar
Default user avatar Mehran Hadidi | NickReynke | posted 5 years ago

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.

1 Reply
NickReynke Avatar

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.

1 Reply
Default user avatar
Default user avatar Mehran Hadidi | NickReynke | posted 5 years ago

It makes sense now. Thanks for describing.

17 Reply
Default user avatar
Default user avatar A Mastou | posted 3 years ago

Awesome ! I really begin understanding what happens behind the scenes. Thank you Ryan !

Reply
Richard Avatar
Richard Avatar Richard | posted 3 years ago

I just read the monolog docs here: https://symfony.com/doc/4.4...

And it seems bind is obsolete in this case.

Just the parameter name hinting is sufficient since 3.5. No change to services.yaml required.

I removed the bind, cleared the cache and ... it works.

Did I miss something?

Reply

Hey Richard

Cool! So the binding it's now done by the bundle automatically, you just need to remember the convention. That feature didn't exist when this tutorials was written :)

Cheers!

1 Reply
Richard Avatar
Richard Avatar Richard | posted 3 years ago

I added Service to the excludes to test in services.yaml:

exclude: '../src/{Service,Entity,Migrations,Tests}'

And it still found MarkdownHelp in the Service directory. Bug? (I cleared the cache too).

Reply

Hey Richard

I believe you still have the custom configuration for the MarkdownHelp class. Could you double check it?

Cheers!

1 Reply
Richard Avatar

I believe you may have been right.. but worse than that... I had "Services" and not "Service" in the exclude. So ignore the noise...

Reply
Avraham M. Avatar
Avraham M. Avatar Avraham M. | posted 4 years ago | edited

Hello!
I am trying to use lazy loading, symfony 4.3,
configuring in services.yaml


services:
    # Default configuration for all services.
    _defaults:
        # Automatically injects dependencies in the container.
        autowire: false
        # Automatically registers the services as commands, event subscribers, etc.
        autoconfigure: true
        # Prevents access to the injected files to be called from the container.
        public: false
        # Injects whether the application runs in debug mode. (use bool $isDebug in constructor)
        bind:
            $isDebug: '%kernel.debug%'

    # Configuration for the dependency injection for App namespace.
    App\:
        lazy: true
        public: false
        autowire: true
        resource: '../common/App/*'
        #exclude: '../common/App/{Entity}'
    App\Helpers\:
        lazy: true
        public: true
        autowire: true
        resource: '../common/App/Helpers/*'

When trying to call:


$container->get("App\Helper\Language")

I get error:

UndefinedMethodException
Attempted to call an undefined method named "staticProxyConstructor" of class "Language_2c90222"

Any idea?
Thanks

Reply

Hey Avraham,

Are you sure you need to lazy loading *everything* in your application? It's not a good practice, actually, I think it's just overkill. Well, except service you also have entities, models, forms, etc - they probably don't need to be lazy loaded. I'd recommend you to start configuring lazy loading for 1 service where you think lazy loading is the most important, get it working, then switch to another class that also should be lazy loaded, and so one. Try to configure it class by class - it would be easier to debug things I think.

About the error, first of all, try to clear the cache the the specific environment where you see the error, and try again. Do you see the same error? Do you have a staticProxyConstructor() method call somewhere in *your* code? If no, probably some version incompatibility, could you try to update Composer dependencies, clear the cache, and try again?

Cheers!

Reply
Avraham M. Avatar
Avraham M. Avatar Avraham M. | Victor | posted 4 years ago | edited

Hey Viktor,
Thanks for your advice.

Yes, I turn off lazy option.I succeed to obtain $entityManager = $this->getDoctrine()->getManager();
Yet still MappingException: Class "App\Entity\Patient" is not a valid entity or mapped super class.
<br />$patient = new Patient();<br />$patient->setFname('Alex1');<br />// FAILS<br />// MappingException: Class "App\Entity\Patient" is not a valid entity or mapped super class.<br />$entityManager->persist($patient); <br />

Reply

Great, let's follow the second part of your comment in another thread to avoid duplications: https://symfonycasts.com/sc...

Cheers!

Reply
Avraham M. Avatar
Avraham M. Avatar Avraham M. | Victor | posted 4 years ago

Sure,
Thanks!

Reply
Default user avatar
Default user avatar Agata | posted 4 years ago | edited

I have a question about 'Services are only Instantiated Once'.
What if we need to process some data in our service (for example some Converter) and we don't want to pass all the data (some arrays for example) to each method so we make this class keep the state in properties (but also uses some services from container)?
I saw that there is an option: shared: false to Define Non Shared Services so it will create a new instance every time and I also found on the stackoverflow similar question: https://stackoverflow.com/questions/28511553/symfony2-services-with-or-without-state but there were just two answers.
So how should we do something like that? Use Non Shared Services; create stateless services, inject dependencies and pass them to statefull classes created by 'new' keyword inside service or some other way?

Reply

Hey Agata,

Sorry for the long reply! Yes, using "shared: false" is a valid way to achieve what you need, and it sounds OK to me. I don't know your design to much, but if you think there's no other way and you do need to use different objects in different places instead of one shared - go for it.

Cheers!

Reply
Jeffrey C. Avatar
Jeffrey C. Avatar Jeffrey C. | posted 4 years ago | edited

Hey,

When i try to test if it wil log but i get this weird error
<br />invalidArgumentException<br />Invalid service "App\Service\MarkdownHelper": method "__construct()" has no argument named "$logger". Check your service definition.
even though i follow exactly like the tutorials and i can't seem to figure out where the problem lies.

Any solution why?

Reply
Jeffrey C. Avatar
Jeffrey C. Avatar Jeffrey C. | Jeffrey C. | posted 4 years ago | edited

UPDATE
Found the solution i still had some code in the services.yaml after i quoted that out it works :)
`

App\Service\MarkdownHelper:
    arguments:
        $logger: '@monolog.logger.markdown'

`
Forgot this bit now it's disabled.

Reply

Hey Jeffrey C.

Yep, indentation matters in YAML files :). Cheers!

Reply

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

Reply

Hey micayael!

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!

Reply

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?

Reply

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

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.1.4
        "symfony/asset": "^4.0", // v4.0.4
        "symfony/console": "^4.0", // v4.0.14
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/framework-bundle": "^4.0", // v4.0.14
        "symfony/lts": "^4@dev", // dev-master
        "symfony/twig-bundle": "^4.0", // v4.0.4
        "symfony/web-server-bundle": "^4.0", // v4.0.4
        "symfony/yaml": "^4.0" // v4.0.14
    },
    "require-dev": {
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.4
        "symfony/debug-bundle": "^3.3|^4.0", // v4.0.4
        "symfony/dotenv": "^4.0", // v4.0.14
        "symfony/maker-bundle": "^1.0", // v1.0.2
        "symfony/monolog-bundle": "^3.0", // v3.1.2
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.0.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.0.4
    }
}
userVoice