Autowiring в сервис-контейнере Symfony. Автоматическая установка зависимостей в сервисах Symfony.

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

Autowiring Symfony спроектирован так, чтобы быть предсказуемым: если не совсем ясно, какую зависимость следует передавать, вы увидите хорошо описанное исключение.

Благодаря скомпилированному контейнеру Symfony нет никаких накладных расходов при использовании autowiting.

Пример autowiring в Symfony.

Представьте, что вы создаете API для публикации статусов в ленте Twitter, обработанных ROT13, забавным кодировщиком, который перемещает все символы на 13 букв вперед по алфавиту.

Начнем с создания класса-трансформера ROT13:

namespace App\Util;

class Rot13Transformer
{
    public function transform($value)
    {
        return str_rot13($value);
    }
}

А теперь клиент Twitter, использующий этот преобразователь:

namespace App\Service;

use App\Util\Rot13Transformer;

class TwitterClient
{
    private $transformer;

    public function __construct(Rot13Transformer $transformer)
    {
        $this->transformer = $transformer;
    }

    public function tweet($user, $key, $status)
    {
        $transformedStatus = $this->transformer->transform($status);

        // ... connect to Twitter and send the encoded status
    }
}

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

Однако, чтобы лучше понять autowiring, следующие примеры явно настраивают обе службы:

# config/services.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true
        public: false
    # ...

    App\Service\TwitterClient:
        # redundant thanks to _defaults, but value is overridable on each service
        autowire: true

    App\Util\Rot13Transformer:
        autowire: true

Теперь вы можете использовать сервис TwitterClient непосредственно в контроллере:

namespace App\Controller;

use App\Service\TwitterClient;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class DefaultController extends AbstractController
{
    /**
     * @Route("/tweet", methods={"POST"})
     */
    public function tweet(TwitterClient $twitterClient)
    {
        // fetch $user, $key, $status from the POST'ed data

        $twitterClient->tweet($user, $key, $status);

        // ...
    }
}

Это заработает автоматически! Контейнер знает, что должен передавать сервис Rot13Transformer в качестве первого аргумента при создании службы TwitterClient.

Логика работы Autowiring.

Autowiring работает, читая тайп-хинт Rot13Transformer в TwitterClient:

// ...
use App\Util\Rot13Transformer;

class TwitterClient
{
    // ...

    public function __construct(Rot13Transformer $transformer)
    {
        $this->transformer = $transformer;
    }
}

Система Autowiring ищет сервис, идентификатор которого точно соответствует тайп-хинту: App\Util\Rot13Transformer. В нашем случае такой сервис существует, когда мы настроили сервис Rot13Transformer, мы использовали его полное имя класса в качестве его идентификатора.

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

Если нет сервиса, чей идентификатор точно соответствует типу, будет выдано понятное исключение.

Autowiring — отличный способ автоматизировать настройку, и Symfony старается быть максимально предсказуемым и понятным.

Использование алиасов-псевдонимов для включения autowiring.

Основным способом настройки autowiring является создание сервиса, идентификатор которого точно соответствует имени его класса. В предыдущем примере идентификатор сервиса — App\Util\Rot13Transformer, что позволяет автоматически подключать этот сервис по тайп-хинту.

Autowiring также может быть выполнен с использованием псевдонима (алиаса). Предположим, что по какой-то причине идентификатор службы был вместо этого app.rot13.transformer. В этом случае любые аргументы, с тайп-хинтом с именем класса (App\Util\Rot13Transformer), больше не смогут быть подключены автоматически.

Нет проблем! Чтобы это исправить, вы можете создать сервис, чей идентификатор соответствует классу, добавив алиас сервиса:

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

    # the id is not a class, so it won't be used for autowiring
    app.rot13.transformer:
        class: App\Util\Rot13Transformer
        # ...

    # but this fixes it!
    # the ``app.rot13.transformer`` service will be injected when
    # an ``App\Util\Rot13Transformer`` type-hint is detected
    App\Util\Rot13Transformer: '@app.rot13.transformer'

Это создает сервис с «алиасом», чей идентификатор App\Util\Rot13Transformer. Благодаря этому autowiring видит это и использует его всякий раз, когда класс Rot13Transformer используется в тайп-хинте.

