Contenedor de servicios

Una aplicación PHP moderna está llena de objetos. Un objeto te puede facilitar la entrega de mensajes de correo electrónico, mientras que otro te puede permitir mantener información en una base de datos. En tu aplicación, puedes crear un objeto que gestione tu inventario de productos, y otro objeto que procese los datos de una API de terceros. El punto es que una aplicación moderna hace muchas cosas y está organizada en muchos objetos que se encargan de cada tarea.

Este capítulo es sobre un objeto PHP especial en Symfony2 que te ayuda a crear instancias, organizar y recuperar los muchos objetos de tu aplicación. Este objeto, llamado contenedor de servicios, te permitirá estandarizar y centralizar la forma en que se construyen los objetos en tu aplicación. El contenedor te facilita la vida, es superveloz, y enfatiza una arquitectura que promueve el código reutilizable y disociado. Y debido a que todas las clases de Symfony2 básicas usan el contenedor, aprenderás cómo ampliar, configurar y utilizar cualquier objeto en Symfony2. En gran parte, el contenedor de servicios es el mayor contribuyente a la velocidad y extensibilidad de Symfony2.

Por último, configurar y usar el contenedor de servicios es fácil. Al final de este capítulo, te sentirás cómodo creando tus propios objetos y personalizando objetos de cualquier paquete de terceros a través del contenedor. Empezarás a escribir código más reutilizable, comprobable y disociado, simplemente porque el contenedor de servicios facilita la escritura de buen código.

Truco

Si quieres conocer un poco más después de leer este capítulo, revisa la Documentación del componente de inyección de dependencias.

¿Qué es un servicio?

En pocas palabras, un Servicio es cualquier objeto PHP que realiza algún tipo de tarea «global». Es un nombre genérico que se utiliza a propósito en informática para describir un objeto creado para un propósito específico (por ejemplo, la entrega de mensajes de correo electrónico). Cada servicio se utiliza en toda tu aplicación cada vez que necesites la funcionalidad específica que proporciona. No tienes que hacer nada especial para hacer un servicio: simplemente escribe una clase PHP con algo de código que realice una tarea específica. ¡Felicidades, acabas de crear un servicio!

Nota

Por regla general, un objeto PHP es un servicio si se utiliza a nivel global en tu aplicación. Utilizamos un solo servicio Mailer a nivel global para enviar mensajes de correo electrónico mientras que muchos objetos Mensaje que este entrega no son servicios. Del mismo modo, un objeto Producto no es un servicio, pero un objeto que persiste objetos Producto a una base de datos es un servicio.

Entonces, ¿cuál es la ventaja? La ventaja de pensar en «servicios» es que comienzas a pensar en la separación de cada parte de la funcionalidad de tu aplicación como una serie de servicios. Puesto que cada servicio se limita a un trabajo, puedes acceder fácilmente a cada servicio y usar su funcionalidad siempre que la necesites. Cada servicio también se puede probar y configurar muy fácilmente, ya que está separado de la otra funcionalidad de tu aplicación. Esta idea se llama arquitectura orientada a servicios y no es única de Symfony2 e incluso de PHP. Estructurando tu aplicación en torno a un conjunto de clases Servicio independientes es una bien conocida y confiable práctica mejor orientada a objetos. Estas habilidades son clave para ser un buen desarrollador en casi cualquier lenguaje.

¿Qué es un contenedor de servicios?

Un Contenedor de servicios (o contenedor de inyección de dependencias) simplemente es un objeto PHP que gestiona la creación de instancias de servicios (es decir, objetos).

Por ejemplo, supongamos que tienes una simple clase PHP que envía mensajes de correo electrónico. Sin un contenedor de servicios, debes crear manualmente el objeto cada vez que lo necesites:

use Acme\HelloBundle\Mailer;

$mailer = new Mailer('sendmail');
$mailer->send('ryan@foobar.net', ...);

