Автоматическое определение зависимостей сервиса (автомонтирование)

Дата обновления перевода 2024-07-27

Автоматическое определение зависимостей сервиса (автомонтирование)

Автомонтирование позволяет вам управлять сервисами в контейнере с минимальной конфигурацией. Оно читает типизирования в вашем конструкторе (или других методах) и автоматически передаёт вам правильные сервисы. Автомонтирование Symfony создано так, чтобы быть предсказуемым: если не абсолютно точно ясно, какую зависимость нужно передать, вы увидите применимое на практике исключение.

Tip

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

Пример автомонтирования

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

Начните с создания класса преобразователя ROT13:

1
2
3
4
5
6
7
8
9
10
// src/Util/Rot13Transformer.php
namespace App\Util;

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Service/TwitterClient.php
namespace App\Service;

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

class TwitterClient
{
    public function __construct(
        private Rot13Transformer $transformer,
    ) {
    }

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

        // ... подключиться к Twitter и отправить зашифрованный статус
    }
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
# config/services.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true
    # ...

    App\Service\TwitterClient:
        # излишне, благодаря _defaults, но значение пеоепределеятся для каждого сервиса
        autowire: true

    App\Util\Rot13Transformer:
        autowire: true

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/Controller/DefaultController.php
namespace App\Controller;

use App\Service\TwitterClient;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class DefaultController extends AbstractController
{
    #[Route('/tweet')]
    public function tweet(TwitterClient $twitterClient, Request $request): Response
    {
        // извлеките $user, $key, $status из опубликованных (POST) данных

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

        // ...
    }
}

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

Объяснение логики автомонтирования

Автомонтирование работает путём считывания подсказок Rot13Transformer в TwitterClient:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Service/TwitterClient.php
namespace App\Service;

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

class TwitterClient
{
    // ...

    public function __construct(
        private Rot13Transformer $transformer,
    ) {
    }
}

Система автомонтирования ищет сервис, id которого точно совпадает с типизированием: то есть AppBundle\Util\Rot13Transformer. В этом случае, он существует! Когда вы сконфигурировали сервис Rot13Transformer, вы использовали его полностью квалифицированное имя класс в качестве id. Автомонтрирование - это не магия: оно просто ищет сервис, id которого совпадает с типизированем. Если вы загружаете сервисы автоматически , то каждый id сервиса является классом его имени. Это главный способ контролировать автомонтирование.

Если сервиса, id которого точно совпадает с типом, нет, тогда будет вызвано ясное исключение.

Автомонтирование - прекрасный способ автоматизировать конфигурацию, и Symfony старается быть максимально предсказуемой и ясной.

Использование псведонимов для включения автомонтирования

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

Этого также можно достинуть, используя псевдоник . Представьте, что, по какой-либо причине, id сервис вместо этого был app.rot13.transformer. В таком случае, любые аргументы, типизированные в имени класса (AppBundle\Util\Rot13Transformer) больше не могут быть автомонтированы (на самом деле, это уже будет работать, но не в Symfony 4.0 ).

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

1
2
3
4
5
6
7
8
9
10
11
12
13
# config/services.yaml
services:
    # ...

    # id не является классом, так что он не будет использоваться для автомонтирования
    app.rot13.transformer:
        class: App\Util\Rot13Transformer
        # ...

    # но это исправляет ошибку!
    # сервис ``app.rot13.transformer`` будет внедрён, когда
    # будет обнаружена подсказка ``AppBundle\Util\Rot13Transformer``
    AppBundle\Util\Rot13Transformer: '@app.rot13.transformer'

Это создаёт "дополнительное имя" сервиса, id которого - AppBundle\Util\Rot13Transformer. Благодаря этому, автомонтирование видит это и использует его каждый раз, когда типизируется Rot13Transformer.

Tip

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

Работа с интерфейсами

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

Чтобы последовать этой наилучшей практике, представьте, что вы решили создать TransformerInterface:

1
2
3
4
5
6
7
// src/Util/TransformerInterface.php
namespace App\Util;

interface TransformerInterface
{
    public function transform(string $value): string;
}

Потом, вы обновляете Rot13Transformer, чтобы реализовать его:

1
2
3
4
5
// ...
class Rot13Transformer implements TransformerInterface
{
    // ...
}

Теперь, когда у вас есть интерфейс, вам стоит использовать это в качестве вашей типизации:

