Gestionando dependencias comunes con servicios padre

A medida que agregas más funcionalidad a tu aplicación, puedes comenzar a tener clases relacionadas que comparten algunas de las mismas dependencias. Por ejemplo, puedes tener un gestor de boletines que utiliza inyección para definir sus dependencias:

class NewsletterManager
{
    protected $mailer;
    protected $emailFormatter;

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

    public function setEmailFormatter(EmailFormatter $emailFormatter)
    {
        $this->emailFormatter = $emailFormatter;
    }

    // ...
}

y también una clase para tus Tarjetas de saludo que comparte las mismas dependencias:

class GreetingCardManager
{
    protected $mailer;
    protected $emailFormatter;

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

    public function setEmailFormatter(EmailFormatter $emailFormatter)
    {
        $this->emailFormatter = $emailFormatter;
    }

    // ...
}

La configuración del servicio de estas clases se vería algo como esto:

  • YAML
    parameters:
        # ...
        newsletter_manager.class: NewsletterManager
        greeting_card_manager.class: GreetingCardManager
    services:
        my_mailer:
            # ...
        my_email_formatter:
            # ...
        newsletter_manager:
            class:     %newsletter_manager.class%
            calls:
                - [setMailer, ["@my_mailer"]]
                - [setEmailFormatter, ["@my_email_formatter"]]
    
        greeting_card_manager:
            class:     "%greeting_card_manager.class%"
            calls:
                - [setMailer, ["@my_mailer"]]
                - [setEmailFormatter, ["@my_email_formatter"]]
  • XML
    <parameters>
        <!-- ... -->
        <parameter key="newsletter_manager.class">NewsletterManager</parameter>
        <parameter key="greeting_card_manager.class">GreetingCardManager</parameter>
    </parameters>
    
        <services>
        <service id="my_mailer" ...>
          <!-- ... -->
        </service>
        <service id="my_email_formatter" ...>
          <!-- ... -->
        </service>
        <service id="newsletter_manager" class="%newsletter_manager.class%">
            <call method="setMailer">
                 <argument type="service" id="my_mailer" />
            </call>
            <call method="setEmailFormatter">
                 <argument type="service" id="my_email_formatter" />
            </call>
        </service>
        <service id="greeting_card_manager" class="%greeting_card_manager.class%">
            <call method="setMailer">
                 <argument type="service" id="my_mailer" />
            </call>
            <call method="setEmailFormatter">
                 <argument type="service" id="my_email_formatter" />
            </call>
        </service>
        </services>
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    
    // ...
    $container->setParameter('newsletter_manager.class', 'NewsletterManager');
    $container->setParameter('greeting_card_manager.class', 'GreetingCardManager');
    
    $container->setDefinition('my_mailer', ...);
    $container->setDefinition('my_email_formatter', ...);
    $container->setDefinition('newsletter_manager', new Definition(
        '%newsletter_manager.class%'
    ))->addMethodCall('setMailer', array(
        new Reference('my_mailer')
    ))->addMethodCall('setEmailFormatter', array(
        new Reference('my_email_formatter')
    ));
    $container->setDefinition('greeting_card_manager', new Definition(
        '%greeting_card_manager.class%'
    ))->addMethodCall('setMailer', array(
        new Reference('my_mailer')
    ))->addMethodCall('setEmailFormatter', array(
        new Reference('my_email_formatter')
    ));
    

Hay mucha repetición, tanto en las clases como en la configuración. Esto significa que si cambias, por ejemplo, las clases de correo de la aplicación Mailer de EmailFormatter para inyectarlas a través del constructor, tendrías que actualizar la configuración en dos lugares. Del mismo modo, si necesitas hacer cambios en los métodos definidores tendrías que hacerlo en ambas clases. La forma típica de hacer frente a los métodos comunes de estas clases relacionadas es extraerlas en una superclase:

abstract class MailManager
{
    protected $mailer;
    protected $emailFormatter;

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

    public function setEmailFormatter(EmailFormatter $emailFormatter)
    {
        $this->emailFormatter = $emailFormatter;
    }

    // ...
}

Entonces NewsletterManager y GreetingCardManager pueden extender esta superclase:

class NewsletterManager extends MailManager
{
    // ...
}

y:

class GreetingCardManager extends MailManager
{
    // ...
}

De manera similar, el contenedor de servicios de Symfony2 también apoya la extensión de servicios en la configuración por lo que también puedes reducir la repetición especificando un padre para un servicio.

  • YAML
    parameters:
        # ...
        newsletter_manager.class: NewsletterManager
        greeting_card_manager.class: GreetingCardManager
    services:
        my_mailer:
            # ...
        my_email_formatter:
            # ...
        mail_manager:
            abstract:  true
            calls:
                - [setMailer, ["@my_mailer"]]
                - [setEmailFormatter, ["@my_email_formatter"]]
    
        newsletter_manager:
            class:     "%newsletter_manager.class%"
            parent: mail_manager
    
        greeting_card_manager:
            class:     "%greeting_card_manager.class%"
            parent: mail_manager
    
  • XML
    <parameters>
        <!-- ... -->
        <parameter key="newsletter_manager.class">NewsletterManager</parameter>
        <parameter key="greeting_card_manager.class">GreetingCardManager</parameter>
    </parameters>
    
        <services>
        <service id="my_mailer" ...>
          <!-- ... -->
        </service>
        <service id="my_email_formatter" ...>
          <!-- ... -->
        </service>
        <service id="mail_manager" abstract="true">
            <call method="setMailer">
                 <argument type="service" id="my_mailer" />
            </call>
            <call method="setEmailFormatter">
                 <argument type="service" id="my_email_formatter" />
            </call>
        </service>
        <service id="newsletter_manager" class="%newsletter_manager.class%" parent="mail_manager"/>
        <service id="greeting_card_manager" class="%greeting_card_manager.class%" parent="mail_manager"/>
        </services>
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\DefinitionDecorator;
    use Symfony\Component\DependencyInjection\Reference;
    
    // ...
    $container->setParameter('newsletter_manager.class', 'NewsletterManager');
    $container->setParameter('greeting_card_manager.class', 'GreetingCardManager');
    
    $container->setDefinition('my_mailer', ...);
    $container->setDefinition('my_email_formatter', ...);
    $container->setDefinition('mail_manager', new Definition(
    ))->setAbstract(
        true
    )->addMethodCall('setMailer', array(
        new Reference('my_mailer')
    ))->addMethodCall('setEmailFormatter', array(
        new Reference('my_email_formatter')
    ));
    $container->setDefinition('newsletter_manager', new DefinitionDecorator(
        'mail_manager'
    ))->setClass(
        '%newsletter_manager.class%'
    );
    $container->setDefinition('greeting_card_manager', new DefinitionDecorator(
        'mail_manager'
    ))->setClass(
        '%greeting_card_manager.class%'
    );
    

En este contexto, tener un servicio padre implica que los argumentos y las llamadas a métodos del servicio padre se deben utilizar en los servicios descendientes. En concreto, los métodos definidores especificados para el servicio padre serán llamados cuando se crean instancias del servicio descendiente.

Nota

Si quitas la clave de configuración del padre, el servicio todavía seguirá siendo una instancia, por supuesto, extendiendo la clase MailManager. La diferencia es que la omisión del padre en la clave de configuración significa que las llamadas definidas en el servicio mail_manager no se ejecutarán al crear instancias de los servicios descendientes.

The parent service is abstract as it should not be directly retrieved from the container or passed into another service. It exists merely as a “template” that other services can use. This is why it can have no class configured which would cause an exception to be raised for a non-abstract service.

Nota

In order for parent dependencies to resolve, the ContainerBuilder must first be compiled. See Compilando el contenedor for more details.

Sustituyendo dependencias del padre

Puede haber ocasiones en las que deses sustituir cual clase se pasa a una dependencia en un único servicio hijo. Afortunadamente, añadiendo la llamada al método de configuración para el servicio hijo, se sustituyen las dependencias establecidas por la clase principal. Así que si necesitas pasar una dependencia diferente sólo para la clase NewsletterManager, la configuración sería la siguiente:

  • YAML
    parameters:
        # ...
        newsletter_manager.class: NewsletterManager
        greeting_card_manager.class: GreetingCardManager
    services:
        my_mailer:
            # ...
        my_alternative_mailer:
            # ...
        my_email_formatter:
            # ...
        mail_manager:
            abstract:  true
            calls:
                - [setMailer, ["@my_mailer"]]
                - [setEmailFormatter, ["@my_email_formatter"]]
    
        newsletter_manager:
            class:     "%newsletter_manager.class%"
            parent: mail_manager
            calls:
                - [setMailer, ["@my_alternative_mailer"]]
    
        greeting_card_manager:
            class:     "%greeting_card_manager.class%"
            parent: mail_manager
    
  • XML
    <parameters>
        <!-- ... -->
        <parameter key="newsletter_manager.class">NewsletterManager</parameter>
        <parameter key="greeting_card_manager.class">GreetingCardManager</parameter>
    </parameters>
    
        <services>
        <service id="my_mailer" ...>
          <!-- ... -->
        </service>
        <service id="my_alternative_mailer" ...>
          <!-- ... -->
        </service>
        <service id="my_email_formatter" ...>
          <!-- ... -->
        </service>
        <service id="mail_manager" abstract="true">
            <call method="setMailer">
                 <argument type="service" id="my_mailer" />
            </call>
            <call method="setEmailFormatter">
                 <argument type="service" id="my_email_formatter" />
            </call>
        </service>
        <service id="newsletter_manager" class="%newsletter_manager.class%" parent="mail_manager">
             <call method="setMailer">
                 <argument type="service" id="my_alternative_mailer" />
            </call>
        </service>
        <service id="greeting_card_manager" class="%greeting_card_manager.class%" parent="mail_manager"/>
        </services>
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\DefinitionDecorator;
    use Symfony\Component\DependencyInjection\Reference;
    
    // ...
    $container->setParameter('newsletter_manager.class', 'NewsletterManager');
    $container->setParameter('greeting_card_manager.class', 'GreetingCardManager');
    
    $container->setDefinition('my_mailer', ...);
    $container->setDefinition('my_alternative_mailer', ...);
    $container->setDefinition('my_email_formatter', ...);
    $container->setDefinition('mail_manager', new Definition(
    ))->setAbstract(
        true
    )->addMethodCall('setMailer', array(
        new Reference('my_mailer')
    ))->addMethodCall('setEmailFormatter', array(
        new Reference('my_email_formatter')
    ));
    $container->setDefinition('newsletter_manager', new DefinitionDecorator(
        'mail_manager'
    ))->setClass(
        '%newsletter_manager.class%'
    )->addMethodCall('setMailer', array(
        new Reference('my_alternative_mailer')
    ));
    $container->setDefinition('greeting_card_manager', new DefinitionDecorator(
        'mail_manager'
    ))->setClass(
        '%greeting_card_manager.class%'
    );
    