Esto es bastante fácil. La clase imaginaria Mailer te permite configurar el método utilizado para entregar los mensajes de correo electrónico (por ejemplo, sendmail, smtp, etc.). Pero, ¿si quisieras utilizar el servicio mailer en algún otro lugar? Desde luego, no quieres repetir la configuración del mailer cada vez que necesites utilizar el objeto Mailer. Qué pasa si necesitas cambiar el transporte de sendmail a smtp en toda tu aplicación? Tendrías que cazar cada sitio en que creas un servicio Mailer y cambiarlo.

Creando/configurando servicios en el contenedor

Una mejor respuesta es dejar que el contenedor de servicios cree el objeto Mailer para ti. Para que esto funcione, debes enseñar al contenedor cómo crear el servicio Mailer. Esto se hace a través de configuración, la cual se puede especificar en YAML, XML o PHP:

  • YAML
    # app/config/config.yml
    services:
        my_mailer:
            class:        Acme\HelloBundle\Mailer
            arguments:    [sendmail]
    
  • XML
    <!-- app/config/config.xml -->
        <services>
        <service id="my_mailer" class="Acme\HelloBundle\Mailer">
            <argument>sendmail</argument>
        </service>
        </services>
    
  • PHP
    // app/config/config.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $container->setDefinition('my_mailer', new Definition(
        'Acme\HelloBundle\Mailer',
        array('sendmail')
    ));
    

Nota

Cuando se inicia, por omisión Symfony2 construye el contenedor de servicios usando la configuración de (app/config/config.yml). El archivo exacto que se carga es dictado por el método AppKernel::registerContainerConfiguration(), el cual carga un archivo de configuración específico al entorno (por ejemplo, config_dev.yml para el entorno dev o config_prod.yml para prod).

Una instancia del objeto Acme\HelloBundle\Mailer ahora está disponible a través del contenedor de servicios. El contenedor está disponible en cualquier controlador tradicional de Symfony2, donde puedes acceder al servicio del contenedor a través del método get():

class HelloController extends Controller
{
    // ...

    public function sendEmailAction()
    {
        // ...
        $mailer = $this->get('my_mailer');
        $mailer->send('ryan@foobar.net', ...);
    }
}

Cuando solicites el servicio my_mailer desde el contenedor, el contenedor construye el objeto y te lo devuelve. Esta es otra de las principales ventajas de utilizar el contenedor de servicios. Es decir, un servicio nunca se construye hasta que es necesario. Si defines un servicio y no lo utilizas en una petición, el servicio no se crea. Esto ahorra memoria y aumenta la velocidad de tu aplicación. Esto también significa que la sanción en rendimiento por definir muchos servicios es mínima o inexistente. Los servicios que nunca se usan nunca se construyen.

Como bono adicional, el servicio Mailer se crea sólo una vez y esa misma instancia se vuelve a utilizar cada vez que solicites el servicio. Este es el comportamiento que —casi siempre— necesitarás (el cual es más flexible y potente), pero, más adelante aprenderás cómo puedes configurar un servicio que tiene varias instancias en el artículo «Cómo trabajar con ámbitos» del recetario.

Parámetros del servicio

La creación de nuevos servicios (es decir, objetos) a través del contenedor es bastante sencilla. Los parámetros provocan que al definir los servicios estén más organizados y sean más flexibles:

  • YAML
    # app/config/config.yml
    parameters:
        my_mailer.class:      Acme\HelloBundle\Mailer
        my_mailer.transport:  sendmail
    
    services:
        my_mailer:
            class:        "%my_mailer.class%"
            arguments:    ["%my_mailer.transport%"]
    
  • XML
    <!-- app/config/config.xml -->
    <parameters>
        <parameter key="my_mailer.class">Acme\HelloBundle\Mailer</parameter>
        <parameter key="my_mailer.transport">sendmail</parameter>
    </parameters>
    
        <services>
        <service id="my_mailer" class="%my_mailer.class%">
            <argument>%my_mailer.transport%</argument>
        </service>
        </services>
    
  • PHP
    // app/config/config.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $container->setParameter('my_mailer.class', 'Acme\HelloBundle\Mailer');
    $container->setParameter('my_mailer.transport', 'sendmail');
    
    $container->setDefinition('my_mailer', new Definition(
        '%my_mailer.class%',
        array('%my_mailer.transport%')
    ));
    

