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 cuyoid
se indica.parameter
, devuelve el valor del parámetro indicado (la sintaxis es idéntica a la de la funciónservice
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.