El GreetingCardManager recibirá las mismas dependencias que antes, pero la NewsletterManager será pasada a my_alternative_mailer en lugar del servicio my_mailer.

Colección de dependencias

Cabe señalar que el método definidor sustituido en el ejemplo anterior en realidad se invoca dos veces — una vez en la definición del padre y otra más en la definición del hijo. En el ejemplo anterior, esto estaba muy bien, ya que la segunda llamada a setMailer sustituye al objeto mailer establecido por la primera llamada.

En algunos casos, sin embargo, esto puede ser un problema. Por ejemplo, si la sustitución a la llamada al método consiste en añadir algo a una colección, entonces se agregarán dos objetos a la colección. A continuación mostramos tal caso, si la clase padre se parece a esto:

abstract class MailManager
{
    protected $filters;

    public function setFilter($filter)
    {
        $this->filters[] = $filter;
    }

    // ...
}

Si tienes la siguiente configuración:

  • YAML
    parameters:
        # ...
        newsletter_manager.class: NewsletterManager
    services:
        my_filter:
            # ...
        another_filter:
            # ...
        mail_manager:
            abstract:  true
            calls:
                - [setFilter, ["@my_filter"]]
    
        newsletter_manager:
            class:     "%newsletter_manager.class%"
            parent: mail_manager
            calls:
                - [setFilter, ["@another_filter"]]
    
  • XML
    <parameters>
        <!-- ... -->
        <parameter key="newsletter_manager.class">NewsletterManager</parameter>
    </parameters>
    
        <services>
        <service id="my_filter" ...>
          <!-- ... -->
        </service>
        <service id="another_filter" ...>
          <!-- ... -->
        </service>
        <service id="mail_manager" abstract="true">
            <call method="setFilter">
                 <argument type="service" id="my_filter" />
            </call>
        </service>
        <service id="newsletter_manager" class="%newsletter_manager.class%" parent="mail_manager">
             <call method="setFilter">
                 <argument type="service" id="another_filter" />
            </call>
        </service>
        </services>
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\DefinitionDecorator;
    use Symfony\Component\DependencyInjection\Reference;
    
    // ...
    $container->setParameter('newsletter_manager.class', 'NewsletterManager');
    $container->setParameter('mail_manager.class', 'MailManager');
    
    $container->setDefinition('my_filter', ...);
    $container->setDefinition('another_filter', ...);
    $container->setDefinition('mail_manager', new Definition(
    ))->setAbstract(
        true
    )->addMethodCall('setFilter', array(
        new Reference('my_filter')
    ));
    $container->setDefinition('newsletter_manager', new DefinitionDecorator(
        'mail_manager'
    ))->setClass(
        '%newsletter_manager.class%'
    )->addMethodCall('setFilter', array(
        new Reference('another_filter')
    ));
    

En este ejemplo, el setFilter del servicio newsletter_manager se llamará dos veces, dando lugar a que el array $filters contenga ambos objetos my_filter y another_filter. Esto es genial si sólo quieres agregar filtros adicionales para subclases. If you want to replace the filters passed to the subclass, removing the parent setting from the config will prevent the base class from calling setFilter.

Truco

In the examples shown there is a similar relationship between the parent and child services and the underlying parent and child classes. This does not need to be the case though, you can extract common parts of similar service definitions into a parent service without also inheriting a parent class.

Bifúrcame en GitHub