El resultado final es exactamente el mismo que antes —la diferencia sólo está en cómo defines el servicio—. Al rodear las cadenas my_mailer.class y my_mailer.transport entre signos de porcentaje (%), el contenedor sabe que tiene que buscar los parámetros con esos nombres. Cuando se construye el contenedor, este busca el valor de cada parámetro y lo utiliza en la definición del servicio.

Nuevo en la versión 2.1: Escapar el carácter @ en los valores de parámetros YAML es nuevo en Symfony 2.1.9 y Symfony 2.2.1.

Nota

Si quieres utilizar una cadena que comience con un signo de @ como valor del parámetro (es decir, una muy segura contraseña de correo) en un archivo yaml, necesitas escaparlo añadiendo otra @ (Esto sólo aplica al formato YAML):

# app/config/parameters.yml
parameters:
    # Esto será analizado como la cadena "@clavesegura"
    mailer_password: "@@clavesegura"

Nota

El signo de porcentaje en un parámetro o argumento, como parte de la cadena, se debe escapar con otro signo de porcentaje:

<argument type="string">http://symfony.com/?foo=%%s&bar=%%d</argument>

Prudencia

Puedes recibir una Symfony\Component\DependencyInjection\Exception\ScopeWideningInjectionException al pasar el servicio request como argumento. Para entender este mejor problema y aprender cómo solucionarlo, consulta el artículo Cómo trabajar con ámbitos en el recetario.

El propósito de los parámetros es alimentar información a los servicios. Por supuesto, no hay nada malo en definir el servicio sin utilizar ningún parámetro. Los parámetros, sin embargo, tienen varias ventajas:

  • Separan y organizan todo el servicio en «opciones» bajo una sola clave parameters;
  • Los valores del parámetro se pueden utilizar en la definición de múltiples servicios;
  • Cuando creas un servicio en un paquete (mostrado en breve), el uso de parámetros facilita la personalización del servicio en tu aplicación.

La opción de usar o no parámetros depende de ti. Los paquetes de terceros de alta calidad siempre usan parámetros, ya que producen servicios más configurables almacenados en el contenedor. Para los servicios de tu aplicación, sin embargo, posiblemente no necesites la flexibilidad de los parámetros.

Arreglo de parámetros

Los parámetros también pueden contener arreglos como valores. Ve Array Parameters.

Importando la configuración de recursos desde otros contenedores

Truco

En esta sección, nos referiremos a los archivos de configuración de servicios como recursos. Esto es para resaltar el hecho de que, si bien la mayoría de la configuración de recursos debe estar en archivos (por ejemplo, YAML, XML, PHP), Symfony2 es tan flexible que la configuración se puede cargar desde cualquier lugar (por ejemplo, una base de datos e incluso a través de un servicio web externo).

El contenedor de servicios se construye usando un recurso de configuración simple (app/config/config.yml por omisión). Toda la configuración de otros servicios (incluido el núcleo de Symfony2 y la configuración de paquetes de terceros) se debe importar desde el interior de este archivo de una u otra manera. Esto proporciona absoluta flexibilidad sobre los servicios en tu aplicación.

La configuración externa de servicios se puede importar de dos diferentes maneras. La primera — y el método más común— es vía la directiva imports. Más tarde, veremos el segundo método, el cual es el método más flexible y preferido para importar la configuración de servicios desde paquetes de terceros.

Importando configuración con imports

Hasta ahora, has puesto tu definición del contenedor del servicios my_mailer directamente en el archivo de configuración de la aplicación (por ejemplo, app/config/config.yml). Por supuesto, debido a que la clase Mailer vive dentro de AcmeHelloBundle, también tiene más sentido poner la definición del contenedor de my_mailer en el paquete.

