Ver índice de contenidos del libro

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. 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).