All about services.yaml

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 creates its container, it needs to get a big list of every service that should be in the container: each service's id, class name and the arguments that should be passed to its constructor. It gets this big list from exactly two places. The first - and the biggest - is from bundles.

If we run:

php bin/console debug:container

The vast majority of these services come from bundles. Each bundle has a list of the services it provides, which includes the id, class name and arguments for each one.

The second place the container goes to complete its list of services is our src/ directory. We already know that MarkdownHelper is in the service container because we've been able to autowire it to our controller.

services.yaml Registers our Services

So when the container is being created, it asks each bundle for its service list and then - to learn about our services - it reads services.yaml.

When Symfony starts parsing this file, nothing in the src/ directory has been registered as a service in the container. Adding our classes to the container is, in fact, the job of this file. And the way it does it is pretty amazing.

The _defaults Key

The first thing under services is a special key called _defaults:

... lines 1 - 8
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.
... lines 14 - 33

This defines default options that should be applied to each service that's added to the container in this file. Every service registered here will have an option called autowire set to true and another called autoconfigure set to true.

Let me... say that a different way. When you configure a single service - like we're doing for MarkdownHelper - it's totally legal to say autowire: true or autowire: false. That's an option that you can configure on any service. The _defaults sections says:

Hey! Don't make me manually add autowire: true to every service - make that the default value.

The autowire Option

What does the autowire option mean? Simply, it tells Symfony's container:

Please try to guess the arguments to my constructor by reading their type-hints.

We like that feature, so it will be "on" automatically for all of our services. The other option - autoconfigure - is more subtle and we'll talk about it later.

So these 3 lines don't do anything: they just set up default config.

Service Auto-Registration

The next section - these 3 lines starting with App\ - are the key to everything:

... lines 1 - 8
services:
... lines 10 - 14
# 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/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
... lines 20 - 33

This says:

Hey container! Please look at my src/ directory and register every class you find as a service in the container.

And when it does this, thanks to _defaults, every service will have autowire and autoconfigure enabled. This is why MarkdownHelper was instantly available as a service in the container and why its arguments are being autowired. This is called "service auto-registration".

But remember, every service in the container needs to have a unique id. When you auto-register services like this, the id matches the class name. We can see this! The vast majority of the services in debug:container have a snake-case id. But if you go all the way to the top, our services are also in this list each service ID is identical to its class name.

This is done to keep life simple... but also because it powers autowiring. If we try to autowire App\Service\MarkdownHelper into our controller or another service, in order to figure out what to pass to that argument, autowiring looks in the container for a service whose id exactly matches the type-hint: App\Service\MarkdownHelper.

Anyways, back in services.yaml, after the _defaults section and this App\ block, we have now registered every class in the src/ directory as a service and told Symfony to autowire each one.

But do we really want every class in src/ to be a service? Actually, no. Not all classes are services and that's what the exclude: key helps with. For example, the Entity/ directory will eventually store database model classes, which are not services: they're just classes that hold some data.

So we register everything in src/ as a service, except for things in these directories. And actually, the exclude key is not that important. Heck, you could delete it! If you accidentally registered something as a service that is not a service, Symfony will realize that when you never use it, and remove it automatically from the container. No big deal.

The point is: everything in src/ is automatically available as a service in the container without you needing to think about it.

And... that's really it for the important stuff! The next section registers everything in src/Controller as a service:

... lines 1 - 8
services:
... lines 10 - 20
# 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 26 - 33

But wait... didn't the section above already do that? Totally! This overrides those in order to add this "tag" thing. This is here to cover an "edge case" that doesn't apply to us. If we deleted this, everything would keep working. So... ignore it.

Now that we understand how our services are being added to the container, the config that we added to the bottom of this file will make more sense. Let's talk about it next and then leverage our new knowledge to learn a way cooler way to pass the $isDebug flag to MarkdownHelper.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "knplabs/knp-markdown-bundle": "^1.8", // 1.8.1
        "sensio/framework-extra-bundle": "^5.5", // v5.5.4
        "sentry/sentry-symfony": "^3.4", // 3.4.4
        "symfony/asset": "5.0.*", // v5.0.8
        "symfony/console": "5.0.*", // v5.0.8
        "symfony/debug-bundle": "5.0.*", // v5.0.8
        "symfony/dotenv": "5.0.*", // v5.0.8
        "symfony/flex": "^1.3.1", // v1.6.2
        "symfony/framework-bundle": "5.0.*", // v5.0.8
        "symfony/monolog-bundle": "^3.0", // v3.5.0
        "symfony/profiler-pack": "*", // v1.0.4
        "symfony/twig-pack": "^1.0", // v1.0.0
        "symfony/var-dumper": "5.0.*", // v5.0.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.7.3
        "symfony/yaml": "5.0.*" // v5.0.8
    },
    "require-dev": {
        "symfony/maker-bundle": "^1.15", // v1.15.0
        "symfony/profiler-pack": "^1.0" // v1.0.4
    }
}