Skip to content

The Symfony 3.3 DI Container Changes Explained (autowiring, _defaults, etc)

Warning: You are browsing the documentation for Symfony 3.x, which is no longer maintained.

Read the updated version of this page for Symfony 7.1 (the current stable version).

If you look at the services.yml file in a new Symfony 3.3 or newer project, you'll notice some big changes: _defaults, autowiring, autoconfigure and more. These features are designed to automate configuration and make development faster, without sacrificing predictability, which is very important! Another goal is to make controllers and services behave more consistently. In Symfony 3.3, controllers are services by default.

The documentation has already been updated to assume you have these new features enabled. If you're an existing Symfony user and want to understand the "what" and "why" behind these changes, this article is for you!

All Changes are Optional

Most importantly, you can upgrade to Symfony 3.3 today without making any changes to your app. Symfony has a strict backwards compatibility promise, which means it's always safe to upgrade across minor versions.

All of the new features are optional: they are not enabled by default, so you need to actually change your configuration files to use them.

The new Default services.yml File

To understand the changes, look at the new default services.yml file in the Symfony Standard Edition:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# app/config/services.yml
services:
    # default configuration for services in *this* file
    _defaults:
        # automatically injects dependencies in your services
        autowire: true
        # automatically registers your services as commands, event subscribers, etc.
        autoconfigure: true
        # this means you cannot fetch services directly from the container via $container->get()
        # if you need to do this, you can override this setting on individual services
        public: false

    # makes classes in src/AppBundle available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    AppBundle\:
        resource: '../../src/AppBundle/*'
        # you can exclude directories or files
        # but if a service is unused, it's removed anyway
        exclude: '../../src/AppBundle/{Entity,Repository}'

    # controllers are imported separately to make sure they're public
    # and have a tag that allows actions to type-hint services
    AppBundle\Controller\:
        resource: '../../src/AppBundle/Controller'
        tags: ['controller.service_arguments']

    # add more services, or override services that need manual wiring
    # AppBundle\Service\ExampleService:
    #     arguments:
    #         $someArgument: 'some_value'

This small bit of configuration contains a paradigm shift of how services are configured in Symfony.

1) Services are Loaded Automatically

See also

Read the documentation for automatic service loading.

The first big change is that services do not need to be defined one-by-one anymore, thanks to the following config:

1
2
3
4
5
6
7
8
9
10
11
# app/config/services.yml
services:
    # ...

    # makes classes in src/AppBundle available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    AppBundle\:
        resource: '../../src/AppBundle/*'
        # you can exclude directories or files
        # but if a service is unused, it's removed anyway
        exclude: '../../src/AppBundle/{Entity,Repository}'

This means that every class in src/AppBundle/ is available to be used as a service. And thanks to the _defaults section at the top of the file, all of these services are autowired and private (i.e. public: false).

The service ids are equal to the class name (e.g. AppBundle\Service\InvoiceGenerator). And that's another change you'll notice in Symfony 3.3: we recommend that you use the class name as your service id, unless you have multiple services for the same class.

But how does the container know the arguments to my services?

Since each service is autowired, the container is able to determine most arguments automatically. But, you can always override the service and manually configure arguments or anything else special about your service.

But wait, if I have some model (non-service) classes in my src/AppBundle/ directory, doesn't this mean that they will also be registered as services? Isn't that a problem?

Actually, this is not a problem. Since all the new services are private (thanks to _defaults), if any of the services are not used in your code, they're automatically removed from the compiled container. This means that the number of services in your container should be the same whether your explicitly configure each service or load them all at once with this method.

OK, but can I exclude some paths that I know won't contain services?

Yes! The exclude key is a glob pattern that can be used to ignore paths that you do not want to be included as services. But, since unused services are automatically removed from the container, exclude is not that important. The biggest benefit is that those paths are not tracked by the container, and so may result in the container needing to be rebuilt less-often in the dev environment.

