Symfony 2.4, el libro oficial

16.6. Inyectando servicios

Hasta el momento, nuestro servicio original my_mailer es muy sencillo ya que sólo toma un argumento (fácilmente configurable) en su constructor. No obstante, el verdadero poder del contenedor se obtiene cuando al crear un servicio es necesario crear otros servicios de los que depende.

Supongamos por ejemplo que tienes un nuevo servicio, NewsletterManager, que facilita el envío de newsletters a una serie de direcciones de email. Como ya disponemos del servicio my_mailer para enviar emails, vamos a utilizarlo dentro de NewsletterManager para manejar el envío de los mensajes. Esta clase podría tener el siguiente aspecto:

// src/Acme/HelloBundle/Newsletter/NewsletterManager.php
namespace Acme\HelloBundle\Newsletter;

use Acme\HelloBundle\Mailer;

class NewsletterManager
{
    protected $mailer;

    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    // ...
}

Sin utilizar el contenedor de servicios, puedes crear fácilmente un nuevo NewsletterManager dentro de un controlador:

use Acme\HelloBundle\Newsletter\NewsletterManager;

// ...

public function sendNewsletterAction()
{
    $mailer = $this->get('my_mailer');
    $newsletter = new NewsletterManager($mailer);
    // ...
}

El código anterior es correcto, pero, ¿si más adelante decides que la clase NewsletterManager necesita un segundo o tercer argumento en su constructor? ¿Y si decides reconstruir tu código y cambiar el nombre de la clase? En ambos casos, habría que encontrar todos los lugares donde se crea una instancia de NewsletterManager y modificarla. Obviamente, el contenedor de servicios te ofrece una alternativa mucho más interesante:

# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
    # ...
    newsletter_manager.class: Acme\HelloBundle\Newsletter\NewsletterManager

services:
    my_mailer:
        # ...
    newsletter_manager:
        class:     %newsletter_manager.class%
        arguments: ["@my_mailer"]
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<parameters>
    <!-- ... -->
    <parameter key="newsletter_manager.class">Acme\HelloBundle\Newsletter\NewsletterManager</parameter>
</parameters>

<services>
    <service id="my_mailer" ...>
      <!-- ... -->
    </service>
    <service id="newsletter_manager" class="%newsletter_manager.class%">
        <argument type="service" id="my_mailer"/>
    </service>
</services>
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

// ...
$container->setParameter(
    'newsletter_manager.class',
    'Acme\HelloBundle\Newsletter\NewsletterManager'
);

$container->setDefinition('my_mailer', ...);
$container->setDefinition('newsletter_manager', new Definition(
    '%newsletter_manager.class%',
    array(new Reference('my_mailer'))
));

En YAML, la sintaxis especial @my_mailer le dice al contenedor que busque un servicio llamado my_mailer y pase ese objeto al constructor de NewsletterManager. Si el servicio especificado (my_mailer) no existe, se muestra una excepción. Por eso puedes marcar tus dependencias como opcionales, tal y como se explicará en la siguiente sección.

La utilización de referencias es una herramienta muy poderosa que te permite crear clases independientes con dependencias bien definidas. En este ejemplo, el servicio newsletter_manager necesita del servicio my_mailer para poder funcionar. Al definir esta dependencia en el contenedor de servicios, el contenedor se encarga de todo el trabajo de instanciar los objetos.

16.6.1. Utilizando el componente ExpressionLanguage

El contenedor de servicios también soporta el uso de expresiones desde la versión 2.4 de Symfony. Mediante el uso de expresiones es posible inyectar valores muy específicos en un servicio.

Imagina que dispones de un servicio llamado mailer_configuration que contiene un método getMailerMethod() que devuelve una cadena de texto como sendmail en función de las opciones de configuración. Volviendo al ejemplo mostrado al principio de este capítulo, otro servicio llamado my_mailer requiere como primer argumento una cadena de texto como sendmail.

En vez de escribir esta cadena de texto a mano como argumento del servicio my_mailer, puedes obtener el valor directamente a través del método getMailerMethod() del servicio mailer_configuration. Para ello, utiliza la siguiente expresión:

# app/config/config.yml
services:
    my_mailer:
        class:        Acme\HelloBundle\Mailer
        arguments:    ["@=service('mailer_configuration').getMailerMethod()"]
<!-- 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="my_mailer" class="Acme\HelloBundle\Mailer">
            <argument type="expression">service('mailer_configuration').getMailerMethod()</argument>
        </service>
    </services>
</container>
// app/config/config.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\ExpressionLanguage\Expression;

$container->setDefinition('my_mailer', new Definition(
    'Acme\HelloBundle\Mailer',
    array(new Expression('service("mailer_configuration").getMailerMethod()'))
));

Consulta la documentación del componente ExpressionLanguage para conocer todas las opciones de su sintaxis.

