The Symfony 3.3 DI Container Changes Explained (autowiring, _defaults, etc)
If you look at the services.yaml
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 :doc:backwards compatibility promise </contributing/code/bc>
,
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.
… _service-33-default_definition
:
The new Default services.yaml File
To understand the changes, look at the new default services.yaml
file (this is
what the file looks like in Symfony 4):
… configuration-block::
.. code-block:: yaml
# config/services.yaml
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']
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
.. code-block:: xml
<!-- config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<defaults autowire="true" autoconfigure="true" public="false"/>
<prototype namespace="App\" resource="../src/*" exclude="../src/{Entity,Migrations,Tests}"/>
<prototype namespace="App\Controller\" resource="../src/Controller">
<tag name="controller.service_arguments"/>
</prototype>
<!-- add more services, or override services that need manual wiring -->
</services>
</container>
.. code-block:: php
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return function(ContainerConfigurator $configurator) {
// default configuration for services in *this* file
$services = $configurator->services()
->defaults()
->autowire() // Automatically injects dependencies in your services.
->autoconfigure() // Automatically registers your services as commands, event subscribers, etc.
;
// makes classes in src/ available to be used as services
// this creates a service per class whose id is the fully-qualified class name
$services->load('App\\', '../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
$services->load('App\\Controller\\', '../src/Controller')
->tag('controller.service_arguments');
// add more service definitions when explicit configuration is needed
// please note that last definitions always *replace* previous ones
};
This small bit of configuration contains a paradigm shift of how services
are configured in Symfony.
… _service-33-changes-automatic-registration
:
- Services are Loaded Automatically
… seealso::
Read the documentation for :ref:`automatic service loading <service-psr4-loader>`.
The first big change is that services do not need to be defined one-by-one anymore,
thanks to the following config:
… configuration-block::
.. code-block:: yaml
# config/services.yaml
services:
# ...
# 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}'
.. code-block:: xml
<!-- config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<!-- ... -->
<prototype namespace="App\" resource="../src/*" exclude="../src/{Entity,Migrations,Tests}"/>
</services>
</container>
.. code-block:: php
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return function(ContainerConfigurator $configurator) {
// ...
// makes classes in src/ available to be used as services
// this creates a service per class whose id is the fully-qualified class name
$services->load('App\\', '../src/*')
->exclude('../src/{Entity,Migrations,Tests}');
};
This means that every class in src/
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. App\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 :ref:multiple services for the same class <services-explicitly-configure-wire-services>
.
But how does the container know the arguments to my services?
Since each service is :ref:autowired <services-autowire>
, the container is able
to determine most arguments automatically. But, you can always override the service
and :ref:manually configure arguments <services-manually-wire-args>
or anything
else special about your service.
But wait, if I have some model (non-service) classes in my ``src/``
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 :ref:private <container-public>
(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 blacklist 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.
- 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:
… configuration-block::
.. code-block:: yaml
# config/services.yaml
services:
app.invoice_generator:
class: App\Service\InvoiceGenerator
app.invoice_mailer:
class: App\Service\InvoiceMailer
arguments:
- '@app.invoice_generator'
.. code-block:: xml
<!-- config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="app.invoice_generator"
class="App\Service\InvoiceGenerator"/>
<service id="app.invoice_mailer"
class="App\Service\InvoiceMailer">
<argument type="service" id="app.invoice_generator"/>
</service>
</services>
</container>
.. code-block:: php
// config/services.php
use App\Service\InvoiceGenerator;
use App\Service\InvoiceMailer;
use Symfony\Component\DependencyInjection\Reference;
$container->register('app.invoice_generator', InvoiceGenerator::class);
$container->register('app.invoice_mailer', InvoiceMailer::class)
->setArguments([new Reference('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
::
// src/Service/InvoiceMailer.php
// ...
class InvoiceMailer
{
private $generator;
public function __construct(InvoiceGenerator $generator)
{
$this->generator = $generator
}
// ...
}
That’s it! Both services are :ref:automatically registered <service-33-changes-automatic-registration>
and set to autowire. Without any configuration, the container knows to pass the
auto-registered App\Service\InvoiceGenerator
as the first argument. As
you can see, the type of the class - App\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 :ref:autowiring-logic-explained
.
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
:ref:manually configure *just* that one argument <services-manually-wire-args>
.
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.
Autowiring aims to automate configuration without magic.
- Controllers are Registered as Services
The third big change is that, in a new Symfony 3.3 project, your controllers are services:
… configuration-block::
.. code-block:: yaml
# config/services.yaml
services:
# ...
# controllers are imported separately to make sure they're public
# and have a tag that allows actions to type-hint services
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
.. code-block:: xml
<!-- config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<!-- ... -->
<prototype namespace="App\Controller\" resource="../src/Controller">
<tag name="controller.service_arguments"/>
</prototype>
</services>
</container>
.. code-block:: php
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return function(ContainerConfigurator $configurator) {
// ...
// controllers are imported separately to make sure they're public
// and have a tag that allows actions to type-hint services
$services->load('App\\Controller\\', '../src/Controller')
->tag('controller.service_arguments');
};
But, you might not even notice this. First, your controllers can still extend
the same base controller class (AbstractController
).
This means you have access to all of the same shortcuts as before. Additionally,
the @Route
annotation and _controller
syntax (e.g. App: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 :doc:/controller/service
for more details. You can even create :ref:invokable controllers <controller-service-invoke>
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::
use Psr\Log\LoggerInterface;
class InvoiceController extends AbstractController
{
public function listInvoices(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).
… _service_autoconfigure:
- 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::
// src/EventSubscriber/SetHeaderSusbcriber.php
// ...
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class SetHeaderSusbcriber implements EventSubscriberInterface
{
public function onKernelResponse(ResponseEvent $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.yaml
and tag it with kernel.event_subscriber
. In Symfony 3.3,
you’re already done!
The service is :ref:automatically registered <service-33-changes-automatic-registration>
.
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. And you can
always set autoconfigure
to false
in services.yaml
, 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! :ref:Manually configure your service <services-manually-wire-args>
and add the tag. Your tag will take precedence over the one added by auto-configuration.
- 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:
… configuration-block::
.. code-block:: yaml
# config/services.yaml
services:
# ...
_instanceof:
App\Domain\LoaderInterface:
public: true
tags: ['app.domain_loader']
.. code-block:: xml
<!-- config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<!-- ... -->
<instanceof id="App\Domain\LoaderInterface" public="true">
<tag name="app.domain_loader"/>
</instanceof>
</services>
</container>
.. code-block:: php
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use App\Domain\LoaderInterface;
return function(ContainerConfigurator $configurator) {
// ...
$services->instanceof(LoaderInterface::class)
->public()
->tag('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:
… code-block:: yaml
# config/services.yaml
services:
app.github_notifier:
class: App\Service\GitHubNotifier
arguments:
- '@app.api_client_github'
markdown_transformer:
class: App\Service\MarkdownTransformer
app.api_client_github:
class: App\Service\ApiClient
arguments:
- 'https://api.github.com'
app.api_client_sl_connect:
class: App\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``.
.. code-block:: diff
# config/services.yaml
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:
… code-block:: diff
# config/services.yaml
services:
# ...
- app.github_notifier:
- class: App\Service\GitHubNotifier
+ App\Service\GitHubNotifier:
arguments:
- '@app.api_client_github'
- markdown_transformer:
- class: App\Service\MarkdownTransformer
+ App\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 :doc:`compiler pass </service_container/compiler_passes>`,
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 :ref:`step 5 of this article <step-5>`.
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 App\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.yaml
file:
… code-block:: yaml
# config/legacy_aliases.yaml
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: '@App\Service\GitHubNotifier'
markdown_transformer: '@App\Service\MarkdownTransformer'
Then import this at the top of services.yaml
:
… code-block:: diff
# config/services.yaml
+ imports:
+ - { resource: legacy_aliases.yaml }
# ...
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:
.. code-block:: diff
# config/services.yaml
# ...
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.yaml``),
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:
.. code-block:: diff
# config/services.yaml
# ...
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/
(and/or any other directory/bundle you have):
… code-block:: diff
# config/services.yaml
services:
_defaults:
# ...
+ App\:
+ resource: '../src/*'
+ exclude: '../src/{Entity,Migrations,Tests}'
+
+ App\Controller\:
+ resource: '../src/Controller'
+ tags: ['controller.service_arguments']
# ...
That’s it! Actually, you’re already overriding and reconfiguring all the services
you’re using (App\Service\GitHubNotifier
and App\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:
… code-block:: diff
# config/services.yaml
services:
# ...
+ # alias ApiClient to one of our services below
+ # app.api_client_github will be used to autowire ApiClient type-hints
+ App\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:
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.yaml``). This means updating any service arguments (e.g.
``@app.github_notifier`` to ``@App\Service\GitHubNotifier``) and updating your
code to not fetch this service directly from the container. For example:
.. code-block:: diff
- public function index()
+ public function index(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.yaml`` 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:
.. code-block:: diff
# config/services.yaml
services:
# ...
app.api_client_github:
# ...
- public: true
app.api_client_sl_connect:
# ...
- public: true
Finally, you can optionally remove any services from ``services.yaml`` whose arguments
can be autowired. The final configuration looks like this:
.. code-block:: yaml
services:
_defaults:
autowire: true
autoconfigure: true
public: false
App\:
resource: '../src/*'
exclude: '../src/{Entity,Migrations,Tests}'
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
App\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
App\Service\ApiClient: '@app.api_client_github'
# keep these ids because there are multiple instances per class
app.api_client_github:
class: App\Service\ApiClient
arguments:
- 'https://api.github.com'
app.api_client_sl_connect:
class: App\Service\ApiClient
arguments:
- 'https://connect.symfony.com/api'
You can now take advantage of the new features going forward.
.. _`FrameworkExtension for 3.3.0`: https://github.com/symfony/symfony/blob/7938fdeceb03cc1df277a249cf3da70f0b50eb98/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php#L247-L284