2) Autowiring by Default: Use Type-hint instead of Service id

The second big change is that autowiring is enabled (via _defaults) for all services you register. This also means that service id's are now less important and "types" (i.e. class or interface names) are now more important.

For example, before Symfony 3.3 (and this is still allowed), you could pass one service as an argument to another with the following config:

1
2
3
4
5
6
7
8
9
# app/config/services.yml
services:
    app.invoice_generator:
        class: AppBundle\Service\InvoiceGenerator

    app.invoice_mailer:
        class: AppBundle\Service\InvoiceMailer
        arguments:
            - '@app.invoice_generator'

To pass the InvoiceGenerator as an argument to InvoiceMailer, you needed to specify the service's id as an argument: app.invoice_generator. Service id's were the main way that you configured things.

But in Symfony 3.3, thanks to autowiring, all you need to do is type-hint the argument with InvoiceGenerator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/AppBundle/Service/InvoiceMailer.php
// ...

class InvoiceMailer
{
    private $generator;

    public function __construct(InvoiceGenerator $generator)
    {
        $this->generator = $generator
    }

    // ...
}

That's it! Both services are automatically registered and set to autowire. Without any configuration, the container knows to pass the auto-registered AppBundle\Service\InvoiceGenerator as the first argument. As you can see, the type of the class - AppBundle\Service\InvoiceGenerator - is what's most important, not the id. You request an instance of a specific type and the container automatically passes you the correct service.

Isn't that magic? How does it know which service to pass me exactly? What if I have multiple services of the same instance?

The autowiring system was designed to be super predictable. It first works by looking for a service whose id exactly matches the type-hint. This means you're in full control of what type-hint maps to what service. You can even use service aliases to get more control. If you have multiple services for a specific type, you choose which should be used for autowiring. For full details on the autowiring logic, see Defining Services Dependencies Automatically (Autowiring).

But what if I have a scalar (e.g. string) argument? How does it autowire that?

If you have an argument that is not an object, it can't be autowired. But that's OK! Symfony will give you a clear exception (on the next refresh of any page) telling you which argument of which service could not be autowired. To fix it, you can manually configure *just* that one argument. This is the philosophy of autowiring: only configure the parts that you need to. Most configuration is automated.

OK, but autowiring makes your applications less stable. If you change one thing or make a mistake, unexpected things might happen. Isn't that a problem?

Symfony has always valued stability, security and predictability first. Autowiring was designed with that in mind. Specifically:

  • If there is a problem wiring any argument to any service, a clear exception is thrown on the next refresh of any page, even if you don't use that service on that page. That's powerful: it is not possible to make an autowiring mistake and not realize it.
  • The container determines which service to pass in an explicit way: it looks for a service whose id matches the type-hint exactly. It does not scan all services looking for objects that have that class/interface (actually, it does do this in Symfony 3.3, but has been deprecated. If you rely on this, you will see a clear deprecation warning).

Autowiring aims to automate configuration without magic.

3) Controllers are Registered as Services

The third big change is that, in a new Symfony 3.3 project, your controllers are services:

1
2
3
4
5
6
7
8
9
# app/config/services.yml
services:
    # ...

    # controllers are imported separately to make sure they're public
    # and have a tag that allows actions to type-hint services
    AppBundle\Controller\:
        resource: '../../src/AppBundle/Controller'
        tags: ['controller.service_arguments']

But, you might not even notice this. First, your controllers can still extend the same base Controller class or a new AbstractController. This means you have access to all of the same shortcuts as before. Additionally, the @Route annotation and _controller syntax (e.g. AppBundle:Default:homepage) used in routing will automatically use your controller as a service (as long as its service id matches its class name, which it does in this case). See How to Define Controllers as Services for more details. You can even create invokable controllers

In other words, everything works the same. You can even add the above configuration to your existing project without any issues: your controllers will behave the same as before. But now that your controllers are services, you can use dependency injection and autowiring like any other service.