Псевдонимы часто используются бандлами для автоматического подключения сервисов. Например, MonologBundle создает сервис с идентификатором logger. Но он также добавляет псевдоним: Psr\Log\LoggerInterface, который указывает на сервис логирования сообщений. Вот почему аргументы, с тайп-хинтом Psr\Log\LoggerInterface, могут быть подключены автоматически.

Работа с несколькими реализациями одного типа.

Предположим, вы создали второй класс — UppercaseTransformer, который реализует TransformerInterface:

namespace App\Util;

class UppercaseTransformer implements TransformerInterface
{
    public function transform($value)
    {
        return strtoupper($value);
    }
}

Если вы зарегистрируете этот клас как сервис, теперь у вас два сервиса, которые реализуют тип App\Util\TransformerInterface. Подсистема autowiring не может решить, какой из них использовать для инъекции зависимости по тайп-хинту. Помните, что автоматическое подключение не волшебство; он ищет сервис, идентификатор которого совпадает с тайп-хинтом. Поэтому вам нужно выбрать один, создав алиас-псевдоним типа для правильного идентификатора сервиса. Кроме того, вы можете определить несколько именованных алиасов-псевдонимов autowiring, если вы хотите использовать одну реализацию в некоторых случаях и другую реализацию в других.

Например, вы можете по умолчанию использовать реализацию Rot13Transformer, когда тайп-хинт имеет интерфейс TransformerInterface, но в некоторых конкретных случаях использовать реализацию UppercaseTransformer. Для этого вы можете создать обычный псевдоним из интерфейса TransformerInterface для Rot13Transformer, а затем создать именованный алиас-псевдоним для autowiring из специальной строки, содержащей интерфейс, за которым следует имя переменной, совпадающее с именем, которое вы используете при инхекции зависимости:

namespace App\Service;

use App\Util\TransformerInterface;

class MastodonClient
{
    private $transformer;

    public function __construct(TransformerInterface $shoutyTransformer)
    {
        $this->transformer = $shoutyTransformer;
    }

    public function toot($user, $key, $status)
    {
        $transformedStatus = $this->transformer->transform($status);

        // ... connect to Mastodon and send the transformed status
    }
}
# config/services.yaml
services:
    # ...

    App\Util\Rot13Transformer: ~
    App\Util\UppercaseTransformer: ~

    # the ``App\Util\UppercaseTransformer`` service will be
    # injected when an ``App\Util\TransformerInterface``
    # type-hint for a ``$shoutyTransformer`` argument is detected.
    App\Util\TransformerInterface $shoutyTransformer: '@App\Util\UppercaseTransformer'

    # If the argument used for injection does not match, but the
    # type-hint still matches, the ``App\Util\Rot13Transformer``
    # service will be injected.
    App\Util\TransformerInterface: '@App\Util\Rot13Transformer'

    App\Service\TwitterClient:
        # the Rot13Transformer will be passed as the $transformer argument
        autowire: true

        # If you wanted to choose the non-default service and do not
        # want to use a named autowiring alias, wire it manually:
        #     $transformer: '@App\Util\UppercaseTransformer'
        # ...

Благодаря алиасу App\Util\TransformerInterface в любой аргумент, с тайп-хинтом этого интерфейса, будет передан сервис App\Util\Rot13Transformer.

Если же аргумент называется $shoutyTransformer, в него будет передан сервис App\Util\UppercaseTransformer.

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

# config/services.yaml

services:
   # ...

    App\Service\VkClient:
        arguments:
            $transformer: '@App\Util\UppercaseTransformer'

Autowiring методов класса (например, сеттеров).

Когда для сервиса включен autowiring, вы также можете настроить контейнер на автоматический вызов методов вашего класса, когда он будет создан (инстанцирован). Например, предположим, что вы хотите внедрить сервис логирования, и решили использовать setter -jection:

namespace App\Util;

class Rot13Transformer
{
    private $logger;

    /**
     * @required
     */
    public function setLogger(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function transform($value)
    {
        $this->logger->info('Transforming '.$value);
        // ...
    }
}

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

Производительность при autowiring.

Благодаря скомпилированному сервис-контейнеру Symfony, использование autowiring не снижает производительность. Однако в среде разработки есть небольшое снижение производительности, так как контейнер может перестраиваться чаще при изменении классов. Если перестройка вашего контейнера идет медленно (возможно, в очень больших проектах), вы не сможете использовать autowring.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *