Service Container
Screencast
Do you prefer video tutorials? Check out the Symfony Fundamentals screencast series.
Your application is full of useful objects: a "Mailer" object might help you send emails while another object might help you save things to the database. Almost everything that your app "does" is actually done by one of these objects. And each time you install a new bundle, you get access to even more!
In Symfony, these useful objects are called services and each service lives inside a very special object called the service container. The container allows you to centralize the way objects are constructed. It makes your life easier, promotes a strong architecture and is super fast!
Fetching and using Services
The moment you start a Symfony app, your container already contains many services. These are like tools: waiting for you to take advantage of them. In your controller, you can "ask" for a service from the container by type-hinting an argument with the service's class or interface name. Want to log something? No problem:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// src/Controller/ProductController.php
namespace App\Controller;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class ProductController extends AbstractController
{
/**
* @Route("/products")
*/
public function list(LoggerInterface $logger): Response
{
$logger->info('Look, I just used a service!');
// ...
}
}
What other services are available? Find out by running:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
$ php bin/console debug:autowiring
# this is just a *small* sample of the output...
Autowirable Types
=================
The following classes & interfaces can be used as type-hints when autowiring:
Describes a logger instance.
Psr\Log\LoggerInterface (logger)
Request stack that controls the lifecycle of requests.
Symfony\Component\HttpFoundation\RequestStack (request_stack)
RouterInterface is the interface that all Router classes must implement.
Symfony\Component\Routing\RouterInterface (router.default)
[...]
When you use these type-hints in your controller methods or inside your own services, Symfony will automatically pass you the service object matching that type.
Throughout the docs, you'll see how to use the many different services that live in the container.
Tip
There are actually many more services in the container, and each service has
a unique id in the container, like request_stack
or router.default
. For a full
list, you can run php bin/console debug:container
. But most of the time,
you won't need to worry about this. See how to choose a specific service. See How to Debug the Service Container & List Services.
Creating/Configuring Services in the Container
You can also organize your own code into services. For example, suppose you need to show your users a random, happy message. If you put this code in your controller, it can't be re-used. Instead, you decide to create a new class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// src/Service/MessageGenerator.php
namespace App\Service;
class MessageGenerator
{
public function getHappyMessage(): string
{
$messages = [
'You did it! You updated the system! Amazing!',
'That was one of the coolest updates I\'ve seen all day!',
'Great work! Keep going!',
];
$index = array_rand($messages);
return $messages[$index];
}
}
Congratulations! You've created your first service class! You can use it immediately inside your controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// src/Controller/ProductController.php
use App\Service\MessageGenerator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class ProductController extends AbstractController
{
#[Route('/products/new')]
public function new(MessageGenerator $messageGenerator): Response
{
// thanks to the type-hint, the container will instantiate a
// new MessageGenerator and pass it to you!
// ...
$message = $messageGenerator->getHappyMessage();
$this->addFlash('success', $message);
// ...
}
}
When you ask for the MessageGenerator
service, the container constructs a new
MessageGenerator
object and returns it (see sidebar below). But if you never ask
for the service, it's never constructed: saving memory and speed. As a bonus, the
MessageGenerator
service is only created once: the same instance is returned
each time you ask for it.
Limiting Services to a specific Symfony Environment
5.3
The #[When]
attribute was introduced in Symfony 5.3.
If you are using PHP 8.0 or later, you can use the #[When]
PHP
attribute to only register the class as a service in some environments:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
use Symfony\Component\DependencyInjection\Attribute\When;
// SomeClass is only registered in the "dev" environment
#[When(env: 'dev')]
class SomeClass
{
// ...
}
// you can also apply more than one When attribute to the same class
#[When(env: 'dev')]
#[When(env: 'test')]
class AnotherClass
{
// ...
}
Injecting Services/Config into a Service
What if you need to access the logger
service from within MessageGenerator
?
No problem! Create a __construct()
method with a $logger
argument that has
the LoggerInterface
type-hint. Set this on a new $logger
property
and use it later:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// src/Service/MessageGenerator.php
namespace App\Service;
use Psr\Log\LoggerInterface;
class MessageGenerator
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function getHappyMessage(): string
{
$this->logger->info('About to find a happy message!');
// ...
}
}
That's it! The container will automatically know to pass the logger
service
when instantiating the MessageGenerator
. How does it know to do this?
Autowiring. The key is the LoggerInterface
type-hint in your __construct()
method and the autowire: true
config in
services.yaml
. When you type-hint an argument, the container will automatically
find the matching service. If it can't, you'll see a clear exception with a helpful
suggestion.
By the way, this method of adding dependencies to your __construct()
method is
called dependency injection.
How should you know to use LoggerInterface
for the type-hint? You can either
read the docs for whatever feature you're using, or get a list of autowireable
type-hints by running:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
$ php bin/console debug:autowiring
# this is just a *small* sample of the output...
Describes a logger instance.
Psr\Log\LoggerInterface (monolog.logger)
Request stack that controls the lifecycle of requests.
Symfony\Component\HttpFoundation\RequestStack (request_stack)
RouterInterface is the interface that all Router classes must implement.
Symfony\Component\Routing\RouterInterface (router.default)
[...]
Handling Multiple Services
Suppose you also want to email a site administrator each time a site update is made. To do that, you create a new class:
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 34 35
// src/Service/SiteUpdateManager.php
namespace App\Service;
use App\Service\MessageGenerator;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
class SiteUpdateManager
{
private MessageGenerator $messageGenerator;
private MailerInterface $mailer;
public function __construct(MessageGenerator $messageGenerator, MailerInterface $mailer)
{
$this->messageGenerator = $messageGenerator;
$this->mailer = $mailer;
}
public function notifyOfSiteUpdate(): bool
{
$happyMessage = $this->messageGenerator->getHappyMessage();
$email = (new Email())
->from('admin@example.com')
->to('manager@example.com')
->subject('Site update just happened!')
->text('Someone just updated the site. We told them: '.$happyMessage);
$this->mailer->send($email);
// ...
return true;
}
}
This needs the MessageGenerator
and the Mailer
service. That's no
problem, we ask them by type hinting their class and interface names!
Now, this new service is ready to be used. In a controller, for example,
you can type-hint the new SiteUpdateManager
class and use it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// src/Controller/SiteController.php
namespace App\Controller;
use App\Service\SiteUpdateManager;
// ...
class SiteController extends AbstractController
{
public function new(SiteUpdateManager $siteUpdateManager): Response
{
// ...
if ($siteUpdateManager->notifyOfSiteUpdate()) {
$this->addFlash('success', 'Notification mail was sent successfully.');
}
// ...
}
}
Thanks to autowiring and your type-hints in __construct()
, the container creates
the SiteUpdateManager
object and passes it the correct argument. In most cases,
this works perfectly.
Manually Wiring Arguments
But there are a few cases when an argument to a service cannot be autowired. For example, suppose you want to make the admin email configurable:
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
// src/Service/SiteUpdateManager.php
// ...
class SiteUpdateManager
{
// ...
+ private $adminEmail;
- public function __construct(MessageGenerator $messageGenerator, MailerInterface $mailer)
+ public function __construct(MessageGenerator $messageGenerator, MailerInterface $mailer, string $adminEmail)
{
// ...
+ $this->adminEmail = $adminEmail;
}
public function notifyOfSiteUpdate(): bool
{
// ...
$email = (new Email())
// ...
- ->to('manager@example.com')
+ ->to($this->adminEmail)
// ...
;
// ...
}
}
If you make this change and refresh, you'll see an error:
Cannot autowire service "App\Service\SiteUpdateManager": argument "$adminEmail" of method "__construct()" must have a type-hint or be given a value explicitly.
That makes sense! There is no way that the container knows what value you want to pass here. No problem! In your configuration, you can explicitly set this argument:
1 2 3 4 5 6 7 8 9 10 11 12 13
# config/services.yaml
services:
# ... same as before
# same as before
App\:
resource: '../src/'
exclude: '../src/{DependencyInjection,Entity,Kernel.php}'
# explicitly configure the service
App\Service\SiteUpdateManager:
arguments:
$adminEmail: 'manager@example.com'
Thanks to this, the container will pass manager@example.com
to the $adminEmail
argument of __construct
when creating the SiteUpdateManager
service. The
other arguments will still be autowired.
But, isn't this fragile? Fortunately, no! If you rename the $adminEmail
argument
to something else - e.g. $mainEmail
- you will get a clear exception when you
reload the next page (even if that page doesn't use this service).
Service Parameters
In addition to holding service objects, the container also holds configuration, called parameters. The main article about Symfony configuration explains the configuration parameters in detail and shows all their types (string, boolean, array, binary and PHP constant parameters).
However, there is another type of parameter related to services. In YAML config,
any string which starts with @
is considered as the ID of a service, instead
of a regular string. In XML config, use the type="service"
type for the
parameter and in PHP config use the service()
function:
1 2 3 4 5 6 7 8 9 10 11
# config/services.yaml
services:
App\Service\MessageGenerator:
arguments:
# this is not a string, but a reference to a service called 'logger'
- '@logger'
# if the value of a string argument starts with '@', you need to escape
# it by adding another '@' so Symfony doesn't consider it a service
# the following example would be parsed as the string '@securepassword'
# - '@@securepassword'
Working with container parameters is straightforward using the container's accessor methods for parameters:
1 2 3 4 5 6 7 8
// checks if a parameter is defined (parameter names are case-sensitive)
$container->hasParameter('mailer.transport');
// gets value of a parameter
$container->getParameter('mailer.transport');
// adds a new parameter
$container->setParameter('mailer.transport', 'sendmail');
Caution
The used .
notation is a
Symfony convention to make parameters
easier to read. Parameters are flat key-value elements, they can't
be organized into a nested array
Note
You can only set a parameter before the container is compiled, not at run-time. To learn more about compiling the container see Compiling the Container.
Choose a Specific Service
The MessageGenerator
service created earlier requires a LoggerInterface
argument:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// src/Service/MessageGenerator.php
namespace App\Service;
use Psr\Log\LoggerInterface;
class MessageGenerator
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
// ...
}
However, there are multiple services in the container that implement LoggerInterface
,
such as logger
, monolog.logger.request
, monolog.logger.php
, etc. How
does the container know which one to use?
In these situations, the container is usually configured to automatically choose
one of the services - logger
in this case (read more about why in Defining Services Dependencies Automatically (Autowiring)).
But, you can control this and pass in a different logger:
1 2 3 4 5 6 7 8 9 10 11
# config/services.yaml
services:
# ... same code as before
# explicitly configure the service
App\Service\MessageGenerator:
arguments:
# the '@' symbol is important: that's what tells the container
# you want to pass the *service* whose id is 'monolog.logger.request',
# and not just the *string* 'monolog.logger.request'
$logger: '@monolog.logger.request'
This tells the container that the $logger
argument to __construct
should use
service whose id is monolog.logger.request
.
For a list of possible logger services that can be used with autowiring, run:
1
$ php bin/console debug:autowiring logger
For a full list of all possible services in the container, run:
1
$ php bin/console debug:container
Remove Services
A service can be removed from the service container if needed. This is useful
for example to make a service unavailable in some configuration environment
(e.g. in the test
environment):
1 2 3 4 5 6 7 8 9 10
// config/services_test.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use App\RemovedService;
return function(ContainerConfigurator $containerConfigurator) {
$services = $containerConfigurator->services();
$services->remove(RemovedService::class);
};
Now, the container will not contain the App\RemovedService
in the test
environment.
Binding Arguments by Name or Type
You can also use the bind
keyword to bind specific arguments by name or type:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# config/services.yaml
services:
_defaults:
bind:
# pass this value to any $adminEmail argument for any service
# that's defined in this file (including controller arguments)
$adminEmail: 'manager@example.com'
# pass this service to any $requestLogger argument for any
# service that's defined in this file
$requestLogger: '@monolog.logger.request'
# pass this service for any LoggerInterface type-hint for any
# service that's defined in this file
Psr\Log\LoggerInterface: '@monolog.logger.request'
# optionally you can define both the name and type of the argument to match
string $adminEmail: 'manager@example.com'
Psr\Log\LoggerInterface $requestLogger: '@monolog.logger.request'
iterable $rules: !tagged_iterator app.foo.rule
# ...
By putting the bind
key under _defaults
, you can specify the value of any
argument for any service defined in this file! You can bind arguments by name
(e.g. $adminEmail
), by type (e.g. Psr\Log\LoggerInterface
) or both
(e.g. Psr\Log\LoggerInterface $requestLogger
).
The bind
config can also be applied to specific services or when
loading many services at once).
Abstract Service Arguments
Sometimes, the values of some service arguments can't be defined in the configuration files because they are calculated at runtime using a compiler pass or bundle extension.
In those cases, you can use the abstract
argument type to define at least
the name of the argument and some short description about its purpose:
1 2 3 4 5 6 7 8 9
# config/services.yaml
services:
# ...
App\Service\MyService:
arguments:
$rootNamespace: !abstract 'should be defined by Pass'
# ...
If you don't replace the value of an abstract argument during runtime, a
RuntimeException
will be thrown with a message like
Argument "$rootNamespace" of service "App\Service\MyService" is abstract: should be defined by Pass.
5.1
The abstract service arguments were introduced in Symfony 5.1.
The autowire Option
Above, the services.yaml
file has autowire: true
in the _defaults
section
so that it applies to all services defined in that file. With this setting, you're
able to type-hint arguments in the __construct()
method of your services and
the container will automatically pass you the correct arguments. This entire entry
has been written around autowiring.
For more details about autowiring, check out Defining Services Dependencies Automatically (Autowiring).
The autoconfigure Option
Above, the services.yaml
file has autoconfigure: true
in the _defaults
section so that it applies to all services defined in that file. With this setting,
the container will automatically apply certain configuration to your services, based
on your service's class. This is mostly used to auto-tag your services.
For example, to create a Twig extension, you need to create a class, register it
as a service, and tag it with twig.extension
.
But, with autoconfigure: true
, you don't need the tag. In fact, if you're using
the default services.yaml config,
you don't need to do anything: the service will be automatically loaded. Then,
autoconfigure
will add the twig.extension
tag for you, because your class
implements Twig\Extension\ExtensionInterface
. And thanks to autowire
, you can even add
constructor arguments without any configuration.
Autoconfiguration also works with attributes. Some attributes like AsMessageHandler, AsEventListener and AsCommand are registered for autoconfiguration. Any class using these attributes will have tags applied to them.
5.3
Autoconfiguration through attributes was introduced in Symfony 5.3.
Linting Service Definitions
The lint:container
command checks that the arguments injected into services
match their type declarations. It's useful to run it before deploying your
application to production (e.g. in your continuous integration server):
1
$ php bin/console lint:container
Checking the types of all service arguments whenever the container is compiled
can hurt performance. That's why this type checking is implemented in a
compiler pass called
CheckTypeDeclarationsPass
which is disabled by default and enabled only when
executing the lint:container
command. If you don't mind the performance
loss, enable the compiler pass in your application.
Public Versus Private Services
Every service defined is private by default. When a service is private, you
cannot access it directly from the container using $container->get()
. As a
best practice, you should only create private services and you should fetch
services using dependency injection instead of using $container->get()
.
If you need to fetch services lazily, instead of using public services you should consider using a service locator.
But, if you do need to make a service public, override the public
setting:
1 2 3 4 5 6 7
# config/services.yaml
services:
# ... same code as before
# explicitly configure the service
App\Service\PublicService:
public: true
It is also possible to define a service as public thanks to the #[Autoconfigure]
attribute. This attribute must be used directly on the class of the service
you want to configure:
1 2 3 4 5 6 7 8 9 10
// src/Service/PublicService.php
namespace App\Service;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
#[Autoconfigure(public: true)]
class PublicService
{
// ...
}
5.3
The #[Autoconfigure]
attribute was introduced in Symfony 5.3. PHP
attributes require at least PHP 8.0.
5.1
As of Symfony 5.1, it is no longer possible to autowire the service
container by type-hinting Psr\Container\ContainerInterface
.
Importing Many Services at once with resource
You've already seen that you can import many services at once by using the resource
key. For example, the default Symfony configuration contains this:
1 2 3 4 5 6 7 8 9
# config/services.yaml
services:
# ... same as before
# 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,Kernel.php}'
Tip
The value of the resource
and exclude
options can be any valid
glob pattern.
This can be used to quickly make many classes available as services and apply some
default configuration. The id
of each service is its fully-qualified class name.
You can override any service that's imported by using its id (class name) below
(e.g. see how to manually wire arguments).
If you override a service, none of the options (e.g. public
) are inherited
from the import (but the overridden service does still inherit from _defaults
).
You can also exclude
certain paths. This is optional, but will slightly increase
performance in the dev
environment: excluded paths are not tracked and so modifying
them will not cause the container to be rebuilt.
Note
Wait, does this mean that every class in src/
is registered as
a service? Even model classes? Actually, no. As long as you keep your imported services as private, all
classes in src/
that are not explicitly used as services are
automatically removed from the final container. In reality, the import
means that all classes are "available to be used as services" without needing
to be manually configured.
Multiple Service Definitions Using the Same Namespace
If you define services using the YAML config format, the PHP namespace is used as the key of each configuration, so you can't define different service configs for classes under the same namespace:
1 2 3 4 5
# config/services.yaml
services:
App\Domain\:
resource: '../src/Domain/*'
# ...
In order to have multiple definitions, add the namespace
option and use any
unique string as the key of each service config:
1 2 3 4 5 6 7 8 9 10 11
# config/services.yaml
services:
command_handlers:
namespace: App\Domain\
resource: '../src/Domain/*/CommandHandler'
tags: [command_handler]
event_subscribers:
namespace: App\Domain\
resource: '../src/Domain/*/EventSubscriber'
tags: [event_subscriber]
Explicitly Configuring Services and Arguments
Prior to Symfony 3.3, all services and (typically) arguments were explicitly configured: it was not possible to load services automatically and autowiring was much less common.
Both of these features are optional. And even if you use them, there may be some
cases where you want to manually wire a service. For example, suppose that you want
to register 2 services for the SiteUpdateManager
class - each with a different
admin email. In this case, each needs to have a unique service id:
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
# config/services.yaml
services:
# ...
# this is the service's id
site_update_manager.superadmin:
class: App\Service\SiteUpdateManager
# you CAN still use autowiring: we just want to show what it looks like without
autowire: false
# manually wire all arguments
arguments:
- '@App\Service\MessageGenerator'
- '@mailer'
- 'superadmin@example.com'
site_update_manager.normal_users:
class: App\Service\SiteUpdateManager
autowire: false
arguments:
- '@App\Service\MessageGenerator'
- '@mailer'
- 'contact@example.com'
# Create an alias, so that - by default - if you type-hint SiteUpdateManager,
# the site_update_manager.superadmin will be used
App\Service\SiteUpdateManager: '@site_update_manager.superadmin'
In this case, two services are registered: site_update_manager.superadmin
and site_update_manager.normal_users
. Thanks to the alias, if you type-hint
SiteUpdateManager
the first (site_update_manager.superadmin
) will be passed.
If you want to pass the second, you'll need to manually wire the service or to create a named autowiring alias.
Caution
If you do not create the alias and are loading all services from src/,
then three services have been created (the automatic service + your two services)
and the automatically loaded service will be passed - by default - when you type-hint
SiteUpdateManager
. That's why creating the alias is a good idea.
Learn more
- How to Create Service Aliases and Mark Services as Private
- Defining Services Dependencies Automatically (Autowiring)
- Service Method Calls and Setter Injection
- How to Work with Compiler Passes
- How to Configure a Service with a Configurator
- How to Debug the Service Container & List Services
- How to work with Service Definition Objects
- How to Inject Values Based on Complex Expressions
- Using a Factory to Create Services
- How to Import Configuration Files/Resources
- Types of Injection
- Lazy Services
- How to Make Service Arguments/References Optional
- How to Manage Common Dependencies with Parent Services
- How to Retrieve the Request from the Service Container
- Service Closures
- How to Decorate Services
- Service Subscribers & Locators
- How to Define Non Shared Services
- How to Inject Instances into the Container
- How to Work with Service Tags