1
2
3
4
5
6
7
8
9
10
class TwitterClient
{
    public function __construct(
        private TransformerInterface $transformer,
    ) {
        // ...
    }

    // ...
}

Однако, сейчас типизация (AppBundle\Util\TransformerInterface) больше не совпадает с id сервиса (AppBundle\Util\Rot13Transformer). Это означает, что аргумент больше не может быть автомонтирован.

Чтобы исправить это, добавьте псевдоник :

1
2
3
4
5
6
7
8
9
# config/services.yaml
services:
    # ...

    App\Util\Rot13Transformer: ~

    # сервис ``AppBundle\Util\Rot13Transformer`` будет внедрён, когда
    # будет обнаружена подсказка ``AppBundle\Util\TransformerInterface``
    AppBundle\Util\TransformerInterface: '@AppBundle\Util\Rot13Transformer'

Благодаря псевдонимму AppBundle\Util\TransformerInterface, подсистема автомонтирования знает, что сервис AppBundle\Util\Rot13Transformer должен быть внедрён при работе с TransformerInterface.

Tip

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

Tip

Автомонтирование достаточно мощное, чтобы угадать, какой сервис следует внедрить, даже при использовании типов union и interection. Это означает, что вы можете указывать подсказку аргумента с сложными типами, такими как:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;

class DataFormatter
{
    public function __construct(
        private (NormalizerInterface&DenormalizerInterface)|SerializerInterface $transformer,
    ) {
        // ...
    }

    // ...
}

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

Представьте, что вы создаёте второй класс - UppercaseTransformer, который внедряет TransformerInterface:

1
2
3
4
5
6
7
8
9
10
// src/Util/UppercaseTransformer.php
namespace App\Util;

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Service/MastodonClient.php
namespace App\Service;

use App\Util\TransformerInterface;

class MastodonClient
{
    public function __construct(
        private TransformerInterface $shoutyTransformer,
    ) {
    }

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

        // ... соединитья с Mastodon и отправить преобразованный статус
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# config/services.yaml
services:
    # ...

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

    # сервис ``App\Util\UppercaseTransformer`` будет внедрен, когда
    # будет обнаружена подсказка ``App\Util\TransformerInterface``
    # для аргумента ``$shoutyTransformer``.
    App\Util\TransformerInterface $shoutyTransformer: '@App\Util\UppercaseTransformer'

    # Если аргумент, используемый для внедрения, не совпадает, а подсказка
    # совпадает, будет внедрен сервис
    # ``App\Util\Rot13Transformer``.
    App\Util\TransformerInterface: '@App\Util\Rot13Transformer'

    App\Service\TwitterClient:
        # Rot13Transformer будет передан как аргумент $transformer
        autowire: true