Dentro de una expresión del contenedor de servicios, tienes acceso a las dos siguientes funciones:

  • service, devuelve el servicio cuyo id se indica.
  • parameter, devuelve el valor del parámetro indicado (la sintaxis es idéntica a la de la función service anterior)

Las expresiones también tienen acceso a una variable de tipo Symfony\Component\DependencyInjection\ContainerBuilder llamada container. El siguiente ejemplo muestra otro posible uso de las expresiones en el contenedor de servicios:

services:
    my_mailer:
        class:     Acme\HelloBundle\Mailer
        arguments: ["@=container.hasParameter('some_param') ? parameter('some_param') : 'default_value'"]
<?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="my_mailer" class="Acme\HelloBundle\Mailer">
            <argument type="expression">@=container.hasParameter('some_param') ? parameter('some_param') : 'default_value'</argument>
        </service>
    </services>
</container>
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\ExpressionLanguage\Expression;

$container->setDefinition('my_mailer', new Definition(
    'Acme\HelloBundle\Mailer',
    array(new Expression(
        "@=container.hasParameter('some_param') ? parameter('some_param') : 'default_value'"
    ))
));

Las expresiones se pueden utilizar en arguments y properties, como argumentos de configurator y también como argumento de calls.

16.6.2. Dependencias opcionales

Inyectar dependencias en el constructor de esta manera es una excelente manera de asegurarte que la dependencia está disponible para usarla. No obstante, si las dependencias de una clase son opcionales, entonces es mucho mejor inyectarlas mediante métodos de tipo setter. Esto significa que la dependencia se inyecta mediante una llamada a un método en vez de a través del constructor. La clase ahora sería así:

namespace Acme\HelloBundle\Newsletter;

use Acme\HelloBundle\Mailer;

class NewsletterManager
{
    protected $mailer;

    public function setMailer(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    // ...
}

Para inyectar el servicio de esta manera, sólo tienes que hacer un pequeño cambio en la sintaxis de la definición del servicio:

# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
    # ...
    newsletter_manager.class: Acme\HelloBundle\Newsletter\NewsletterManager

services:
    my_mailer:
        # ...
    newsletter_manager:
        class:     %newsletter_manager.class%
        calls:
            - [setMailer, ["@my_mailer"]]
<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<parameters>
    <!-- ... -->
    <parameter key="newsletter_manager.class">Acme\HelloBundle\Newsletter\NewsletterManager</parameter>
</parameters>

<services>
    <service id="my_mailer" ...>
      <!-- ... -->
    </service>
    <service id="newsletter_manager" class="%newsletter_manager.class%">
        <call method="setMailer">
             <argument type="service" id="my_mailer" />
        </call>
    </service>
</services>
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

// ...
$container->setParameter(
    'newsletter_manager.class',
    'Acme\HelloBundle\Newsletter\NewsletterManager'
);

$container->setDefinition('my_mailer', ...);
$container->setDefinition('newsletter_manager', new Definition(
    '%newsletter_manager.class%'
))->addMethodCall('setMailer', array(
    new Reference('my_mailer'),
));

Nota Los dos métodos anteriores se denominan inyección en el constructor (constructor injection) e inyección por método setter (setter injection). El contenedor de servicios de Symfony2 también soporta la inyección mediante las propiedades de la clase (property injection).

16.6.3. Inyectando la petición

A partir de la versión 2.4 de Symfony, en vez de inyectar el servicio request en tus servicios, deberías inyectar el servicio request_stack y obtener la petición mediante el método getCurrentRequest():

namespace Acme\HelloBundle\Newsletter;

use Symfony\Component\HttpFoundation\RequestStack;

class NewsletterManager
{
    protected $requestStack;

    public function __construct(RequestStack $requestStack)
    {
        $this->requestStack = $requestStack;
    }

    public function anyMethod()
    {
        $request = $this->requestStack->getCurrentRequest();
        // ... do something with the request
    }

    // ...
}

Para inyectar el servicio request_stack, utiliza la misma sintaxis que para cualquier otro servicio:

# src/Acme/HelloBundle/Resources/config/services.yml
services:
    newsletter_manager:
        class:     Acme\HelloBundle\Newsletter\NewsletterManager
        arguments: ["@request_stack"]
<!-- src/Acme/HelloBundle/Resources/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 http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service
            id="newsletter_manager"
            class="Acme\HelloBundle\Newsletter\NewsletterManager"
        >
            <argument type="service" id="request_stack"/>
        </service>
    </services>
</container>
// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

// ...
$container->setDefinition('newsletter_manager', new Definition(
    'Acme\HelloBundle\Newsletter\NewsletterManager',
    array(new Reference('request_stack'))
));

Truco Si defines un controlador como servicio, puedes obtener el objeto Request que representa a la petición sin tener que inyectar el contenedor o sin tener que pasarlo como argumento del método de tu acción. Consulta la sección The Request as a Controller Argument¶ para saber cómo hacerlo.