Дата обновления перевода 2021-06-09

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

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

Tip

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

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

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

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

// src/Util/Rot13Transformer.php
namespace App\Util;

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

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

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

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

class TwitterClient
{
    private $transformer;

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

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


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

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

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

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    services:
        _defaults:
            autowire: true
            autoconfigure: true
            public: false
        # ...
    
        AppBundle\Service\TwitterClient:
            # излишне, благодаря _defaults, но значение пеоепределеятся для каждого сервиса
            autowire: true
            # не требуется, но поможет в нашем примере
            public: true
    
        AppBundle\Util\Rot13Transformer:
            autowire: true
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    <?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 https://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <defaults autowire="true" autoconfigure="true" public="false" />
            <!-- ... -->
    
            <!-- автомонтирование излишне, благодаря _defaults, но значение пеоепределеятся для каждого сервиса -->
            <service id="AppBundle\Service\TwitterClient" autowire="true" public="true" />
    
            <service id="AppBundle\Util\Rot13Transformer" autowire="true" />
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    // config/services.php
    return function(ContainerConfigurator $configurator) {
        $services = $configurator->services()
            ->defaults()
                ->autowire()
                ->autoconfigure()
        ;
    
        $services->set(TwitterClient::class)
            // излишне, благодаря _defaults, но значение пеоепределеятся для каждого сервиса
            ->autowire();
    
        $services->set(Rot13Transformer::class)
            ->autowire();
    };
    

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

// 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\Annotation\Route;

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

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

        // ...
    }
}

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

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

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

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

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

class TwitterClient
{
    // ...

    public function __construct(Rot13Transformer $transformer)
    {
        $this->transformer = $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 которого совпадает с классом, добавив псевдоник сервиса:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    services:
        # ...
    
        # id не является классом, так что он не будет использоваться для автомонтирования
        app.rot13.transformer:
            class AppBundle\Util\Rot13Transformer
            # ...
    
        # но это исправляет ошибку!
        # сервис ``app.rot13.transformer`` будет внедрён, когда
        # будет обнаружена подсказка ``AppBundle\Util\Rot13Transformer``
        AppBundle\Util\Rot13Transformer: '@app.rot13.transformer'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    <?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.rot13.transformer" class="AppBundle\Util\Rot13Transformer" autowire="true" />
            <service id="AppBundle\Util\Rot13Transformer" alias="app.rot13.transformer" />
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    use App\Util\Rot13Transformer;
    
    return function(ContainerConfigurator $configurator) {
        // ...
    
        // id не является классом, так что он не будет использоваться для автомонтирования
        $services->set('app.rot13.transformer', Rot13Transformer::class)
            ->autowire();
    
        // но это исправляет ошибку!
        // сервис ``app.rot13.transformer`` будет внедрён, когда
        // будет обнаружена подсказка ``AppBundle\Util\Rot13Transformer``
        $services->alias(Rot13Transformer::class, 'app.rot13.transformer');
    };
    

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

Tip

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

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

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

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

// src/Util/TransformerInterface.php
namespace App\Util;

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

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

// ...
class Rot13Transformer implements TransformerInterface
{
    // ...
}

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

class TwitterClient
{
    public function __construct(TransformerInterface $transformer)
    {
         // ...
    }

    // ...
}

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

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

  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    services:
        # ...
    
        AppBundle\Util\Rot13Transformer: ~
    
        # сервис ``AppBundle\Util\Rot13Transformer`` будет внедрён, когда
        # будет обнаружена подсказка ``AppBundle\Util\TransformerInterface``
        AppBundle\Util\TransformerInterface: '@AppBundle\Util\Rot13Transformer'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    <?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="AppBundle\Util\Rot13Transformer" />
    
            <service id="AppBundle\Util\TransformerInterface" alias="AppBundle\Util\Rot13Transformer" />
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    use App\Util\Rot13Transformer;
    use App\Util\TransformerInterface;
    
    return function(ContainerConfigurator $configurator) {
        // ...
    
        $services->set(Rot13Transformer::class);
    
        // сервис ``AppBundle\Util\Rot13Transformer`` будет внедрён, когда
        // будет обнаружена подсказка ``AppBundle\Util\TransformerInterface``
        $services->alias(TransformerInterface::class, Rot13Transformer::class);
    };
    

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

Tip

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

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

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

// 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 сервиса (см. Working with Interfaces). Кроме того, вы можете определить несколько проименованных псевдонимов автомонтирования, если вы хотите использовать одну рализацию в одних случаях, и другу - в других.

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

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

use App\Util\TransformerInterface;

class MastodonClient
{
    private $transformer;

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

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

        // ... соединитья с Mastodon и отправить преобразованный статус
    }
}
  • YAML
     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'
            # ...
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <!-- config/services.xml -->
    <?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 https://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <!-- ... -->
            <service id="App\Util\Rot13Transformer"/>
            <service id="App\Util\UppercaseTransformer"/>
    
            <service id="App\Util\TransformerInterface" alias="App\Util\Rot13Transformer"/>
            <service
                id="App\Util\TransformerInterface $shoutyTransformer"
                alias="App\Util\UppercaseTransformer"/>
    
            <service id="App\Service\TwitterClient" autowire="true">
                <!-- <argument key="$transformer" type="service" id="App\Util\UppercaseTransformer"/> -->
            </service>
        </services>
    </container>
    
  • PHP
     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
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    use App\Service\MastodonClient;
    use App\Service\TwitterClient;
    use App\Util\Rot13Transformer;
    use App\Util\TransformerInterface;
    use App\Util\UppercaseTransformer;
    
    return function(ContainerConfigurator $configurator) {
        // ...
    
        $services->set(Rot13Transformer::class)->autowire();
        $services->set(UppercaseTransformer::class)->autowire();
    
        // сервис ``App\Util\UppercaseTransformer`` будет внедрен, когда
        // будет обнаружена подсказка ``App\Util\TransformerInterface``
        // для аргумента ``$shoutyTransformer``.
        $services->alias(TransformerInterface::class.' $shoutyTransformer', UppercaseTransformer::class);
    
        // Если аргумент, используемый для внедрения, не совпадает, а подсказка
        // совпадает, будет внедрен сервис
        // ``App\Util\Rot13Transformer``.
        $services->alias(TransformerInterface::class, Rot13Transformer::class);
    
        $services->set(TwitterClient::class)
            // Rot13Transformer будет передан как аргумент $transformer
            ->autowire()
    
            // Если вы хотите выбрать сервис не по умолчанию, и не хотите
            // использовать именованный псевдоним автомонтирования, подключите его вручную:
            //     ->arg('$transformer', service(UppercaseTransformer::class))
            // ...
        ;
    };
    

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

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

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

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

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

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

.. configuration-block::
 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;

class Rot13Transformer
{
    private $logger;

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

    public function transform($value): string
    {
        $this->logger->info('Transforming '.$value);
        // ...
    }
}
 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 $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.

New in version 5.2: Атрибут #[Required] был представлен в Symfony 5.2.

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

  • Annotations
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    namespace App\Util;
    
    class Rot13Transformer
    {
        /** @required */
        public LoggerInterface $logger;
    
        public function transform($value)
        {
            $this->logger->info('Transforming '.$value);
            // ...
        }
    }
    
  • Attributes
     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)
        {
            $this->logger->info('Transforming '.$value);
            // ...
        }
    }
    

New in version 5.1: Автомонтирование свойств публичного типа было представлено в Symfony 5.1.

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

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

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

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

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

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

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