Esta entrada trata sobre los ámbitos, un tema un tanto avanzado relacionado con el Contenedor de servicios. Si alguna vez has tenido un error hablando de «ámbitos» en la creación de servicios o necesitas crear un servicio que depende del servicio Petición, entonces este artículo es para ti.
El ámbito de un servicio controla la duración de una instancia de un servicio utilizado por el contenedor. El componente Inyección de dependencias tiene dos ámbitos genéricos:
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).
Los ámbitos agregan una restricción en las dependencias de un servicio: un servicio no puede depender de los servicios de un ámbito más estrecho. For example, if you create a generic my_foo service, but try to inject the request service, you will receive a Symfony\Component\DependencyInjection\Exception\ScopeWideningInjectionException when compiling the container. Lee la barra lateral más adelante para más detalles.
Nota
Un servicio puede, por supuesto, depender de un servicio desde un ámbito más amplio sin ningún problema.
If your service has a dependency on a scoped service (like the request), you have three ways to deal with it:
Each scenario is detailed in the following sections.
Nuevo en la versión 2.3: Synchronized services are new in Symfony 2.3.
Injecting the container or setting your service to a narrower scope have drawbacks. For synchronized services (like the request), using setter injection is the best option as it has no drawbacks and everything works without any special code in your service or in your definition:
// src/Acme/HelloBundle/Mail/Mailer.php
namespace Acme\HelloBundle\Mail;
use Symfony\Component\HttpFoundation\Request;
class Mailer
{
protected $request;
public function setRequest(Request $request = null)
{
$this->request = $request;
}
public function sendEmail()
{
if (null === $this->request) {
// throw an error?
}
// ... haz algo con la respuesta de redirección
}
}
Whenever the request scope is entered or left, the service container will automatically call the setRequest() method with the current request instance.
You might have noticed that the setRequest() method accepts null as a valid value for the request argument. That’s because when leaving the request scope, the request instance can be null (for the master request for instance). Of course, you should take care of this possibility in your code. This should also be taken into account when declaring your service:
# src/Acme/HelloBundle/Resources/config/services.yml
services:
greeting_card_manager:
class: Acme\HelloBundle\Mail\GreetingCardManager
calls:
- [setRequest, ['@?request']]
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<services>
<service id="greeting_card_manager"
class="Acme\HelloBundle\Mail\GreetingCardManager"
/>
<call method="setRequest">
<argument type="service" id="request" on-invalid="null" strict="false" />
</call>
</services>
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ContainerInterface;
$definition = $container->setDefinition(
'greeting_card_manager',
new Definition('Acme\HelloBundle\Mail\GreetingCardManager')
)
->addMethodCall('setRequest', array(
new Reference('request', ContainerInterface::NULL_ON_INVALID_REFERENCE, false)
));
Truco
You can declare your own synchronized services very easily; here is the declaration of the request service for reference:
services:
request:
scope: request
synthetic: true
synchronized: true
<services>
<service id="request" scope="request" synthetic="true" synchronized="true" />
</services>
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ContainerInterface;
$definition = $container->setDefinition('request')
->setScope('request')
->setSynthetic(true)
->setSynchronized(true);
Changing the scope of a service should be done in its definition:
# src/Acme/HelloBundle/Resources/config/services.yml
services:
greeting_card_manager:
class: Acme\HelloBundle\Mail\GreetingCardManager
scope: request
arguments: [@request]
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<services>
<service id="greeting_card_manager"
class="Acme\HelloBundle\Mail\GreetingCardManager"
scope="request"
/>
<argument type="service" id="request" />
</services>
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
$definition = $container->setDefinition(
'greeting_card_manager',
new Definition(
'Acme\HelloBundle\Mail\GreetingCardManager',
array(new Reference('request'),
))
)->setScope('request');
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/Acme/HelloBundle/Mail/Mailer.php
namespace Acme\HelloBundle\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('request');
// ... haz algo con la respuesta de redirección
}
}
Prudencia
Ten cuidado de no guardar la petición en una propiedad del objeto para una futura llamada del servicio, ya que sería el mismo problema descrito en la primera sección (excepto que Symfony no puede detectar qué estás haciendo mal).
La configuración del servicio de esta clase sería algo como esto:
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
# ...
my_mailer.class: Acme\HelloBundle\Mail\Mailer
services:
my_mailer:
class: "%my_mailer.class%"
arguments: ["@service_container"]
# scope: el contenedor se puede omitir como si fuera el predefinido
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<parameters>
<!-- ... -->
<parameter key="my_mailer.class">Acme\HelloBundle\Mail\Mailer</parameter>
</parameters>
<services>
<service id="my_mailer" class="%my_mailer.class%">
<argument type="service" id="service_container" />
</service>
</services>
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
// ...
$container->setParameter('my_mailer.class', 'Acme\HelloBundle\Mail\Mailer');
$container->setDefinition('my_mailer', new Definition(
'%my_mailer.class%',
array(new Reference('service_container'))
));
Nota
Inyectar el contenedor completo en un servicio generalmente no es una buena idea (inyecta sólo lo que necesitas).
Truco
If you define a controller as a service then you can get the Request object without injecting the container by having it passed in as an argument of your action method. See La Petición como argumento para el controlador for details.