Автоматичне визначення залежностей сервісу (автомонтування)
Дата оновлення перекладу 2024-06-03
Автоматичне визначення залежностей сервісу (автомонтування)
Автомонутвання дозволяє вам управляти сервісами в контейнері з мінімальною конфігурацією. Воно читає підказки у вашому конструкторі (або інших методах) і автоматично передає вам правильні сервіси. Автомонтування 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
// 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
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
). Це означає, що аргумент більше не може
бути автозмонтований.
Щоб виправити це, додайте псевдонім :
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
Автомонтування достатньо потужне, щоб здогадатися, який сервіс впровадити, навіть якщо ви використовуєте типи об'єднання та перетину. Це означає, що ви можете використовувати підказку зі складними типами таким чином:
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]
над
ним, автомонтуючи кожний аргумент. Якщо вам потрібно вручну змонтувати деякі з аргументів
методу, ви завжди можете чітко сконфігурувати виклик методу.
Незважаючи на те, що впровадження властивостей має деякі недоліки ,
автомонтування за допомогою #[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
,
так як контейнер може перебудовуватися частище, коли ви змінюєте класи. Якщо перебудова
вашого контейнера відбувається повільно (можливо лише в дуже великих проектах), можливо
ви не зможете використовувати автомонтування.
Публічні та повторно використовувані пакети
Публічні пакети мають чітко конфігурувати свої сервіси та не покладатися на автомонтування. Автомонтування залежить від сервісім, доступних у контейнері, і пакети не мають контролю над сервіс-контейнерами додатків, в які они включені. Ви можете використовувати автомонтування при створенні повторно використовуваних пакетів всередині вашої компанії, так як у вас буде повний контроль над всім кодом.