        # Если вы хотите выбрать сервис не по умолчанию, и не хотите
        # использовать именованный псевдоним автомонтирования, подключите его вручную:
        #     $transformer: '@App\Util\UppercaseTransformer'
        # ...

Благодаря псевдониму AppBundle\Util\TransformerInterface, любой аргумент подсказанный этим интерфейсом, будет передан сервису AppBundle\Util\Rot13Transformer. Если аргумент имеет имя $shoutyTransformer, вместо этого будет использован App\Util\UppercaseTransformer. Однако, вы также можете вручную смонтировать другой сервис, указав аргумент под ключом аргументов.

Другой возможностью является использование атрибута #[Target]. Используя этот атрибут в аргументе, который вы хотите автомонтировать, вы можете точно определить, какой именно сервис нужно внедрить, используя его псевдоним. Благодаря этому можно иметь несколько сервисов, реализующих один и тот же интерфейс, и при этом имя аргумента не будет зависеть от имени реализации (как показано в примере выше).

Допустим, вы определили псевдоним app.uppercase_transformer для сервиса App\Util\UppercaseTransformer. Вы могли бы использовать атрибут #[Target]. следующим образом:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Service/MastodonClient.php
namespace App\Service;

use App\Util\TransformerInterface;
use Symfony\Component\DependencyInjection\Attribute\Target;

class MastodonClient
{
    public function __construct(
        #[Target('app.uppercase_transformer')]
        private TransformerInterface $transformer
    ){
    }
}

Note

Некоторые IDE выдают ошибку при использовании #[Target], как в предыдущем примере: "Атрибут не может быть применен к свойству, так как не содержит флажка'Attribute::TARGET_PROPERTY'". Причина в том, что благодаря продвижению конструктора PHP этот аргумент конструктора является одновременно и параметром, и свойством класса. Вы можете смело игнорировать это сообщение об ошибке.

Исправление аргументов, не поддающихся автомонтированию

Автомонтирование работает только в случае, если ваш аргумент является объектом. Но если у вас есть скалярный аргумент (например, строка), то его нельзя автомонтировать: Symfony выдаст чёткое исключение.

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

Вы также можете использовать параметр #[Autowire], чтобы проинструктировать логику автомонтирования об этих аргументах:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Service/MessageGenerator.php
namespace App\Service;

use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

class MessageGenerator
{
    public function __construct(
        #[Autowire(service: 'monolog.logger.request')] LoggerInterface $logger
    ) {
        // ...
    }
}

Атрибут #[Autowire] также может быть использован для параметров , сложных выражений и даже переменных окружения :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src/Service/MessageGenerator.php
namespace App\Service;

use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

class MessageGenerator
{
    public function __construct(
        // использовать синтаксис %...% для параметров
        #[Autowire('%kernel.project_dir%/data')]
        string $dataDir,

        // или использовать аргумент "param"
        #[Autowire(param: 'kernel.debug')]
        bool $debugMode,

        // выражения
        #[Autowire(expression: 'service("App\\Mail\\MailerConfiguration").getMailerMethod()')]
        string $mailerMethod

        // переменные окружения
        #[Autowire(env: 'SOME_ENV_VAR')]
        string $senderName
    ) {
    }
    // ...
}

Генерирование замыканий с помощью автомонтирования

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

Автоматическое создание замыкания, заключающего в себе инстанциирование сервиса, может быть сделано с помощью атрибута AutowireServiceClosure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// src/Service/Remote/MessageFormatter.php
namespace App\Service\Remote;

use Symfony\Component\DependencyInjection\Attribute\AsAlias;

#[AsAlias('third_party.remote_message_formatter')]
class MessageFormatter
{
    public function __construct()
    {
        // ...
    }

    public function format(string $message): string
    {
        // ...
    }
}

// src/Service/MessageGenerator.php
namespace App\Service;

use App\Service\Remote\MessageFormatter;
use Symfony\Component\DependencyInjection\Attribute\AutowireServiceClosure;

class MessageGenerator
{
    public function __construct(
        #[AutowireServiceClosure('third_party.remote_message_formatter')]
        private \Closure $messageFormatterResolver
    ) {
    }

    public function generate(string $message): void
    {
        $formattedMessage = ($this->messageFormatterResolver)()->format($message);

        // ...
    }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Service/MessageGenerator.php
namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;

class MessageGenerator
{
    public function __construct(
        #[AutowireCallable(service: 'third_party.remote_message_formatter', method: 'format')]
        private \Closure $formatCallable
    ) {
    }

    public function generate(string $message): void
    {
        $formattedMessage = ($this->formatCallable)($message);

        // ...
    }
}

Наконец, вы можете передать опцию lazy: true атрибуту AutowireCallable. В этом случае вызываемое автоматически станет ленивым, что означает, что инкапсулированный сервис будет инстанцирован только при первом вызове замыкания.

Автомонтирование других методов (например, сеттеров и свойств публичного типа)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/Util/Rot13Transformer.php
namespace App\Util;

use Symfony\Contracts\Service\Attribute\Required;

class Rot13Transformer
{
    private LoggerInterface $logger;

    #[Required]
    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }

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

Автомонтирование автоматически вызовет любой метод с атрибутом #[Required] над ним, автомонтируя каждый аргумент. Если вам нужно вручную смонтировать некоторые из аргументов метода, вы всегда можете ясно сконфигурировать вызов метода.

Если ваша версия PHP не поддерживает атрибуты (они были представлены в PHP 8), вы можете вместо этого использовать аннотацию @required.

Несмотря на то, что внедрение свойств имеет некоторые недостатки , автомонтирование с помощью #[Required] или @required также может применяться к свойствам публичного типа:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace App\Util;

use Symfony\Contracts\Service\Attribute\Required;

class Rot13Transformer
{
    #[Required]
    public LoggerInterface $logger;

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

Автомонтирование методов действий контроллера

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

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

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

Публичные и повторно используемые пакеты

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