Способы внедрения зависимостей Symfony

Symfony Messenger Systemd

10 Января 2022 | Symfony

В этой статье мы рассмотрим, как можно внедрять зависимости  (Dependen с Injection) в другие классы

Способы внедрения зависимостей Symfony

Что такое внедрение зависимостей?

Dependen с Injection ( далее DI ) и Service Container решают одну из самых важных проблем — создание и внедрение зависимостей в класс.

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

Сервисный контейнер — это набор классов с конфигурацией того, как их создавать. (О том, как настроить сервис-контейнер, вы можете прочитать в официальной документации .)

После регистрации зависимостей вы можете использовать DI и Service Container для создания новых объектов. Контейнер будет автоматически создавать их экземпляры и внедряя их во вновь созданные объекты. Подключание зависимостей является рекурсивным, что означает, что если у зависимости есть другие зависимости, эти зависимости также будут подключаться автоматически.

Основная цель внедрения зависимостей — сделать зависимости классов явными, а требование их внедрения — хороший способ сделать класс более пригодным для повторного использования, тестируемым и отделенным от других.

Существует 4 способа внедрения зависимости, а именно:

  • Внедрение в конструкторе
  • Внедрение неизменяемого сеттера
  • Инъекция сеттера
  • Внедрение свойств

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

 

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

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


<?php
# src/Service/UserManager.php

namespace App\Service;

class UserManager
{
    public function __construct(private SmsInterface $sms)
    {
    }
    public function restorePassword(Request $request)
    {
        //...
        $this->sms->send(...);
        //..
    }
}

    

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


# config/services.yaml
services:
    # ...

    App\Service\UserManager:
        arguments: ['@sms']
  

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

@см.: https://symfony.com/doc/current/service_container.html#the-autowire-option

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

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

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

Лучший вариант использования — это контроллеры и службы, которые вы не хотите наследовать. Кроме того, избегайте его использования в абстрактных классах.

 

Внедрение неизменяемого сеттера

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


<?php
# src/Service/UserManager.php
namespace App\Service;

// ...
class UserManager
{
    private SmsInterface $sms;

    /**
     * @required
     * @return static
     */
    public function withSms(SmsInterface $sms): self
    {
        $new = clone $this;
        $new--->sms = $sms;

        return $new;
    }
    public function getSms(): SmsInterface
    {
        if(!isset($this->sms)){
            throw new \Exception("Typed property must not be accessed before initialization");
        }
        return $this->sms;    
    }
    public function restorePassword(Request $request): mixed
    {
        //...
        $this->getSms()->send(...);
        //..
    }
    // ...
}

Чтобы использовать этот тип инъекции, не забудьте его настроить:


# config/services.yaml
services:
     # ...

     app.user_manager:
         class: App\Service\UserManager
         calls:
             - withSms: !returns_clone ['@sms']
  

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

Этот подход полезен, когда вам нужно настроить службу с необязательными зависимостями, поэтому вот преимущества неизменяемых сеттеров:

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

Недостатки:

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

Этот подход не так распространен, как другие, но в умелых руках он может быть полезен.

Например, такой подход реализован в компоненте Symfony — Messenger . Существует класс Envelope , и каждый раз, когда вы добавляете штамп, метод копирует объект, так что тело сообщения остается прежним.

 

Инъекция сеттера

Третья возможная точка внедрения в класс — принятие зависимости через метод установки:


<?php
# src/Service/UserManager.php
namespace App\Service;

// ...
class UserManager
{
    private SmsInterface $sms;

    /**
     * @required
     */
    public function setSms(SmsInterface $sms): void
    {
        $this--->sms = $sms;
    }
    public function restorePassword(Request $request)
    {
        //...
        $this->sms->send(...);
        //..
    }
    // ...
}

  

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


# config/services.yaml
services:
    # ...

    app.newsletter_manager:
        class: App\Service\UserManager
        calls:
            - setSms: ['@sms']
  

На этот раз преимущества в основном такие же, как у Внедрение неизменяемого сеттера:

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

Недостатки сеттерного впрыска:

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

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

Например, этот подход ловко используется в PSR-3 . Существует трейт с опциональной зависимостью, который может быть составлен с любым классом.

 

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

Последний вариант внедрения — установка публичных полей класса напрямую:


<?php
# src/Service/UserManager.php
namespace App\Service;

// ...
class UserManager
{
    public SmsInterface $sms;
    public function restorePassword(Request $request)
    {
        //...
        $this--->sms->send(...);
        //..
    }
    // ...
}

  

Использовать автопроводку нельзя, поэтому не забудьте ее настроить:


# config/services.yaml
services:
    # ...

    app.newsletter_manager:
        class: App\Service\UserManager
        properties:
            sms: '@sms'
  

Этот подход имеет в основном только недостатки. Он похож на Инъекция сеттера, но имеет следующие дополнительные проблемы:

  • Невозможно контролировать, когда зависимость установлена. Вы не можете предотвратить перезапись этого свойства.

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

Мы НЕ рекомендуем использовать этот способ в вашем коде.

 

Вывод

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

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

Надеемся, что Вы нашли эту статью полезной.

Источники:

 

Комментарии
Если у вас есть вопросы, не стесняйтесь оставлять комментарии ниже.
Загрузка комментариев...