To make life even easier, it's now possible to autowire arguments to your controller action methods, just like you can with the constructor of services. For example:

1
2
3
4
5
6
7
8
9
use Psr\Log\LoggerInterface;

class InvoiceController extends Controller
{
    public function listAction(LoggerInterface $logger)
    {
        $logger->info('A new way to access services!');
    }
}

This is only possible in a controller, and your controller service must be tagged with controller.service_arguments to make it happen. This new feature is used throughout the documentation.

In general, the new best practice is to use normal constructor dependency injection (or "action" injection in controllers) instead of fetching public services via $this->get() (though that does still work).

4) Auto-tagging with autoconfigure

The fourth big change is the autoconfigure key, which is set to true under _defaults. Thanks to this, the container will auto-tag services registered in this file. For example, suppose you want to create an event subscriber. First, you create the class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/AppBundle/EventSubscriber/SetHeaderSusbcriber.php
// ...

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class SetHeaderSusbcriber implements EventSubscriberInterface
{
    public function onKernelResponse(FilterResponseEvent $event)
    {
        $event->getResponse()->headers->set('X-SYMFONY-3.3', 'Less config');
    }

    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::RESPONSE => 'onKernelResponse'
        ];
    }
}

Great! In Symfony 3.2 or lower, you would now need to register this as a service in services.yml and tag it with kernel.event_subscriber. In Symfony 3.3, you're already done! The service is automatically registered. And thanks to autoconfigure, Symfony automatically tags the service because it implements EventSubscriberInterface.

That sounds like magic - it automatically tags my services?

In this case, you've created a class that implements EventSubscriberInterface and registered it as a service. This is more than enough for the container to know that you want this to be used as an event subscriber: more configuration is not needed. And the tags system is its own, Symfony-specific mechanism. You can always set autoconfigure to false in services.yml, or disable it for a specific service.

Does this mean tags are dead? Does this work for all tags?

This does not work for all tags. Many tags have required attributes, like event listeners, where you also need to specify the event name and method in your tag. Autoconfigure works only for tags without any required tag attributes, and as you read the docs for a feature, it'll tell you whether or not the tag is needed. You can also look at the extension classes (e.g. FrameworkExtension for 3.3.0) to see what it autoconfigures.

What if I need to add a priority to my tag?

Many autoconfigured tags have an optional priority. If you need to specify a priority (or any other optional tag attribute), no problem! Just manually configure your service and add the tag. Your tag will take precedence over the one added by auto-configuration.

5) Auto-configure with _instanceof

And the final big change is _instanceof. It acts as a default definition template (see service-33-default_definition), but only for services whose class matches a defined one.

This can be very useful when many services share some tag that cannot be inherited from an abstract definition:

1
2
3
4
5
6
7
8
# app/config/services.yml
services:
    # ...

    _instanceof:
        AppBundle\Domain\LoaderInterface:
            public: true
            tags: ['app.domain_loader']

What about Performance

Symfony is unique because it has a compiled container. This means that there is no runtime performance impact for using any of these features. That's also why the autowiring system can give you such clear errors.

However, there is some performance impact in the dev environment. Most importantly, your container will likely be rebuilt more often when you modify your service classes. This is because it needs to rebuild whenever you add a new argument to a service, or add an interface to your class that should be autoconfigured.

In very big projects, this may be a problem. If it is, you can always opt to not use autowiring. If you think the cache rebuilding system could be smarter in some situation, please open an issue!

Upgrading to the new Symfony 3.3 Configuration

Ready to upgrade your existing project? Great! Suppose you have the following configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# app/config/services.yml
services:
    app.github_notifier:
        class: AppBundle\Service\GitHubNotifier
        arguments:
            - '@app.api_client_github'

    markdown_transformer:
        class: AppBundle\Service\MarkdownTransformer

    app.api_client_github:
        class: AppBundle\Service\ApiClient
        arguments:
            - 'https://api.github.com'

    app.api_client_sl_connect:
        class: AppBundle\Service\ApiClient
        arguments:
            - 'https://connect.symfony.com/api'

