Contenedor de servicios

Una moderna aplicación PHP 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 gestiona tu inventario de productos, u otro objeto que procesa 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.

En este capítulo, vamos a hablar de un objeto PHP especial en Symfony2 que te ayuda a crear una instancia, organizar y recuperar 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 como todas las clases 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.

¿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 más 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 tenemos una clase PHP simple que envía mensajes de correo electrónico. Sin un contenedor de servicios, debemos crear manualmente el objeto cada vez que lo necesitemos:

use Acme\HelloBundle\Mailer;

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

Esto es bastante fácil. La clase imaginaria Mailer nos permite configurar el método utilizado para entregar los mensajes de correo electrónico (por ejemplo, sendmail, smtp, etc.) ¿Pero qué si queremos utilizar el servicio cliente de correo en algún otro lugar? Desde luego, no queremos repetir la configuración del gestor de correo cada vez que tenemos que utilizar el objeto Mailer. ¿Qué pasa si necesitamos cambiar el transporte de sendmail a smtp en todas partes en la aplicación? Necesitaríamos cazar todos los lugares que crean un servicio Mailer y modificarlo.

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, debemos 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 pedimos el servicio my_mailer desde el contenedor, el contenedor construye el objeto y lo devuelve. Esta es otra de las principales ventajas de utilizar el contenedor de servicios. Es decir, un servicio nunca es construido 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 muy poca o ninguna. 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 casi siempre es el comportamiento que tendrá (el cual es más flexible y potente), pero vamos a aprender más adelante cómo puedes configurar un servicio que tiene varias instancias.

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 igual que antes —la diferencia es sólo en cómo definimos 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.

El propósito de los parámetros es alimentar información a los servicios. Por supuesto no había nada malo en la definición del 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 (vamos a mostrar esto en breve), utilizar parámetros permite que el servicio sea fácil de personalizar 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 no tienen que ser cadenas planas, sino que también pueden ser matrices. Para el formato XML, necesitas utilizar el atributo type="collection" para todos los parámetros que son arreglos.

  • YAML
    # app/config/config.yml
    parameters:
        my_mailer.gateways:
            - mail1
            - mail2
            - mail3
        my_multilang.language_fallback:
            en:
                - en
                - fr
            fr:
                - fr
                - en
    
  • XML
    <!-- app/config/config.xml -->
    <parameters>
        <parameter key="my_mailer.gateways" type="collection">
            <parameter>mail1</parameter>
            <parameter>mail2</parameter>
            <parameter>mail3</parameter>
        </parameter>
        <parameter key="my_multilang.language_fallback" type="collection">
            <parameter key="en" type="collection">
                <parameter>en</parameter>
                <parameter>fr</parameter>
            </parameter>
            <parameter key="fr" type="collection">
                <parameter>fr</parameter>
                <parameter>en</parameter>
            </parameter>
        </parameter>
    </parameters>
    
  • PHP
    // app/config/config.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $container->setParameter('my_mailer.gateways', array('mail1', 'mail2', 'mail3'));
    $container->setParameter('my_multilang.language_fallback',
                             array('en' => array('en', 'fr'),
                                   'fr' => array('fr', 'en'),
                            ));
    

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. Se trata de 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 en una u otra forma. Esto proporciona absoluta flexibilidad sobre los servicios en tu aplicación.

La configuración externa de servicios se puede importar de dos maneras diferentes. En primer lugar, vamos a hablar sobre el método que utilizarás con más frecuencia en tu aplicación: la directiva imports. En la siguiente sección, vamos a introducir el segundo método, que es el método más flexible y preferido para importar la configuración del servicio desde paquetes de terceros.

Importando configuración con imports

Hasta ahora, hemos puesto nuestra definición del contenedor del servicio 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:
        hello_bundle:
            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 muy parecido a lo que hemos 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 en el interior del contenedor de servicios FrameworkBundle:

  • YAML
    # app/config/config.yml
    framework:
        secret:          xxxxxxxxxx
        charset:         UTF-8
        form:            true
        csrf_protection: true
        router:        { resource: "%kernel.root_dir%/config/routing.yml" }
        # ...
    
  • XML
    <!-- app/config/config.xml -->
    <framework:config charset="UTF-8" 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',
        'charset'         => 'UTF-8',
        '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 la clave framework del archivo de configuración de tu aplicación por completo, 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 o realiza cualquier acción en que tú no tengas el control.

Por supuesto que puedes hacer mucho más que simplemente “activar” la extensión del contenedor de servicios del FrameworkBundle. 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 el juego de caracteres — charset, gestor de errores — error_handler, protección CSRFcsrf_protection, configuración del ruteador — 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 de cómo se deben instalar y configurar los servicios para el paquete. 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.

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 realiza cuando es necesario crear un servicio que depende de uno o varios otros servicios en el contenedor.

Comencemos con un ejemplo. Supongamos que tenemos 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 ya es realmente 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:

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:

