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

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

Tip

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

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

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

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

1
2
3
4
5
6
7
8
9
namespace AppBundle\Util;

class Rot13Transformer
{
    public function transform($value)
    {
        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
namespace AppBundle\Service;

use AppBundle\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);

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

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

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

  • 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
    <?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>
            <defaults autowire="true" autoconfigure="true" public="false" />
            <!-- ... -->
    
            <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
    use AppBundle\Service\TwitterClient;
    use AppBundle\Util\Rot13Transformer;
    
    // ...
    
    // этот метод автомонтирования новый в Symfony 3.3
    // в более ранних версиях, используйте register(), а потом вызовите setAutowired(true)
    $container->autowire(TwitterClient::class)
        ->setPublic(true);
    
    $container->autowire(Rot13Transformer::class)
        ->setPublic(false);
    

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
namespace AppBundle\Controller;

use AppBundle\Service\TwitterClient;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class DefaultController extends Controller
{
    /**
     * @Route("/tweet")
     */
    public function tweetAction()
    {
        // вызовите $user, $key, $status из опубликованных (POST) данных

        $twitterClient = $this->container->get(TwitterClient::class);
        $twitterClient->tweet($user, $key, $status);

        // ...
    }
}

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// ...
use AppBundle\Util\Rot13Transformer;

class TwitterClient
{
    // ...

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

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

Если сервиса, id которого точно совпадает с типом, нет, тогда:

Если в контейнере 0 сервисов, которые имеют такой тип, тогда:
Если тип - конкретный класс, тогда в контейнере автоматически регистрируется новый приватный и автомонтируемый сервис, который используется для аргумента.
Если в контейнере 1 сервис, который имеют такой тип, тогда:
(осуждено) Этот сервис используется для аргумента. В Symfony 4.0, это будет удалено. Правильным решением является создание дополнительного имени из типа в id сервиса, чтобы работало нормальное автомонтирование.
Если в контейнере 2 и более сервисов, которые имеют такой тип, тогда:
Выдаётся чёткое исключение. Вам нужно выбрать, какой сервис должен быть использован, путём создания дополнительного имени или ясной конфигурации аргумента.

Автомонтирование - это отличный способ автоматизировать конфигурацию, и 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
    use AppBundle\Util\Rot13Transformer;
    
    // ...
    
    $container->autowire('app.rot13.transformer', Rot13Transformer::class)
        ->setPublic(false);
    $container->setAlias(Rot13Transformer::class, '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
namespace AppBundle\Util;

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

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

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

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

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

    // ...
}

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

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

  • 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
    use AppBundle\Util\Rot13Transformer;
    use AppBundle\Util\TransformerInterface;
    
    // ...
    $container->autowire(Rot13Transformer::class);
    $container->setAlias(TransformerInterface::class, Rot13Transformer::class);
    

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

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

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

1
2
3
4
5
6
7
8
9
namespace AppBundle\Util;

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

Если вы зарегистрируете его как сервис, то у вас будет два сервиса, внедряющих тип AppBundle\Util\TransformerInterface. Symfony не знает, какой нужно использовать для автомонтирования, так что вам нужно выбрать один из них, создав дополнительное имя из типа для правильного id сервиса (смотрите Работа с интерфейсами).

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

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    services:
        # ...
    
        AppBundle\Util\Rot13Transformer: ~
        AppBundle\Util\UppercaseTransformer: ~
    
        # сервис ``AppBundle\Util\Rot13Transformer`` будет внедрён, когда
        # будет обнаружено типизирование ``AppBundle\Util\TransformerInterface``
        AppBundle\Util\TransformerInterface: '@AppBundle\Util\Rot13Transformer'
    
        AppBundle\Service\TwitterClient:
            # Rot13Transformer будет передан, как аргумент $transformer
            autowire: true
    
            # Если вы хотели выбрать сервис не по умолчанию, смонтируйте его вручную
            # arguments:
            #     $transformer: '@AppBundle\Util\UppercaseTransformer'
            # ...
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <?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\UppercaseTransformer" />
    
            <service id="AppBundle\Util\TransformerInterface" alias="AppBundle\Util\Rot13Transformer" />
    
            <service id="AppBundle\Service\TwitterClient" autowire="true">
                <!-- <argument key="$transformer" type="service" id="AppBundle\Util\UppercaseTransformer" /> -->
            </service>
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    use AppBundle\Util\Rot13Transformer;
    use AppBundle\Util\UppercaseTransformer;
    use AppBundle\Util\TransformerInterface;
    use AppBundle\Service\TwitterClient;
    
    // ...
    $container->autowire(Rot13Transformer::class);
    $container->autowire(UppercaseTransformer::class);
    $container->setAlias(TransformerInterface::class, Rot13Transformer::class);
    $container->autowire(TwitterClient::class)
        //->setArgument('$transformer', new Reference(UppercaseTransformer::class))
    ;
    

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

New in version 3.3: Использование дополнительных имён FQCN для исправления двусмысленности автомонтирования было представлено в Symfony 3.3. До версии 3.3, вам нужно было использовать ключ autowiring_types.

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

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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
namespace AppBundle\Util;

class Rot13Transformer
{
    private $logger;

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

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

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

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

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

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

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

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

Публичные пакеты должны ясно конфигурировать свои сервисы и не полагаться на автомонтирование.

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