It's optional, but let's upgrade this to the new Symfony 3.3 configuration step-by-step, without breaking our application.

Step 1): Adding _defaults

Start by adding a _defaults section with autowire and autoconfigure.

1
2
3
4
5
6
7
# app/config/services.yml
services:
+     _defaults:
+         autowire: true
+         autoconfigure: true

    # ...

You're already explicitly configuring all of your services. So, autowire does nothing. You're also already tagging your services, so autoconfigure also doesn't change any existing services.

You have not added public: false yet. That will come in a minute.

Step 2) Using Class Service id's

Right now, the service ids are machine names - e.g. app.github_notifier. To work well with the new configuration system, your service ids should be class names, except when you have multiple instances of the same service.

Start by updating the service ids to class names:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# app/config/services.yml
services:
    # ...

-     app.github_notifier:
-         class: AppBundle\Service\GitHubNotifier
+     AppBundle\Service\GitHubNotifier:
        arguments:
            - '@app.api_client_github'

-     markdown_transformer:
-         class: AppBundle\Service\MarkdownTransformer
+     AppBundle\Service\MarkdownTransformer: ~

    # keep these ids because there are multiple instances per class
    app.api_client_github:
        # ...
    app.api_client_sl_connect:
        # ...

Caution

Services associated with global PHP classes (i.e. not using PHP namespaces) must maintain the class parameter. For example, when using the old Twig classes (e.g. Twig_Extensions_Extension_Intl instead of Twig\Extensions\IntlExtension), you can't redefine the service as Twig_Extensions_Extension_Intl: ~ and you must keep the original class parameter.

Caution

If a service is processed by a compiler pass, you could face a "You have requested a non-existent service" error. To get rid of this, be sure that the Compiler Pass is using findDefinition() instead of getDefinition(). The latter won't take aliases into account when looking up for services. Furthermore it is always recommended to check for definition existence using has() function.

Note

If you get rid of deprecations and make your controllers extend from AbstractController instead of Controller, you can skip the rest of this step because AbstractController doesn't provide a container where you can get the services from. All services need to be injected as explained in the step 5 of this article.

But, this change will break our app! The old service ids (e.g. app.github_notifier) no longer exist. The simplest way to fix this is to find all your old service ids and update them to the new class id: app.github_notifier to AppBundle\Service\GitHubNotifier.

In large projects, there's a better way: create legacy aliases that map the old id to the new id. Create a new legacy_aliases.yml file:

1
2
3
4
5
6
7
8
9
# app/config/legacy_aliases.yml
services:
    _defaults:
        public: true
    # aliases so that the old service ids can still be accessed
    # remove these if/when you are not fetching these directly
    # from the container via $container->get()
    app.github_notifier: '@AppBundle\Service\GitHubNotifier'
    markdown_transformer: '@AppBundle\Service\MarkdownTransformer'

Then import this at the top of services.yml:

1
2
3
4
5
# app/config/services.yml
+ imports:
+     - { resource: legacy_aliases.yml }

# ...

That's it! The old service ids still work. Later, (see the cleanup step below), you can remove these from your app.

Step 3) Make the Services Private

Now you're ready to default all services to be private:

1
2
3
4
5
6
7
8
# app/config/services.yml
# ...

services:
    _defaults:
        autowire: true
        autoconfigure: true
+       public: false

Thanks to this, any services created in this file cannot be fetched directly from the container. But, since the old service id's are aliases in a separate file (legacy_aliases.yml), these are still public. This makes sure the app keeps working.

If you did not change the id of some of your services (because there are multiple instances of the same class), you may need to make those public:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# app/config/services.yml
# ...

services:
    # ...

    app.api_client_github:
        # ...

