Типы внедрения

Сделать зависимости класса ясными и требовать, чтобы они были внедрены в него - это хороший способ сделать класс более пригодным к повторному использованию, тестированию и отделённому от других.

Существует несколько способов того, как могут быть внедрены зависимости. Каждая точка внедрения имеет свои преимущества и недостатки, которые стоит учесть, так же как и разные способы работы с ними при использовании сервис-контейнера.

Конструкторное веедение

Наиболее распространённый способ внедрить зависимости - через конструктор класса. Чтобы сделать это, вам нужно добавить аргумент к подписи конструктора, чтобы принять зависимость:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
namespace AppBundle\Mail;

// ...
class NewsletterManager
{
    private $mailer;

    public function __construct(MailerInterface $mailer)
    {
        $this->mailer = $mailer;
    }

    // ...
}

Вы можете указать, какой сервис вы хотите внедрить в него в конфигурации сервис-контейнера:

  • YAML
    1
    2
    3
    4
    5
    services:
         # ...
    
         AppBundle\Mail\NewsletterManager:
             arguments: ['@mailer']
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    <?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="app.newsletter_manager" class="AppBundle\Mail\NewsletterManager">
                <argument type="service" id="mailer"/>
            </service>
        </services>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    use AppBundle\Mail\NewsletterManager;
    use Symfony\Component\DependencyInjection\Reference;
    
    // ...
    $container->register('app.newsletter_manager', NewsletterManager::class)
        ->addArgument(new Reference('mailer'));
    

Tip

Типизирование внедрённого объетка означает, что вы можете быть уверены, что была внедрена подходящая зависимость. Путём типизирования вы получите чёткую ошибку сразу же, если внедряется неподходящая зависимость. Типизируя, используя интерфейс, а не класс, вы делаете выбор зависимости более гибким. И если предположить, что вы используете толко методы, определённые в интерфейсе, вы можете получить эту гибкость и всё равно безопасно использовать объект.

Существует несколько преимуществ использования конструкторного внедрения:

  • Если зависимость является требованием и класс не может без неё работать, тогда внедрение через конструктор гарантирует, что она будет присутствовать, когда будет использован класс, так каккласс не может быть создан без неё.
  • Конструктор вызывается только один раз, когда создаётся объект, так что вы можете быть уверены, что зависимость не изменится во время жизненного цикла объекта.

Эти преимущества означают, что конструкторное внедрение не подходит для работы с необязательными зависимостями. Также его сложнее использовать в комбинации с иерархией классов: если класс использует конструкторное внедрение, то расширение и переопределения конструктора становятся проблематичны.

Сеттер-внедрение

Ещё одной точкой внедрения в класс является добавление сеттер-метода, который принмает зависимость:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// ...
class NewsletterManager
{
    private $mailer;

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

    // ...
}
  • YAML
    1
    2
    3
    4
    5
    6
    7
    services:
         # ...
    
         app.newsletter_manager:
             class: AppBundle\Mail\NewsletterManager
             calls:
                 - [setMailer, ['@mailer']]
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    <?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="app.newsletter_manager" class="AppBundle\Mail\NewsletterManager">
                <call method="setMailer">
                    <argument type="service" id="mailer" />
                </call>
            </service>
        </services>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    use AppBundle\Mail\NewsletterManager;
    use Symfony\Component\DependencyInjection\Reference;
    
    // ...
    $container->register('app.newsletter_manager', NewsletterManager::class)
        ->addMethodCall('setMailer', array(new Reference('mailer')));
    

В этом случае преимущества такие:

  • Сеттер-внедрение хорошо работает с необязательными зависимостями. Если вам не нужна зависимость, то просто не вызывайте сеттер.
  • Вы можете вызывать сеттер несколько раз. Это особенно полезно, если метод добавляет зависимость в коллекцию. Потом у вас может быть переменное количество зависимостей.

Недостатками сеттер-внедрения являются:

  • Сеттер может быть вызван чаще, чем просто во время построения, так что вы не можете быть уверены, что зависимость не будет заменена во время жизненного цикла объекта (разве что вы ясно не напишите сеттер- методу проверить, был ли он уже вызван).
  • Вы не можете быть уверены, что сеттер будет вызван, и поэтому вам надо добавить проверки того, были ли внедрены любые обязательные зависимости.

Внедрение свойств

Ещё одна возможность - просто установить публичные поля класса напрямую:

1
2
3
4
5
6
7
// ...
class NewsletterManager
{
    public $mailer;

    // ...
}
  • YAML
    1
    2
    3
    4
    5
    6
    7
    services:
         # ...
    
         app.newsletter_manager:
             class: AppBundle\Mail\NewsletterManager
             properties:
                 mailer: '@mailer'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    <?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="app.newsletter_manager" class="AppBundle\Mail\NewsletterManager">
                <property name="mailer" type="service" id="mailer" />
            </service>
        </services>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    use AppBundle\Mail\NewsletterManager;
    use Symfony\Component\DependencyInjection\Reference;
    
    // ...
    $container->register('newsletter_manager', NewsletterManager::class)
        ->setProperty('mailer', new Reference('mailer'));
    

В основном у внедрения свойств существуют только недостатки, оно схоже с сеттер- внедрением, но со следующими дополнительными и важными проблемами:

  • Вы вообще не можете контролировать, когда устаналивается зависимость, она может быть изенена в любой момент жизненного цикла объекта.
  • Вы не можете использовать типизирование, так что вы не можете быть уверены в том, какая зависимость была внедрена, кроме как ясно написав в код класса протестировать экземпляр, перед использованием.

Однако полезно знать, что это может быть сделано с сервис-контейнером, особенно если вы работаете с кодом, который неподвластен вам, как, например, сторонняя библиотека, которая использует публичные свойства для своих зависимостей.

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.