En primer lugar, mueve la definición del contenedor de my_mailer a un nuevo archivo contenedor de recursos dentro de AcmeHelloBundle. Si los directorios Resources y Resources/config no existen, créalos.

  • YAML
    # src/Acme/HelloBundle/Resources/config/services.yml
    parameters:
        my_mailer.class:      Acme\HelloBundle\Mailer
        my_mailer.transport:  sendmail
    
    services:
        my_mailer:
            class:        "%my_mailer.class%"
            arguments:    ["%my_mailer.transport%"]
    
  • XML
    <!-- src/Acme/HelloBundle/Resources/config/services.xml -->
    <parameters>
        <parameter key="my_mailer.class">Acme\HelloBundle\Mailer</parameter>
        <parameter key="my_mailer.transport">sendmail</parameter>
    </parameters>
    
        <services>
        <service id="my_mailer" class="%my_mailer.class%">
            <argument>%my_mailer.transport%</argument>
        </service>
        </services>
    
  • PHP
    // src/Acme/HelloBundle/Resources/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $container->setParameter('my_mailer.class', 'Acme\HelloBundle\Mailer');
    $container->setParameter('my_mailer.transport', 'sendmail');
    
    $container->setDefinition('my_mailer', new Definition(
        '%my_mailer.class%',
        array('%my_mailer.transport%')
    ));
    

La propia definición no ha cambiado, sólo su ubicación. Por supuesto, el contenedor de servicios no sabe sobre el nuevo archivo de recursos. Afortunadamente, es fácil importar el archivo de recursos utilizando la clave imports en la configuración de la aplicación.

  • YAML
    # app/config/config.yml
    imports:
        - { resource: "@AcmeHelloBundle/Resources/config/services.yml" }
    
  • XML
    <!-- app/config/config.xml -->
    <imports>
        <import resource="@AcmeHelloBundle/Resources/config/services.xml"/>
    </imports>
    
  • PHP
    // app/config/config.php
    $this->import('@AcmeHelloBundle/Resources/config/services.php');
    

La directiva imports permite a tu aplicación incluir recursos de configuración del contenedor de servicios desde cualquier otro lugar (comúnmente desde paquetes). La ubicación de resources, para archivos, es la ruta absoluta al archivo de recursos. La sintaxis especial @AcmeHello resuelve la ruta al directorio del paquete AcmeHelloBundle. Esto te ayuda a especificar la ruta a los recursos sin tener que preocuparte más adelante de si se mueve el AcmeHelloBundle a un directorio diferente.

Importando configuración vía el contenedor de extensiones

Cuando desarrollas en Symfony2, comúnmente debes usar la directiva imports para importar la configuración del contenedor desde los paquetes que has creado específicamente para tu aplicación. La configuración del contenedor de paquetes de terceros, incluyendo los servicios básicos de Symfony2, normalmente se carga con cualquier otro método que sea más flexible y fácil de configurar en tu aplicación.

Así es como funciona. Internamente, cada paquete define sus servicios de manera muy parecida a lo que has visto hasta ahora. Es decir, un paquete utiliza uno o más archivos de configuración de recursos (por lo general XML) para especificar los parámetros y servicios para ese paquete. Sin embargo, en lugar de importar cada uno de estos recursos directamente desde la configuración de tu aplicación utilizando la directiva imports, sólo tienes que invocar una extensión contenedora de servicios dentro del paquete, la cual hace el trabajo por ti. Una extensión del contenedor de servicios es una clase PHP creada por el autor del paquete para lograr dos cosas:

  • Importar todos los recursos del contenedor de servicios necesarios para configurar los servicios del paquete;
  • Permitir una configuración semántica y directa para poder ajustar el paquete sin interactuar con los parámetros planos de configuración del paquete contenedor del servicio.

En otras palabras, una extensión del contenedor de servicios configura los servicios para un paquete en tu nombre. Y como veremos en un momento, la extensión proporciona una interfaz sensible y de alto nivel para configurar el paquete.