public function sendNewsletterAction()
{
    $cartero = $this->get('mi_cartero');
    $newsletter = new Acme\HelloBundle\Newsletter\NewsletterManager($mailer);
    // ...
}

Este enfoque está bien, pero, ¿si más adelante decidimos que la clase NewsletterManager necesita un segundo o tercer argumento constructor? ¿Y si nos decidimos a reconstruir nuestro 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 nos 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, NewsletterManager también se debe escribir para permitir 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.

Podemos dar un paso más allá usando estos servicios dentro de los servicios que has creado para tu aplicación. Vamos a modificar 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 de que la entrada SwiftMailer aparece en la configuración de la aplicación. Como mencionamos en Importando configuración vía el contenedor de extensiones, la clave SwiftMailer invoca a la extensión de servicio desde SwiftmailerBundle, la cual registra el servicio mailer.

Configuración avanzada del contenedor

Como hemos visto, definir servicios dentro del contenedor es fácil, generalmente implica una clave de configuración service y algunos parámetros. Sin embargo, el contenedor cuenta con otras herramientas disponibles que te ayudan a etiquetar servicios por funcionalidad especial, crear servicios más complejos y realizar operaciones después de que el contenedor está construido.

Marcando servicios como públicos/privados

Cuando definas servicios, generalmente, querrás poder acceder a estas definiciones dentro del código de tu aplicación. Estos servicios se llaman public. Por ejemplo, el servicio doctrine registrado en el contenedor cuando se utiliza DoctrineBundle es un servicio público al que puedes acceder a través de:

$doctrine = $container->get('doctrine');

Sin embargo, hay casos de uso cuando no quieres que un servicio sea público. Esto es común cuando sólo se define un servicio, ya que se podría utilizar como argumento para otro servicio.

Nota

Si utilizas un servicio privado como argumento a más de otro servicio, esto se traducirá en dos diferentes instancias utilizadas como la creación del servicio privado realizada en línea (por ejemplo, new PrivateFooBar()).

Simplemente dice: El servicio será privado cuando no deseas acceder a él directamente desde tu código.

Aquí está un ejemplo:

  • YAML
    services:
       foo:
         class: Acme\HelloBundle\Foo
         public: false
    
  • XML
    <service id="foo" class="Acme\HelloBundle\Foo" public="false" />
    
  • PHP
    $definition = new Definition('Acme\HelloBundle\Foo');
    $definition->setPublic(false);
    $container->setDefinition('foo', $definition);
    

Ahora que el servicio es privado, no puedes llamar a:

$container->get('foo');

Sin embargo, si has marcado un servicio como privado, todavía puedes asignarle un alias (ve más abajo) para acceder a este servicio (a través del alias).

Nota

De manera predeterminada los servicios son públicos.

Rebautizando

Cuando utilizas el núcleo o paquetes de terceros dentro de tu aplicación, posiblemente desees utilizar métodos abreviados para acceder a algunos servicios. Puedes hacerlo rebautizándolos y, además, puedes incluso rebautizar servicios no públicos.

  • YAML
    services:
       foo:
         class: Acme\HelloBundle\Foo
       bar:
         alias: foo
    
  • XML
    <service id="foo" class="Acme\HelloBundle\Foo"/>
    
    <service id="bar" alias="foo" />
    
  • PHP
    $definition = new Definition('Acme\HelloBundle\Foo');
    $container->setDefinition('foo', $definition);
    
    $containerBuilder->setAlias('bar', 'foo');
    

Esto significa que cuando utilizas el contenedor directamente, puedes acceder al servicio foo al pedir el servicio bar así:

$container->get('bar'); // debería devolver el servicio foo

Incluyendo archivos

Puede haber casos de uso cuando necesites incluir otro archivo justo antes de cargar el servicio en sí. Para ello, puedes utilizar la directiva file.

  • YAML
    services:
       foo:
         class: Acme\HelloBundle\Foo\Bar
         file: %kernel.root_dir%/src/ruta/al/archivo/foo.php
  • XML
    <service id="foo" class="Acme\HelloBundle\Foo\Bar">
        <file>%kernel.root_dir%/src/ruta/al/archivo/foo.php</file>
    </service>
    
  • PHP
    $definition = new Definition('Acme\HelloBundle\Foo\Bar');
    $definition->setFile('%kernel.root_dir%/src/ruta/al/archivo/foo.php');
    $container->setDefinition('foo', $definition);
    

Ten en cuenta que internamente Symfony llama a la función PHP require_once, lo cual significa que el archivo se incluirá una sola vez por petición.

Etiquetas (tags)

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

  • assetic.filter
  • assetic.templating.php
  • data_collector
  • form.field_factory.guesser
  • kernel.cache_warmer
  • kernel.event_listener
  • monolog.logger
  • routing.loader
  • security.listener.factory
  • security.voter
  • templating.helper
  • twig.extension
  • translation.loader
  • validator.constraint_validator