Trabajando con servicios etiquetados

Las etiquetas son una cadena genérica (junto con algunas opciones) que puedes aplicar a cualquier servicio. Por sí mismas, las etiquetas en realidad no hacen la funcionalidad de tu servicio en modo alguno. Pero si quieres, puedes pedir al constructor del contenedor que consiga una lista de todos los servicios que estén etiquetados con alguna etiqueta específica. Esto es útil en los pases del compilador donde puedes encontrar estos servicios y usarlos o modificarlos de alguna manera específica.

Por ejemplo, si estás usando Swift Mailer te puedes imaginar que quieres implementar una «cadena de transporte», que es una colección de clases que implementan \Swift_Transport. Usando la cadena, querrá que Swift Mailer pruebe varias maneras de transportar el mensaje hasta que una lo consiga.

En primer lugar, define la clase TransportChain:

class TransportChain
{
    private $transports;

    public function __construct()
    {
        $this->transports = array();
    }

    public function addTransport(\Swift_Transport  $transport)
    {
        $this->transports[] = $transport;
    }
}

Entonces, define la cadena como un servicio:

  • YAML
    parameters:
        acme_mailer.transport_chain.class: TransportChain
    
    services:
        acme_mailer.transport_chain:
            class: "%acme_mailer.transport_chain.class%"
    
  • XML
    <parameters>
        <parameter key="acme_mailer.transport_chain.class">TransportChain</parameter>
    </parameters>
    
        <services>
        <service id="acme_mailer.transport_chain" class="%acme_mailer.transport_chain.class%" />
        </services>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    
    $container->setParameter('acme_mailer.transport_chain.class', 'TransportChain');
    
    $container->setDefinition('acme_mailer.transport_chain', new Definition('%acme_mailer.transport_chain.class%'));
    

Definiendo servicios con una etiqueta personalizada

Ahora, posiblemente quieras que varias de las clases \Swift_Transport sean creadas y añadidas automáticamente a la cadena usando el método addTransport(). Como ejemplo, puedes añadir los siguientes transportes como servicios:

  • YAML
    services:
        acme_mailer.transport.smtp:
            class: \Swift_SmtpTransport
            arguments:
                - "%mailer_host%"
            tags:
                -  { name: acme_mailer.transport }
        acme_mailer.transport.sendmail:
            class: \Swift_SendmailTransport
            tags:
                -  { name: acme_mailer.transport }
    
  • XML
    <service id="acme_mailer.transport.smtp" class="\Swift_SmtpTransport">
        <argument>%mailer_host%</argument>
        <tag name="acme_mailer.transport" />
    </service>
    
    <service id="acme_mailer.transport.sendmail" class="\Swift_SendmailTransport">
        <tag name="acme_mailer.transport" />
    </service>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    
    $definitionSmtp = new Definition('\Swift_SmtpTransport', array('%mailer_host%'));
    $definitionSmtp->addTag('acme_mailer.transport');
    $container->setDefinition('acme_mailer.transport.smtp', $definitionSmtp);
    
    $definitionSendmail = new Definition('\Swift_SendmailTransport');
    $definitionSendmail->addTag('acme_mailer.transport');
    $container->setDefinition('acme_mailer.transport.sendmail', $definitionSendmail);
    

Ten en cuenta que a cada uno se le dio una etiqueta llamada acme_mailer.transport. Esta es la etiqueta personalizada que utilizarás en tu pase del compilador. El pase del compilador es lo que da algún «significado» a la etiqueta.

Crear un CompilerPass

Tu pase del compilador ahora puede pedir al contenedor cualquier servicio con la etiqueta personalizada:

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;

class TransportCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (!$container->hasDefinition('acme_mailer.transport_chain')) {
            return;
        }

        $definition = $container->getDefinition(
            'acme_mailer.transport_chain'
        );

        $taggedServices = $container->findTaggedServiceIds(
            'acme_mailer.transport'
        );
        foreach ($taggedServices as $id => $attributes) {
            $definition->addMethodCall(
                'addTransport',
                array(new Reference($id))
            );
        }
    }
}

El método process() comprueba la existencia del servicio acme_mailer.transport_chain, a continuación, busca todos los servicios etiquetados cómo acme_mailer.transport. Los añade a la definición del servicio acme_mailer.transport_chain llamando a addTransport() por cada servicios "acme_mailer.transport" que haya encontrado. El primer argumento de cada una de estas llamadas será el servicio de transporte de correo en sí mismo.

Registrando el pase en el contenedor

También necesitas registrar el pase en el contenedor, entonces se ejecutará al compilar el contenedor:

use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();
$container->addCompilerPass(new TransportCompilerPass);

Nota

Los pases del compilador son registrados de manera diferente si estás usando la pila completa de la plataforma. Ve Cómo trabajan los pases del compilador en los paquetes para más detalles.

Añadiendo atributos adicionales en etiquetas

Algunas veces necesitarás información adicional acerca de cada servicio que se ha marcado con tu etiqueta. Por ejemplo, posiblemente quieras agregar un alias a cada TransportChain.

Para empezar, cambia la clase TransportChain:

class TransportChain
{
    private $transports;

    public function __construct()
    {
        $this->transports = array();
    }

    public function addTransport(\Swift_Transport $transport, $alias)
    {
        $this->transports[$alias] = $transport;
    }

    public function getTransport($alias)
    {
        if (array_key_exists($alias, $this->transports)) {
           return $this->transports[$alias];
        }
        else {
           return;
        }
    }
}

Como puedes ver, cuando se llama a addTransport, no sólo necesita un objeto Swift_Transport, sino también una cadena con el alias del transporte. Por lo tanto, ¿cómo podemos permitir que cada servicio etiquetado como transporte también suministre un alias?

Para responder a esto, cambia la declaración del servicio:

  • YAML
    services:
        acme_mailer.transport.smtp:
            class: \Swift_SmtpTransport
            arguments:
                - "%mailer_host%"
            tags:
                -  { name: acme_mailer.transport, alias: foo }
        acme_mailer.transport.sendmail:
            class: \Swift_SendmailTransport
            tags:
                -  { name: acme_mailer.transport, alias: bar }
    
  • XML
    <service id="acme_mailer.transport.smtp" class="\Swift_SmtpTransport">
        <argument>%mailer_host%</argument>
        <tag name="acme_mailer.transport" alias="foo" />
    </service>
    
    <service id="acme_mailer.transport.sendmail" class="\Swift_SendmailTransport">
        <tag name="acme_mailer.transport" alias="bar" />
    </service>
    

Ten en cuenta que agregaste una clave genérica alias a la etiqueta. Para utilizarla efectivamente, actualiza el compilador:

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;

class TransportCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (!$container->hasDefinition('acme_mailer.transport_chain')) {
            return;
        }

        $definition = $container->getDefinition(
            'acme_mailer.transport_chain'
        );

        $taggedServices = $container->findTaggedServiceIds(
            'acme_mailer.transport'
        );
        foreach ($taggedServices as $id => $tagAttributes) {
            foreach ($tagAttributes as $attributes) {
                $definition->addMethodCall(
                    'addTransport',
                    array(new Reference($id), $attributes["alias"])
                );
            }
        }
    }
}

La parte más complicada es la variable $attributes. Debido a que puedes utilizar la misma etiqueta muchas veces en el mismo servicio (por ejemplo, en teoría, podrías etiquetar el mismo servicio 5 veces con la etiqueta acme_mailer.transport), $attributes es un arreglo con información de la etiqueta de cada una de las etiquetas en ese servicio.

Bifúrcame en GitHub