Tomemos el FrameworkBundle —el núcleo de la plataforma Symfony2— como ejemplo. La presencia del siguiente código en la configuración de tu aplicación invoca a la extensión FrameworkBundle en el interior del contenedor de servicios:

  • YAML
    # app/config/config.yml
    framework:
        secret:          xxxxxxxxxx
        form:            true
        csrf_protection: true
        router:        { resource: "%kernel.root_dir%/config/routing.yml" }
        # ...
    
  • XML
    <!-- app/config/config.xml -->
    <framework:config secret="xxxxxxxxxx">
        <framework:form />
        <framework:csrf-protection />
        <framework:router resource="%kernel.root_dir%/config/routing.xml" />
        <!-- ... -->
    </framework>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        'secret'          => 'xxxxxxxxxx',
        'form'            => array(),
        'csrf-protection' => array(),
        'router'          => array('resource' => '%kernel.root_dir%/config/routing.php'),
    
        // ...
    ));
    

Cuando se analiza la configuración, el contenedor busca una extensión que pueda manejar la directiva de configuración framework. La extensión en cuestión, que vive en el FrameworkBundle, es invocada y cargada la configuración del servicio para el FrameworkBundle. Si quitas por completo la clave framework del archivo de configuración de tu aplicación, no se cargarán los servicios básicos de Symfony2. El punto es que tú tienes el control: la plataforma Symfony2 no contiene ningún tipo de magia ni realiza alguna acción en que tú no tengas el control.

Por supuesto, puedes hacer mucho más sencilla la «activación» de la extensión FrameworkBundle en el contenedor de servicios. Cada extensión te permite personalizar fácilmente el paquete, sin tener que preocuparte acerca de cómo se definen los servicios internos.

En este caso, la extensión te permite personalizar la configuración de error_handler, csrf_protection, router y mucho más. Internamente, el FrameworkBundle utiliza las opciones especificadas aquí para definir y configurar los servicios específicos del mismo. El paquete se encarga de crear todos los parámetros y servicios necesarios para el contenedor de servicios, mientras permite que la mayor parte de la configuración se pueda personalizar fácilmente. Como bono adicional, la mayoría de las extensiones del contenedor de servicios también son lo suficientemente inteligentes como para realizar la validación —notificándote opciones omitidas o datos de tipo incorrecto.

Al instalar o configurar un paquete, consulta la documentación del paquete sobre cómo se deben instalar y configurar sus servicios. Las opciones disponibles para los paquetes básicos se pueden encontrar dentro de la Guía de Referencia.

Nota

Nativamente, el contenedor de servicios sólo reconoce las directivas parameters, services e imports. Cualquier otra directiva es manejada por una extensión del contenedor de servicios.

Si quieres exponer la configuración fácil en tus propios paquetes, lee «Cómo exponer la configuración semántica de un paquete» del recetario.

Refiriendo (inyectando) servicios

Hasta el momento, nuestro servicio original my_mailer es simple: sólo toma un argumento en su constructor, el cual es fácilmente configurable. Como verás, el poder real del contenedor se lleva a cabo cuando es necesario crear un servicio que depende de uno o varios otros servicios en el contenedor.

Por ejemplo, supongamos que tienes un nuevo servicio, NewsletterManager, que ayuda a gestionar la preparación y entrega de un mensaje de correo electrónico a una colección de direcciones. Por supuesto el servicio my_mailer ciertamente ya es bueno en la entrega de mensajes de correo electrónico, así que lo usaremos dentro de NewsletterManager para manejar la entrega real de los mensajes. Se pretende que esta clase pudiera ser algo como esto:

// 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, podemos crear un nuevo NewsletterManager muy fácilmente desde el interior de un controlador:

use Acme\HelloBundle\Newsletter\NewsletterManager;

// ...

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

