How to Work with Scopes
How to Work with Scopes¶
This entry is all about scopes, a somewhat advanced topic related to the Service Container. If you’ve ever gotten an error mentioning “scopes” when creating services, then this entry is for you.
Note
If you are trying to inject the request
service, the simple solution
is to inject the request_stack
service instead and access the current
Request by calling the
getCurrentRequest()
method (see Injecting the Request). The rest of this entry
talks about scopes in a theoretical and more advanced way. If you’re
dealing with scopes for the request
service, simply inject request_stack
.
Understanding Scopes¶
The scope of a service controls how long an instance of a service is used by the container. The DependencyInjection component provides two generic scopes:
container
(the default one): The same instance is used each time you request it from this container.prototype
: A new instance is created each time you request the service.
The
Symfony\Component\HttpKernel\DependencyInjection\ContainerAwareHttpKernel
also defines a third scope: request
. This scope is tied to the request,
meaning a new instance is created for each subrequest and is unavailable
outside the request (for instance in the CLI).
An Example: Client Scope¶
Other than the request
service (which has a simple solution, see the
above note), no services in the default Symfony2 container belong to any
scope other than container
and prototype
. But for the purposes of
this entry, imagine there is another scope client
and a service client_configuration
that belongs to it. This is not a common situation, but the idea is that
you may enter and exit multiple client
scopes during a request, and each
has its own client_configuration
service.
Scopes add a constraint on the dependencies of a service: a service cannot
depend on services from a narrower scope. For example, if you create a generic
my_foo
service, but try to inject the client_configuration
service,
you will receive a
Symfony\Component\DependencyInjection\Exception\ScopeWideningInjectionException
when compiling the container. Read the sidebar below for more details.
Note
A service can of course depend on a service from a wider scope without any issue.
Using a Service from a Narrower Scope¶
There are several solutions to the scope problem:
- A) Use setter injection if the dependency is
synchronized
(see A) Using a Synchronized Service); - B) Put your service in the same scope as the dependency (or a narrower one). If
you depend on the
client_configuration
service, this means putting your new service in theclient
scope (see B) Changing the Scope of your Service); - C) Pass the entire container to your service and retrieve your dependency from
the container each time you need it to be sure you have the right instance
– your service can live in the default
container
scope (see C) Passing the Container as a Dependency of your Service).
Each scenario is detailed in the following sections.
A) Using a Synchronized Service¶
New in version 2.3: Synchronized services were introduced in Symfony 2.3.
Both injecting the container and setting your service to a narrower scope have
drawbacks. Assume first that the client_configuration
service has been
marked as synchronized
:
- YAML
1 2 3 4 5 6 7 8
# app/config/config.yml services: client_configuration: class: AppBundle\Client\ClientConfiguration scope: client synchronized: true synthetic: true # ...
- XML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
<!-- app/config/config.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 http://symfony.com/schema/dic/services/services-1.0.xsd" > <services> <service id="client_configuration" scope="client" synchronized="true" synthetic="true" class="AppBundle\Client\ClientConfiguration" /> </services> </container>
- PHP
1 2 3 4 5 6 7 8 9 10 11
// app/config/config.php use Symfony\Component\DependencyInjection\Definition; $definition = new Definition( 'AppBundle\Client\ClientConfiguration', array() ); $definition->setScope('client'); $definition->setSynchronized(true); $definition->setSynthetic(true); $container->setDefinition('client_configuration', $definition);
Now, if you inject this service using setter injection, there are no drawbacks and everything works without any special code in your service or in your definition:
// src/AppBundle/Mail/Mailer.php
namespace AppBundle\Mail;
use AppBundle\Client\ClientConfiguration;
class Mailer
{
protected $clientConfiguration;
public function setClientConfiguration(ClientConfiguration $clientConfiguration = null)
{
$this->clientConfiguration = $clientConfiguration;
}
public function sendEmail()
{
if (null === $this->clientConfiguration) {
// throw an error?
}
// ... do something using the client configuration here
}
}
Whenever the client
scope is active, the service container will
automatically call the setClientConfiguration()
method when the
client_configuration
service is set in the container.
You might have noticed that the setClientConfiguration()
method accepts
null
as a valid value for the client_configuration
argument. That’s
because when leaving the client
scope, the client_configuration
instance
can be null
. Of course, you should take care of this possibility in
your code. This should also be taken into account when declaring your service:
- YAML
1 2 3 4 5 6
# app/config/services.yml services: my_mailer: class: AppBundle\Mail\Mailer calls: - [setClientConfiguration, ["@?client_configuration="]]
- XML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<!-- app/config/services.xml --> <services> <service id="my_mailer" class="AppBundle\Mail\Mailer" > <call method="setClientConfiguration"> <argument type="service" id="client_configuration" on-invalid="null" strict="false" /> </call> </service> </services>
- PHP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// app/config/services.php use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\ContainerInterface; $definition = $container->setDefinition( 'my_mailer', new Definition('AppBundle\Mail\Mailer') ) ->addMethodCall('setClientConfiguration', array( new Reference( 'client_configuration', ContainerInterface::NULL_ON_INVALID_REFERENCE, false ) ));
B) Changing the Scope of your Service¶
Changing the scope of a service should be done in its definition. This example
assumes that the Mailer
class has a __construct
function whose first
argument is the ClientConfiguration
object:
- YAML
1 2 3 4 5 6
# app/config/services.yml services: my_mailer: class: AppBundle\Mail\Mailer scope: client arguments: ["@client_configuration"]
- XML
1 2 3 4 5 6 7 8
<!-- app/config/services.xml --> <services> <service id="my_mailer" class="AppBundle\Mail\Mailer" scope="client"> <argument type="service" id="client_configuration" /> </service> </services>
- PHP
1 2 3 4 5 6 7 8 9 10
// app/config/services.php use Symfony\Component\DependencyInjection\Definition; $definition = $container->setDefinition( 'my_mailer', new Definition( 'AppBundle\Mail\Mailer', array(new Reference('client_configuration'), )) )->setScope('client');
C) Passing the Container as a Dependency of your Service¶
Setting the scope to a narrower one is not always possible (for instance, a
twig extension must be in the container
scope as the Twig environment
needs it as a dependency). In these cases, you can pass the entire container
into your service:
// src/AppBundle/Mail/Mailer.php
namespace AppBundle\Mail;
use Symfony\Component\DependencyInjection\ContainerInterface;
class Mailer
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function sendEmail()
{
$request = $this->container->get('client_configuration');
// ... do something using the client configuration here
}
}
Caution
Take care not to store the client configuration in a property of the object for a future call of the service as it would cause the same issue described in the first section (except that Symfony cannot detect that you are wrong).
The service config for this class would look something like this:
- YAML
1 2 3 4 5 6
# app/config/services.yml services: my_mailer: class: AppBundle\Mail\Mailer arguments: ["@service_container"] # scope: container can be omitted as it is the default
- XML
1 2 3 4 5 6
<!-- app/config/services.xml --> <services> <service id="my_mailer" class="AppBundle\Mail\Mailer"> <argument type="service" id="service_container" /> </service> </services>
- PHP
1 2 3 4 5 6 7 8
// app/config/services.php use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; $container->setDefinition('my_mailer', new Definition( 'AppBundle\Mail\Mailer', array(new Reference('service_container')) ));
Note
Injecting the whole container into a service is generally not a good idea (only inject what you need).
This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.