Автоматичне визначення залежностей сервісу (автомонтування)

Дата оновлення перекладу 2022-12-23

Автоматичне визначення залежностей сервісу (автомонтування)

Автомонутвання дозволяє вам управляти сервісами в контейнері з мінімальною конфігурацією. Воно читає підказки у вашому конструкторі (або інших методах) і автоматично передає вам правильні сервіси. Автомонтування 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
21
22
23
// 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
  • XML
  • PHP
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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:

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(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
  • XML
  • PHP
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'

Це створює "псевдонім" сервісу, 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
class TwitterClient
{
    public function __construct(TransformerInterface $transformer)
    {
         // ...
    }

    // ...
}

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

Щоб виправити це, додайте псевдонім :

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
services:
    # ...

    AppBundle\Util\Rot13Transformer: ~

    # сервіс ``AppBundle\Util\Rot13Transformer`` буде впроваджений, коли
    # буде виявлена підказка ``AppBundle\Util\TransformerInterface``
    AppBundle\Util\TransformerInterface: '@AppBundle\Util\Rot13Transformer'

Завдяки псевдоніму AppBundle\Util\TransformerInterface, підсистема автомонтування знає, що сервіс AppBundle\Util\Rot13Transformer має бути впроваджений при роботі з TransformerInterface.

Tip

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

Робота з декількома впровадженнями одного типу

Уявіть, що ви створюєте другий клас - 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
20
21
// 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
  • XML
  • 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
# 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. Однак ви можете також вручну змонтувати інший сервіс, вказавши аргумент під ключем аргументів.

Виправлення аргументів, які не піддаються автомонтуванню

Автомонтування працює лише у випадках, якщо ваш аргумент є об'єктом. Але якщо у вас є скалярний аргумент (наприклад, рядок), то його не можна автомонтувати: 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
    ) {
        // ...
    }
}

6.1

Атрибут #[Autowire] було представлено в Symfony 6.1.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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,

        #[Autowire('%kernel.debug%')]
        bool $debugMode,

        // додати вирази expressions
        #[Autowire(expression: 'service("App\\Mail\\MailerConfiguration").getMailerMethod()')]
        string $mailerMethod
    ) {
    }
    // ...
}

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

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

  • Annotations
  • Attributes
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);
        // ...
    }
}

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

Якщо ваша версія PHP не підтримує атрибути (вони були представлені в PHP 8), ви можете замість цього використати анотацію @required.

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

  • Annotations
  • Attributes
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);
        // ...
    }
}

Автомонтування методів дій контролера

Якщо ви використовуєте фреймворк Symfony, ви також можете автомонтувати аргументи до ваших методів дій контролера. Це особливий випадок автомонтування, який існує для зручності. Дивіться , щоб дізнаится більше.

Наслідки для продуктивності

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

Публічні та повторно використовувані пакети

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