Este enfoque está bien, pero, ¿si más adelante decides que la clase NewsletterManager necesita un segundo o tercer argumento 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. Por supuesto, el contenedor de servicios te da una opción mucho más atractiva:

  • YAML
    # 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"]
  • XML
    <!-- 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>
  • PHP
    // 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. En este caso, sin embargo, el servicio especificado my_mailer debe existir. Si no es así, lanzará una excepción. Puedes marcar tus dependencias como opcionales — explicaremos esto en la siguiente sección.

La utilización de referencias es una herramienta muy poderosa que te permite crear clases de servicios 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 crear instancias de objetos.

Dependencias opcionales: Inyección del definidor

Inyectar dependencias en el constructor de esta manera es una excelente manera de asegurarte que la dependencia está disponible para usarla. Si tienes dependencias opcionales para una clase, entonces, la «inyección del definidor» puede ser una mejor opción. Esto significa inyectar la dependencia usando una llamada a método en lugar de a través del constructor. La clase se vería así:

namespace Acme\HelloBundle\Newsletter;

use Acme\HelloBundle\Mailer;

class NewsletterManager
{
    protected $mailer;

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

    // ...
}

La inyección de la dependencia por medio del método definidor sólo necesita un cambio de sintaxis:

  • YAML
    # 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"]]
  • XML
    <!-- 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>
  • PHP
    // 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 enfoques presentados en esta sección se llaman «inyección de constructor» e «inyección de definidor». El contenedor de servicios de Symfony2 también es compatible con la «inyección de propiedad».

Haciendo que las referencias sean opcionales

A veces, uno de tus servicios puede tener una dependencia opcional, lo cual significa que la dependencia no es necesaria para que el servicio funcione correctamente. En el ejemplo anterior, el servicio my_mailer debe existir, si no, será lanzada una excepción. Al modificar la definición del servicio newsletter_manager, puedes hacer opcional esta referencia. Entonces, el contenedor será inyectado si es que existe y no hace nada si no:

  • YAML
    # src/Acme/HelloBundle/Resources/config/services.yml
    parameters:
        # ...
    
    services:
        newsletter_manager:
            class:     %newsletter_manager.class%
            arguments: ["@?my_mailer"]
  • XML
    <!-- src/Acme/HelloBundle/Resources/config/services.xml -->
    
        <services>
        <service id="my_mailer" ...>
          <!-- ... -->
        </service>
        <service id="newsletter_manager" class="%newsletter_manager.class%">
            <argument type="service" id="my_mailer" on-invalid="ignore" />
        </service>
        </services>
  • PHP
    // src/Acme/HelloBundle/Resources/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    use Symfony\Component\DependencyInjection\ContainerInterface;
    
    // ...
    $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',
                ContainerInterface::IGNORE_ON_INVALID_REFERENCE
            )
        )
    ));
    

En YAML, la sintaxis especial @? le dice al contenedor de servicios que la dependencia es opcional. Por supuesto, también debes escribir NewsletterManager para que permita una dependencia opcional:

public function __construct(Mailer $mailer = null)
{
    // ...
}

El núcleo de Symfony y servicios en paquetes de terceros

Puesto que Symfony2 y todos los paquetes de terceros configuran y recuperan sus servicios a través del contenedor, puedes acceder fácilmente a ellos e incluso utilizarlos en tus propios servicios. Para mantener las cosas simples, de manera predeterminada Symfony2 no requiere que los controladores se definan como servicios. Además Symfony2 inyecta el contenedor de servicios completo en el controlador. Por ejemplo, para manejar el almacenamiento de información sobre la sesión de un usuario, Symfony2 proporciona un servicio sesión, al cual se puede acceder dentro de un controlador estándar de la siguiente manera:

public function indexAction($bar)
{
    $session = $this->get('session');
    $session->set('foo', $bar);

    // ...
}

En Symfony2, constantemente vas a utilizar los servicios prestados por el núcleo de Symfony o paquetes de terceros para realizar tareas como la reproducción de plantillas (templating), el envío de mensajes de correo electrónico (mailer), o para acceder a información sobre la petición.