+         # remove this if/when you are not fetching this
+         # directly from the container via $container->get()
+         public: true

    app.api_client_sl_connect:
        # ...
+         public: true

This is to guarantee that the application doesn't break. If you're not fetching these services directly from the container, this isn't needed. In a minute, you'll clean that up.

Step 4) Auto-registering Services

You're now ready to automatically register all services in src/AppBundle/ (and/or any other directory/bundle you have):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# app/config/services.yml

services:
    _defaults:
        # ...

+     AppBundle\:
+         resource: '../../src/AppBundle/*'
+         exclude: '../../src/AppBundle/{Entity,Repository}'
+
+     AppBundle\Controller\:
+         resource: '../../src/AppBundle/Controller'
+         tags: ['controller.service_arguments']

    # ...

That's it! Actually, you're already overriding and reconfiguring all the services you're using (AppBundle\Service\GitHubNotifier and AppBundle\Service\MarkdownTransformer). But now, you won't need to manually register future services.

Once again, there is one extra complication if you have multiple services of the same class:

1
2
3
4
5
6
7
8
9
10
11
12
13
# app/config/services.yml

services:
    # ...

+     # alias ApiClient to one of our services below
+     # app.api_client_github will be used to autowire ApiClient type-hints
+     AppBundle\Service\ApiClient: '@app.api_client_github'

    app.api_client_github:
        # ...
    app.api_client_sl_connect:
        # ...

This guarantees that if you try to autowire an ApiClient instance, the app.api_client_github will be used. If you don't have this, the auto-registration feature will try to register a third ApiClient service and use that for autowiring (which will fail, because the class has a non-autowireable argument).

Step 5) Cleanup!

To make sure your application didn't break, you did some extra work. Now it's time to clean things up! First, update your application to not use the old service id's (the ones in legacy_aliases.yml). This means updating any service arguments (e.g. @app.github_notifier to @AppBundle\Service\GitHubNotifier) and updating your code to not fetch this service directly from the container. For example:

1
2
3
4
5
6
7
8
9
-     public function indexAction()
+     public function indexAction(GitHubNotifier $gitHubNotifier, MarkdownTransformer $markdownTransformer)
    {
-         // the old way of fetching services
-         $githubNotifier = $this->container->get('app.github_notifier');
-         $markdownTransformer = $this->container->get('markdown_transformer');

        // ...
    }

As soon as you do this, you can delete legacy_aliases.yml and remove its import. You should do the same thing for any services that you made public, like app.api_client_github and app.api_client_sl_connect. Once you're not fetching these directly from the container, you can remove the public: true flag:

1
2
3
4
5
6
7
8
9
10
11
# app/config/services.yml
services:
    # ...

    app.api_client_github:
        # ...
-         public: true

    app.api_client_sl_connect:
        # ...
-         public: true

Finally, you can optionally remove any services from services.yml whose arguments can be autowired. The final configuration looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
services:
    _defaults:
        autowire: true
        autoconfigure: true
        public: false

    AppBundle\:
        resource: '../../src/AppBundle/*'
        exclude: '../../src/AppBundle/{Entity,Repository}'

    AppBundle\Controller\:
        resource: '../../src/AppBundle/Controller'
        tags: ['controller.service_arguments']

    AppBundle\Service\GitHubNotifier:
        # this could be deleted, or I can keep being explicit
        arguments:
            - '@app.api_client_github'

    # alias ApiClient to one of our services below
    # app.api_client_github will be used to autowire ApiClient type-hints
    AppBundle\Service\ApiClient: '@app.api_client_github'

    # keep these ids because there are multiple instances per class
    app.api_client_github:
        class: AppBundle\Service\ApiClient
        arguments:
            - 'https://api.github.com'

    app.api_client_sl_connect:
        class: AppBundle\Service\ApiClient
        arguments:
            - 'https://connect.symfony.com/api'

You can now take advantage of the new features going forward.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version