Cómo trabajar con ámbitos

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.

Entendiendo los ámbitos

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:

  • container (la opción predeterminada): Usa la misma instancia cada vez que la solicites desde el contenedor.
  • prototype: Crea una nueva instancia cada vez que solicitas el servicio.

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.

Usando un servicio de menor ámbito

If your service has a dependency on a scoped service (like the request), you have three ways to deal with it:

  • Use setter injection if the dependency is “synchronized”; this is the recommended way and the best solution for the request instance as it is synchronized with the request scope (see Using a synchronized Service).
  • Put your service in the same scope as the dependency (or a narrower one). If you depend on the request service, this means putting your new service in the request scope (see Changing the Scope of your Service);
  • 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 Passing the Container as a Dependency of your Service);

Each scenario is detailed in the following sections.

Using a synchronized Service

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:

  • YAML
    # src/Acme/HelloBundle/Resources/config/services.yml
    services:
        greeting_card_manager:
            class: Acme\HelloBundle\Mail\GreetingCardManager
            calls:
                - [setRequest, ['@?request']]
    
  • XML
    <!-- 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>
    
  • PHP
    // 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:

  • YAML
    services:
        request:
            scope: request
            synthetic: true
            synchronized: true
    
  • XML
    <services>
    <service id="request" scope="request" synthetic="true" synchronized="true" />
    </services>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\ContainerInterface;
    
    $definition = $container->setDefinition('request')
        ->setScope('request')
        ->setSynthetic(true)
        ->setSynchronized(true);
    

Changing the Scope of your Service

Changing the scope of a service should be done in its definition:

  • YAML
    # src/Acme/HelloBundle/Resources/config/services.yml
    services:
        greeting_card_manager:
            class: Acme\HelloBundle\Mail\GreetingCardManager
            scope: request
            arguments: [@request]
  • XML
    <!-- 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>
    
  • PHP
    // 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');
    

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/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:

  • YAML
    # 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
    
  • XML
    <!-- 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>
    
  • PHP
    // 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.

Bifúrcame en GitHub