Puedes dar un paso más allá usando estos servicios dentro de los servicios que has creado para tu aplicación. Empieza modificando el NewsletterManager para usar el gestor de correo real de Symfony2, el servicio mailer (en vez del pretendido my_mailer). También vamos a pasar el servicio del motor de plantillas al NewsletterManager para que puedas generar el contenido del correo electrónico a través de una plantilla:

namespace Acme\HelloBundle\Newsletter;

use Symfony\Component\Templating\EngineInterface;

class NewsletterManager
{
    protected $mailer;

    protected $templating;

    public function __construct(\Swift_Mailer $mailer, EngineInterface $templating)
    {
        $this->mailer = $mailer;
        $this->templating = $templating;
    }

    // ...
}

Configurar el contenedor de servicios es fácil:

  • YAML
    services:
        newsletter_manager:
            class:     %newsletter_manager.class%
            arguments: ["@mailer", "@templating"]
  • XML
    <service id="newsletter_manager" class="%newsletter_manager.class%">
        <argument type="service" id="mailer"/>
        <argument type="service" id="templating"/>
    </service>
    
  • PHP
    $container->setDefinition('newsletter_manager', new Definition(
        '%newsletter_manager.class%',
        array(
            new Reference('mailer'),
            new Reference('templating'),
        )
    ));
    

El servicio newsletter_manager ahora tiene acceso a los servicios del núcleo mailer y templating. Esta es una forma común de crear servicios específicos para tu aplicación que aprovechan el poder de los distintos servicios en la plataforma.

Truco

Asegúrate que la clave swiftmailer aparece en la configuración de tu aplicación. Como se mencionó en Importando configuración vía el contenedor de extensiones, la clave SwiftMailer invoca a la extensión de servicio desde el SwiftmailerBundle, el cual registra el servicio mailer.

Etiquetas

De la misma manera que en la Web una entrada de blog se puede etiquetar con cosas tales como «Symfony» o «PHP», los servicios configurados en el contenedor también se pueden etiquetar. En el contenedor de servicios, una etiqueta implica que el servicio está destinado a usarse para un propósito específico. Tomemos el siguiente ejemplo:

  • YAML
    services:
        foo.twig.extension:
            class: Acme\HelloBundle\Extension\FooExtension
            tags:
                -  { name: twig.extension }
    
  • XML
    <service id="foo.twig.extension" class="Acme\HelloBundle\Extension\FooExtension">
        <tag name="twig.extension" />
    </service>
    
  • PHP
    $definition = new Definition('Acme\HelloBundle\Extension\FooExtension');
    $definition->addTag('twig.extension');
    $container->setDefinition('foo.twig.extension', $definition);
    

La etiqueta twig.extension es una etiqueta especial que TwigBundle usa durante la configuración. Al dar al servicio esta etiqueta twig.extension, el paquete sabe que el servicio foo.twig.extension se debe registrar como una extensión Twig con Twig. En otras palabras, Twig encuentra todos los servicios con la etiqueta twig.extension y automáticamente los registra como extensiones.

Las etiquetas, entonces, son una manera de decirle a Symfony2 u otros paquetes de terceros que el paquete se debe registrar o utilizar de alguna forma especial.

La siguiente es una lista de etiquetas disponibles con los paquetes del núcleo de Symfony2. Cada una de ellas tiene un efecto diferente en tu servicio y muchas etiquetas requieren argumentos adicionales (más allá de sólo el parámetro name).

Para una lista de todas las etiquetas disponibles en el núcleo de la plataforma Symfony, revisa las Etiquetas de inyección de dependencias.

Depurando servicios

Puedes descubrir los servicios que están registrados con el contenedor utilizando la consola. Para mostrar todos los servicios y la clase de cada servicio, ejecuta:

$ php app/console container:debug

De manera predefinida únicamente se muestran los servicios públicos, pero también puedes ver servicios privados:

$ php app/console container:debug --show-private

Puedes conseguir información más detallada sobre un servicio particular especificando su id:

$ php app/console container:debug my_mailer
Bifúrcame en GitHub