Сервіс-контейнер — Symfony
Частину підрозділів ще не перекладено — вони показані англійською нижче в тексті або лишились в оригіналі. Готові фрагменти вже перевірені редактором.
Скринкаст
Віддаєте перевагу відеоурокам? Перегляньте серію скринкастів
Symfony Fundamentals.
Впровадження залежностей (dependency injection) — один із найважливіших патернів проєктування у розробці програмного забезпечення. Його ідея в тому, що класи не створюють потрібні їм обʼєкти; вони отримують їх ззовні. Це тримає ваш код розвʼязаним і робить його більш придатним до повторного використання й тестування.
Інструмент, що автоматизує цей патерн, називають контейнером сервісів (service container, або «контейнером впровадження залежностей»): це обʼєкт, який знає, як створити й повʼязати всі обʼєкти вашого застосунку, і надає їх вам, коли вони потрібні. У Symfony такі обʼєкти називають сервісами, і майже все, що робить ваш застосунок, насправді робить один із них. Сама Symfony побудована навколо контейнера сервісів, тож уміння добре з ним працювати — один із ключів до опанування Symfony.
Контейнер сервісів надає компонент DependencyInjection. Застосунки Symfony створюють і налаштовують контейнер за вас. Ви також можете використовувати контейнер у будь-якому PHP-застосунку, як пояснено в розділі про самостійне використання цієї статті. Наступний розділ детально пояснює патерн впровадження залежностей; якщо ви вже знайомі з ним, можете перейти до решти статті.
Розуміння впровадження залежностей
Щоб зрозуміти, чому впровадження залежностей корисне і які проблеми воно розвʼязує, розгляньте клас, який генерує повідомлення про статус і форматує їх перед поверненням:
// src/Service/MessageGenerator.php
namespace App\Service;
use App\Formatter\TextFormatter;
class MessageGenerator
{
public function getHappyMessage(): string
{
$messages = [
'You did it! You updated the system! Amazing!',
'That was one of the coolest updates I\'ve seen all day!',
'Great work! Keep going!',
];
$formatter = new TextFormatter();
return $formatter->format($messages[array_rand($messages)]);
}
}
Цей код працює добре, але має кілька проблем, які неприйнятні в реальних застосунках:
- Клас обмежений: він підтримує лише один конкретний спосіб форматування повідомлень. Якщо десь інде вам потрібні HTML-повідомлення, доведеться змінювати сам клас
MessageGenerator; - Клас негнучкий: форматувальник створюється всередині класу, тож немає жодного способу його налаштувати. Увесь код, що використовує
MessageGenerator, отримує точнісінько ту саму поведінку; - Клас важко тестувати: ви не можете замінити форматувальник фейковим обʼєктом, тож будь-який тест
MessageGeneratorтестує також і реальну поведінкуTextFormatter.
Походження цих проблем те саме: клас створює потрібні йому обʼєкти (свої залежності) замість того, щоб отримувати їх. Гарне емпіричне правило — уникати ключового слова new для створення залежностей усередині ваших класів.
Перший крок до виправлення — перенести залежність у конструктор. Клас більше не створює форматувальник; він оголошує, що потребує його, і очікує його отримати:
// src/Service/MessageGenerator.php
namespace App\Service;
use App\Formatter\TextFormatter;
class MessageGenerator
{
public function __construct(
private TextFormatter $formatter,
) {
}
public function getHappyMessage(): string
{
$messages = [
// ...
];
return $this->formatter->format($messages[array_rand($messages)]);
}
}
Тепер той, хто створює MessageGenerator, має також створити його форматувальник і передати (або «впровадити») його як аргумент конструктора:
$generator = new MessageGenerator(new TextFormatter());
$message = $generator->getHappyMessage();
Це і є впровадження залежностей: замість того, щоб створювати власні залежності, класи отримують їх через конструктор (або, рідше, через сетери чи властивості; див. /service_container/injection_types).
Другий крок — залежати від абстракції замість конкретного класу. Визначте інтерфейс для форматувальників і використовуйте його як тип-підказку конструктора:
// src/Formatter/FormatterInterface.php
namespace App\Formatter;
interface FormatterInterface
{
public function format(string $message): string;
}
// src/Service/MessageGenerator.php
namespace App\Service;
use App\Formatter\FormatterInterface;
class MessageGenerator
{
public function __construct(
private FormatterInterface $formatter,
) {
}
// ...
}
Проблеми початкового класу тепер розвʼязані: ви можете передати TextFormatter, HtmlFormatter або будь-який інший клас, що реалізує інтерфейс, налаштований у будь-який потрібний вам спосіб. У тестах ви можете передати мінімальний фейковий форматувальник, щоб протестувати логіку генерації повідомлень ізольовано:
// той самий клас працює з різними форматувальниками
$generator = new MessageGenerator(new HtmlFormatter());
Проте впровадження залежностей створює нову проблему: хтось має створити всі ці обʼєкти. У реальному застосунку сервіси залежать від інших сервісів, які залежать від інших сервісів. Вам довелося б памʼятати, як будувати кожен обʼєкт, створювати залежності в правильному порядку й оновлювати весь цей код щоразу, коли змінюється якийсь конструктор.
Розвʼязання цієї проблеми — робота контейнера сервісів: він зберігає «рецепт» того, як побудувати кожен сервіс, і створює обʼєкти за вас, у правильному порядку, лише коли потрібно і лише один раз (за замовчуванням ви отримуєте той самий екземпляр щоразу, коли просите сервіс). У застосунках Symfony вам навіть не треба писати ці рецепти: завдяки автопідключенню (autowiring) контейнер читає тип-підказки конструктора й сам зʼясовує залежності.
Отримання та використання сервісів
Щойно ви запускаєте застосунок Symfony, ваш контейнер уже містить багато сервісів. Вони як інструменти: чекають, поки ви ними скористаєтесь. У вашому контролері ви можете «попросити» сервіс у контейнера, вказавши тип аргумента — назву класу або інтерфейсу сервісу. Хочете щось залогувати? Без проблем:
// src/Controller/ProductController.php
namespace App\Controller;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class ProductController extends AbstractController
{
#[Route('/products')]
public function list(LoggerInterface $logger): Response
{
$logger->info('Look, I just used a service!');
// ...
}
}
Які ще сервіси доступні? Дізнайтеся, виконавши:
$ php bin/console debug:autowiring
Коли ви використовуєте ці тип-підказки в методах контролера або всередині ваших власних сервісів, Symfony автоматично передасть вам обʼєкт сервісу, що відповідає цьому типу.
Упродовж документації ви побачите, як користуватися багатьма різними сервісами, що живуть у контейнері.
Порада
Насправді в контейнері є набагато більше сервісів, і кожен сервіс має унікальний ідентифікатор у контейнері, як-от
request_stackабоrouter.default. Щоб отримати повний список, ви можете виконатиphp bin/console debug:container(див. як налагоджувати контейнер). Але здебільшого вам не варто цим перейматися. Див. як вибрати конкретний сервіс.
Створення/налаштування сервісів у контейнері
Ви також можете організувати власний код у сервіси. Розгляньте клас MessageGenerator, створений раніше, щоб показувати вашим користувачам випадкове радісне повідомлення. Якщо ви покладете цей код у контролер, його не можна буде повторно використати. Якщо визначити його як окремий клас, він стає сервісом, який ви можете одразу використати у своєму контролері:
// src/Controller/ProductController.php
namespace App\Controller;
use App\Service\MessageGenerator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class ProductController extends AbstractController
{
#[Route('/products/new')]
public function new(MessageGenerator $messageGenerator): Response
{
// завдяки тип-підказці контейнер створить
// новий MessageGenerator і передасть його вам!
// ...
$message = $messageGenerator->getHappyMessage();
$this->addFlash('success', $message);
// ...
}
}
Коли ви просите сервіс MessageGenerator, контейнер конструює новий обʼєкт MessageGenerator і повертає його. Він також конструює й впроваджує його залежність-форматувальник, як пояснено в наступних розділах. Але якщо ви ніколи не попросите цей сервіс, він ніколи не буде сконструйований: заощаджуючи памʼять і швидкість. Як бонус, сервіс MessageGenerator створюється лише один раз: той самий екземпляр повертається щоразу, коли ви його просите.
Типова конфігурація сервісів
Документація припускає, що ви використовуєте наведену нижче конфігурацію сервісів, яка є типовою конфігурацією для нового проєкту:
# config/services.yaml
services:
# типова конфігурація для сервісів у *цьому* файлі
_defaults:
autowire: true # автоматично впроваджує залежності у ваші сервіси.
autoconfigure: true # автоматично реєструє ваші сервіси як команди, підписники подій тощо.
# робить класи в src/ доступними для використання як сервіси
# це створює сервіс на кожен клас, ідентифікатором якого є повне імʼя класу
App\:
resource: '../src/'
# порядок у цьому файлі важливий, бо визначення сервісів
# завжди *заміщують* попередні; додавайте власну конфігурацію сервісів нижче
# ...
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return App::config([
'services' => [
// автопідключення й автоконфігурація увімкнені за замовчуванням при використанні App::config()
// '_defaults' => [
// 'autowire' => true, // автоматично впроваджує залежності у ваші сервіси.
// 'autoconfigure' => true, // автоматично реєструє ваші сервіси як команди, підписники подій тощо.
// ],
// робить класи в src/ доступними для використання як сервіси
// це створює сервіс на кожен клас, ідентифікатором якого є повне імʼя класу
'App\\' => [
'resource' => '../src/',
],
// порядок у цьому файлі важливий, бо визначення сервісів
// завжди *заміщують* попередні; додавайте власну конфігурацію сервісів нижче
],
]);
Порада
Значенням опції
resourceможе бути будь-який коректний glob pattern.
Завдяки цій конфігурації ви можете автоматично використовувати будь-які класи з теки src/ як сервіс, без потреби налаштовувати це вручну. id кожного сервісу — це його повне імʼя класу. Ви можете перевизначити будь-який імпортований сервіс, використавши його id (імʼя класу) далі в цьому файлі (наприклад, див. як вручну підключити аргументи). Якщо ви перевизначаєте сервіс, жодна з опцій (наприклад, public) не успадковується від імпорту (але перевизначений сервіс усе ж успадковує від _defaults).
Зверніть увагу
Стривайте, чи означає це, що кожен клас у
src/реєструється як сервіс? Навіть класи моделей? Насправді ні. Поки ви тримаєте свої імпортовані сервіси приватними, усі класи вsrc/, які не використовуються явно як сервіси, автоматично вилучаються з підсумкового контейнера. Насправді імпорт означає, що всі класи «доступні для використання як сервіси» без потреби налаштовувати їх вручну.
Якщо ви волієте підключати свій сервіс вручну, можете скористатися явною конфігурацією.
Виключення сервісів
Якщо якісь файли чи теки у вашому проєкті не мають ставати сервісами, ви можете виключити їх за допомогою опції exclude:
# config/services.yaml
services:
# ...
App\:
resource: '../src/'
exclude:
- '../src/SomeDirectory/'
- '../src/AnotherDirectory/'
- '../src/SomeFile.php'
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return App::config([
'services' => [
// ...
'App\\' => [
'resource' => '../src/',
'exclude' => [
'../src/SomeDirectory/',
'../src/AnotherDirectory/',
'../src/SomeFile.php',
],
],
],
]);
Порада
Значенням опції
excludeможе бути будь-який коректний glob pattern.
Виключення шляхів необовʼязкове, але воно трохи підвищить продуктивність у середовищі dev: виключені шляхи не відстежуються, тож їхня зміна не спричинить перебудову контейнера.
Якщо ви хочете виключити лише кілька сервісів, ви можете скористатися атрибутом Symfony\Component\DependencyInjection\Attribute\Exclude безпосередньо на своєму класі, щоб виключити його:
// src/Service/SomeService.php
namespace App\Service;
use Symfony\Component\DependencyInjection\Attribute\Exclude;
#[Exclude]
class SomeService
{
// ...
}
Кілька визначень сервісів з тим самим простором імен
У прикладах вище ключ кожного запису (наприклад, App\) — це префікс простору імен класів, які треба завантажити. Іноді вам потрібно завантажити класи того самого простору імен кількома групами, кожну з іншою конфігурацією (наприклад, щоб застосувати різні теги до кожної групи). Ви не можете зробити це попереднім синтаксисом, бо ключі конфігурації мають бути унікальними і в YAML-, і в PHP-файлах конфігурації.
Щоб це розвʼязати, використовуйте будь-який унікальний рядок як ключ кожної групи, а справжній префікс простору імен визначайте в опції namespace:
# config/services.yaml
services:
# 'command_handlers' і 'event_subscribers' не є ідентифікаторами сервісів чи
# просторами імен; це може бути будь-який унікальний рядок як ключ запису
command_handlers:
namespace: App\Domain\
resource: '../src/Domain/*/CommandHandler'
tags: [command_handler]
event_subscribers:
namespace: App\Domain\
resource: '../src/Domain/*/EventSubscriber'
tags: [event_subscriber]
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return App::config([
'services' => [
// 'command_handlers' і 'event_subscribers' не є ідентифікаторами сервісів чи
// просторами імен; це може бути будь-який унікальний рядок як ключ запису
'command_handlers' => [
'namespace' => 'App\Domain\\',
'resource' => '../src/Domain/*/CommandHandler',
'tags' => ['command_handler'],
],
'event_subscribers' => [
'namespace' => 'App\Domain\\',
'resource' => '../src/Domain/*/EventSubscriber',
'tags' => ['event_subscriber'],
],
],
]);
Опція autowire
Типова конфігурація встановлює autowire: true у секції _defaults, тож це стосується всіх сервісів, визначених у цьому файлі. З цим налаштуванням контейнер дивиться на тип-підказки аргументів у методі __construct() ваших сервісів і автоматично передає їм правильні сервіси. Якщо він не може цього зробити, ви побачите зрозумілий виняток із корисною підказкою. Уся ця стаття написана навколо автопідключення.
Автопідключення — це те, як «проблема впровадження залежностей», описана у вступі, зникає в застосунках Symfony: ви оголошуєте залежності тип-підказками, а решту робить контейнер. Здебільшого ви не писатимете жодної конфігурації сервісів.
Докладніше дивіться в документації з автопідключення сервісів (/service_container/autowiring), яка охоплює те, як працює автопідключення, як мати справу з кількома реалізаціями одного типу, атрибут #[Autowire] для аргументів, що не є сервісами, і генерування замикань із сервісів.
Опція autoconfigure
Типова конфігурація також встановлює autoconfigure: true у секції _defaults, тож це стосується всіх сервісів, визначених у цьому файлі. З цим налаштуванням контейнер дивиться на інтерфейси, які реалізують класи ваших сервісів (а також на їхні базові класи та PHP-атрибути), щоб визначити призначення кожного сервісу й автоматично інтегрувати його у функціональність, що його використовує.
Symfony потрібно знати, наприклад, які з ваших сервісів є консольними командами (щоб запускати їх, коли ви виконуєте їхню команду), які є підписниками подій (щоб викликати їх, коли стаються їхні події), які є розширеннями Twig тощо. Завдяки autoconfigure вам не треба нічого з цього оголошувати: створіть клас, що реалізує потрібний інтерфейс або розширює потрібний базовий клас, і Symfony виявить його й інтегрує за вас.
Усередині автоконфігурація використовує теги сервісів: мітки, додані до визначень сервісів, щоб позначити, що сервіс має бути оброблений якимось особливим чином Symfony або сторонніми бандлами. Здебільшого вам не доводиться працювати з тегами самотужки, бо автоконфігурація додає їх за вас. Наприклад, якщо ваш клас реалізує Twig\Extension\ExtensionInterface, autoconfigure додає тег twig.extension до сервісу, і Twig завантажує його як одне зі своїх розширень. Читайте більше про теги і про те, як використовувати їх явно, у статті про теги сервісів (/service_container/tags).
Автоконфігурація також працює з атрибутами. Деякі атрибути, як-от Symfony\Component\Messenger\Attribute\AsMessageHandler, Symfony\Component\EventDispatcher\Attribute\AsEventListener та Symfony\Component\Console\Attribute\AsCommand, зареєстровані для автоконфігурації. Будь-який клас, що використовує ці атрибути, отримає застосовані до нього теги. Атрибути автоконфігурації також розбираються на абстрактних класах при завантаженні сервісів із ресурсу.
Впровадження сервісів/конфігурації в сервіс
А що, якщо вам потрібен доступ до сервісу logger зсередини MessageGenerator? Без проблем! Створіть метод __construct() з аргументом $logger, що має тип-підказку LoggerInterface. Установіть його в нову властивість $logger і використовуйте пізніше:
// src/Service/MessageGenerator.php
namespace App\Service;
use App\Formatter\FormatterInterface;
use Psr\Log\LoggerInterface;
class MessageGenerator
{
public function __construct(
private FormatterInterface $formatter,
private LoggerInterface $logger,
) {
}
public function getHappyMessage(): string
{
$this->logger->info('About to find a happy message!');
// ...
}
}
Ось і все! Контейнер автоматично знатиме, що треба передати сервіс logger при створенні MessageGenerator. Звідки він знає, що так робити? Автопідключення. Ключ — це тип-підказка LoggerInterface у вашому методі __construct() і конфігурація autowire: true у services.yaml. Коли ви вказуєте тип аргумента, контейнер автоматично знайде відповідний сервіс.
Звідки вам знати, що для тип-підказки треба використати LoggerInterface? Ви можете або читати документацію тієї функціональності, яку використовуєте, або скористатися командою debug:autowiring, показаною раніше, щоб отримати список усіх типів, доступних для автопідключення у вашому застосунку.
Порада
Якщо якась залежність необовʼязкова (тобто ваш сервіс може працювати без неї), оголосіть аргумент як nullable зі значенням за замовчуванням
null(наприклад,private ?LoggerInterface $logger = null). Контейнер впровадить сервіс, якщо він існує, іnullінакше. Див. як зробити залежності необовʼязковими.
Впровадження значень, які не можна автопідключити
Автопідключення працює незалежно від того, скільки аргументів має конструктор, але воно працює лише тоді, коли аргументи є сервісами. Найпоширеніший випадок аргументів, що не є сервісами, — передавання конфігураційних значень у сервіси.
Припустімо, ви створюєте новий сервіс, щоб надсилати листа адміністраторові сайту щоразу, коли відбувається оновлення сайту, і що email адміністратора має бути налаштовуваним:
// src/Service/SiteUpdater.php
namespace App\Service;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
class SiteUpdater
{
public function __construct(
private MailerInterface $mailer,
private string $adminEmail,
) {
}
public function notifyOfSiteUpdate(): void
{
// ...
$email = new Email()
->from('[email protected]')
->to($this->adminEmail)
->subject('Site update just happened!')
->text('...');
$this->mailer->send($email);
}
}
Аргумент MailerInterface автопідключається як завжди. Але контейнер не може вгадати значення, яке треба передати в аргумент $adminEmail, тож якщо ви скористаєтеся цим сервісом, побачите помилку:
Cannot autowire service "App\Service\SiteUpdater": argument "$adminEmail" of method "__construct()" must have a type-hint or be given a value explicitly.
Рекомендований спосіб це розвʼязати — атрибут #[Autowire], який визначає значення для впровадження в тому самому місці, де оголошено аргумент:
// src/Service/SiteUpdater.php
namespace App\Service;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
// ...
class SiteUpdater
{
public function __construct(
private MailerInterface $mailer,
#[Autowire('[email protected]')]
private string $adminEmail,
) {
}
// ...
}
Атрибут #[Autowire] також може впроваджувати параметри, змінні середовища, вирази і навіть інші сервіси. Читайте більше про це у статті про автопідключення.
Як альтернативу, ви можете задати аргумент явно у конфігурації сервісів:
# config/services.yaml
services:
# ... так само, як раніше
# так само, як раніше
App\:
resource: '../src/'
exclude: '../src/{DependencyInjection,Entity,Kernel.php}'
# явно налаштовуємо сервіс
App\Service\SiteUpdater:
arguments:
$adminEmail: '[email protected]'
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use App\Service\SiteUpdater;
return App::config([
'services' => [
// ... так само, як раніше
// так само, як раніше
'App\\' => [
'resource' => '../src/',
'exclude' => '../src/{DependencyInjection,Entity,Kernel.php}',
],
// явно налаштовуємо сервіс
SiteUpdater::class => [
'arguments' => [
'$adminEmail' => '[email protected]',
],
],
],
]);
Завдяки цьому контейнер передасть [email protected] в аргумент $adminEmail методу __construct при створенні сервісу SiteUpdater. Інший аргумент усе одно буде автопідключено.
Але хіба це не крихко? На щастя, ні! Якщо ви перейменуєте аргумент $adminEmail на щось інше (наприклад, $mainEmail), ви отримаєте зрозумілий виняток при перезавантаженні наступної сторінки (навіть якщо та сторінка не використовує цей сервіс).
Впровадження скалярних значень і колекцій
Окрім впровадження сервісів, ви можете передати як аргумент сервісу значення будь-якого типу:
# config/services.yaml
services:
App\Service\SomeService:
arguments:
# рядкові, числові й булеві аргументи можна передавати «як є»
- 'Foo'
- true
- 7
- 3.14
# константи можуть бути вбудованими, визначеними користувачем або Enum
- !php/const E_ALL
- !php/const PDO::FETCH_NUM
- !php/const Symfony\Component\HttpKernel\Kernel::VERSION
- !php/const App\Config\SomeEnum::SomeCase
# коли не використовується автопідключення, ви можете передавати аргументи-сервіси явно
- '@some-service-id' # початковий '@' каже, що це ідентифікатор сервісу, а не рядок
- '@?some-service-id' # використання '?' означає передати null, якщо сервіс не існує
# якщо значення рядкового аргумента починається з '@', вам треба екранувати
# його, додавши ще один '@', щоб Symfony не вважала це сервісом;
# наступний приклад буде розібрано як рядок '@securepassword'
- '@@securepassword'
# бінарний вміст передається закодованим у рядки base64
- !!binary VGhpcyBpcyBhIEJlbGwgY2hhciAH
# колекції (масиви) можуть містити аргументи будь-якого типу
-
first: !php/const true
second: 'Foo'
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return App::config([
'services' => [
App\Service\SomeService::class => [
'arguments' => [
// рядкові, числові й булеві аргументи можна передавати «як є»
'Foo',
true,
7,
3.14,
// константи можуть бути вбудованими, визначеними користувачем або Enum
E_ALL,
\PDO::FETCH_NUM,
Symfony\Component\HttpKernel\Kernel::VERSION,
App\Config\SomeEnum::SomeCase,
// коли не використовується автопідключення, ви можете передавати аргументи-сервіси
// явно; це впаде з помилкою, якщо сервіс не існує
service('some-service-id'),
// це передасть null, якщо сервіс не існує
service('some-service-id')->nullOnInvalid(),
// колекції (масиви) можуть містити аргументи будь-якого типу
[
'first' => true,
'second' => 'Foo',
],
],
],
],
]);
Впровадження параметрів контейнера
Окрім обʼєктів сервісів, контейнер також містить конфігураційні значення, які називають параметрами. Основна стаття про конфігурацію Symfony детально пояснює конфігураційні параметри й показує всі їхні типи (рядкові, булеві, масивні, бінарні параметри та параметри-константи PHP).
Щоб впровадити параметр у сервіс, використовуйте його назву, оточену двома символами % (або спеціальну опцію #[Autowire]):
// src/Service/SiteUpdater.php
namespace App\Service;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
// ...
class SiteUpdater
{
public function __construct(
#[Autowire(param: 'app.admin_email')]
private string $adminEmail,
) {
}
// ...
}
# config/services.yaml
services:
App\Service\SiteUpdater:
arguments:
$adminEmail: '%app.admin_email%'
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use App\Service\SiteUpdater;
return App::config([
'services' => [
SiteUpdater::class => [
'arguments' => [
'$adminEmail' => param('app.admin_email'),
],
],
],
]);
use App\Service\SiteUpdater;
use Symfony\Component\DependencyInjection\ContainerBuilder;
$container = new ContainerBuilder();
$container->setParameter('app.admin_email', '[email protected]');
$container->register(SiteUpdater::class)
->setAutowired(true)
->setArgument('$adminEmail', '%app.admin_email%');
Попередження
Використання нотації
.у назвах параметрів — це конвенція Symfony, щоб параметри було легше читати. Параметри є плоскими елементами «ключ-значення»; їх не можна організувати у вкладений масив.
Той самий синтаксис працює для змінних середовища: використовуйте %env(SOME_VARIABLE)%, щоб впровадити значення змінної середовища під час виконання.
Впровадження значень на основі виразів
Іноді значення для впровадження не є статичним, а є результатом якоїсь логіки. Для таких випадків контейнер сервісів підтримує вирази, написані синтаксисом ExpressionLanguage (/reference/formats/expression_language).
Припустімо, що сервіс App\Mail\MailerConfiguration має метод getMailerMethod(), який повертає рядок на кшталт sendmail на основі якоїсь конфігурації, і що ви хочете передати результат цього методу як аргумент конструктора іншому сервісу:
// src/Mailer.php
namespace App;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class Mailer
{
public function __construct(
// через екранування, яке застосовує PHP, ви маєте додати 4 зворотні скісні риски на кожну оригінальну
#[Autowire(expression: 'service("App\\\\Mail\\\\MailerConfiguration").getMailerMethod()')]
private string $mailerMethod,
) {
}
// ...
}
# config/services.yaml
services:
# ...
App\Mailer:
# префікс '@=' обовʼязковий при використанні виразів для аргументів у YAML-файлах
arguments: ['@=service("App\\Mail\\MailerConfiguration").getMailerMethod()']
# при використанні рядків у подвійних лапках зворотну скісну риску треба екранувати двічі (див. https://yaml.org/spec/1.2/spec.html#id2787109)
# arguments: ["@=service('App\\\\Mail\\\\MailerConfiguration').getMailerMethod()"]
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use App\Mailer;
return App::config([
'services' => [
// ...
Mailer::class => [
// через екранування, яке застосовує PHP, ви маєте додати 4 зворотні скісні риски на кожну оригінальну
'arguments' => [expr("service('App\\\\Mail\\\\MailerConfiguration').getMailerMethod()")],
],
],
]);
У цьому контексті вам доступні 3 функції:
service
: Повертає заданий сервіс (див. приклад вище).
parameter
: Повертає значення конкретного параметра (синтаксис такий самий, як у service).
env
: Повертає значення змінної середовища.
Вам також доступний Symfony\Component\DependencyInjection\Container через змінну container, що дозволяє писати складніші вирази, як-от container.hasParameter('some_param') ? parameter('some_param') : 'default_value'.
Вирази можна використовувати в arguments, properties, як аргументи з configurator, як аргументи до calls (виклики методів) і у factories (фабрики сервісів, /service_container/factories).
Вибір конкретного сервісу
Сервіс MessageGenerator, створений раніше, вимагає аргумент LoggerInterface:
// src/Service/MessageGenerator.php
namespace App\Service;
use Psr\Log\LoggerInterface;
class MessageGenerator
{
public function __construct(
private LoggerInterface $logger,
) {
}
// ...
}
Проте в контейнері є кілька сервісів, що реалізують LoggerInterface, як-от logger, monolog.logger.request, monolog.logger.php тощо. Звідки контейнер знає, який використати?
У таких ситуаціях контейнер зазвичай налаштований так, щоб автоматично вибирати один із сервісів: у цьому випадку logger (читайте більше про те, чому, у розділі про псевдоніми автопідключення). Але ви можете це контролювати й передати інший логер:
// 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')]
private LoggerInterface $logger,
) {
}
// ...
}
# config/services.yaml
services:
# ... той самий код, що й раніше
# явно налаштовуємо сервіс
App\Service\MessageGenerator:
arguments:
# символ '@' важливий: саме він каже контейнеру,
# що ви хочете передати *сервіс* з ідентифікатором 'monolog.logger.request',
# а не просто *рядок* 'monolog.logger.request'
$logger: '@monolog.logger.request'
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use App\Service\MessageGenerator;
return App::config([
'services' => [
// ... той самий код, що й раніше
// явно налаштовуємо сервіс
MessageGenerator::class => [
'arguments' => [
'$logger' => service('monolog.logger.request'),
],
],
],
]);
Це каже контейнеру, що аргумент $logger методу __construct має використовувати сервіс з ідентифікатором monolog.logger.request.
Порада
Якщо вам потрібно вибирати між кількома реалізаціями одного типу в межах вашого застосунку, ви можете використовувати іменовані псевдоніми автопідключення замість того, щоб вручну підключати кожну точку впровадження.
Щоб отримати список можливих сервісів-логерів, які можна використовувати з автопідключенням, виконайте:
$ php bin/console debug:autowiring logger
Явне налаштування сервісів і аргументів
Автоматичне завантаження сервісів і автопідключення необовʼязкові. І навіть якщо ви їх використовуєте, можуть бути випадки, коли ви хочете підключити сервіс вручну. Наприклад, припустімо, що ви хочете зареєструвати два сервіси для класу SiteUpdater, кожен зі своїм email адміністратора. У цьому випадку кожен має мати унікальний ідентифікатор сервісу:
# config/services.yaml
services:
# ...
# це ідентифікатор сервісу
site_updater.superadmin:
class: App\Service\SiteUpdater
# ви ВСЕ ЩЕ можете використовувати тут автопідключення, але цей приклад показує, як це виглядає без нього
autowire: false
# вручну підключаємо всі аргументи
arguments:
- '@mailer'
- '[email protected]'
site_updater.normal_users:
class: App\Service\SiteUpdater
autowire: false
arguments:
- '@mailer'
- '[email protected]'
# Створюємо псевдонім, щоб за замовчуванням, якщо ви вказуєте тип SiteUpdater,
# використовувався site_updater.superadmin
App\Service\SiteUpdater: '@site_updater.superadmin'
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use App\Service\SiteUpdater;
return App::config([
'services' => [
// ...
// site_updater.superadmin — це ідентифікатор сервісу
'site_updater.superadmin' => [
'class' => SiteUpdater::class,
// ви ВСЕ ЩЕ можете використовувати тут автопідключення, але цей приклад показує, як це виглядає без нього
'autowire' => false,
// вручну підключаємо всі аргументи
'arguments' => [
service('mailer'),
'[email protected]',
],
],
'site_updater.normal_users' => [
'class' => SiteUpdater::class,
'autowire' => false,
'arguments' => [
service('mailer'),
'[email protected]',
],
],
// створюємо псевдонім, щоб за замовчуванням, якщо ви вказуєте тип SiteUpdater,
// використовувався site_updater.superadmin
SiteUpdater::class => service('site_updater.superadmin'),
],
]);
У цьому випадку зареєстровано два сервіси: site_updater.superadmin і site_updater.normal_users. Завдяки псевдоніму, якщо ви вкажете тип SiteUpdater, буде передано перший (site_updater.superadmin). Читайте більше про псевдоніми у статті про автопідключення.
Якщо ви хочете передати другий, вам треба буде вручну підключити сервіс або створити іменований псевдонім автопідключення.
Попередження
Якщо ви не створите псевдонім і завантажуєте всі сервіси з
src/, то створено три сервіси (автоматичний сервіс + два ваші), і автоматично завантажений сервіс буде передано за замовчуванням, коли ви вказуєте типSiteUpdater. Саме тому створити псевдонім — гарна ідея.
Імпорт конфігурації за допомогою imports
За замовчуванням конфігурація сервісів живе у config/services.yaml. Але якщо цей файл стає великим, ви вільні організувати конфігурацію в кілька файлів. Припустімо, ви вирішили перенести частину конфігурації в новий файл:
# config/services/mailer.yaml
parameters:
# ... деякі параметри
services:
# ... деякі сервіси
// config/services/mailer.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return App::config([
'parameters' => [
// ... деякі параметри
],
'services' => [
// ... деякі сервіси
],
]);
Щоб імпортувати цей файл, використайте ключ imports із будь-якого іншого файлу й передайте відносний або абсолютний шлях до імпортованого файлу:
# config/services.yaml
imports:
- { resource: services/mailer.yaml }
# якщо ви хочете імпортувати цілу теку:
- { resource: services/ }
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
# ...
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return App::config([
'imports' => [
['resource' => 'services/mailer.php'],
// якщо ви хочете імпортувати цілу теку:
['resource' => 'services/'],
],
'services' => [
'App\\' => [
'resource' => '../src/',
],
// ...
],
]);
Під час імпорту теки або використання glob-патернів ви можете скористатися опцією exclude, щоб пропустити конкретні файли чи патерни:
# config/services.yaml
imports:
- { resource: services/, exclude: ['services/legacy_*.yaml'] }
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return App::config([
'imports' => [
['resource' => 'services/', 'exclude' => ['services/legacy_*.php']],
],
]);
Нововведення 8.1
Опцію
excludeдляimportsбуло впроваджено в Symfony 8.1.
Коли Symfony завантажує конфігураційний файл, вона спершу обробляє всі імпортовані файли в порядку, у якому вони перелічені під ключем imports. Після того як усі імпорти оброблено, вона обробляє параметри й сервіси, визначені безпосередньо в поточному файлі. На практиці це означає, що пізніші визначення заміщують попередні.
Наприклад, якщо ви використовуєте типову конфігурацію services.yaml, як у прикладі вище, ваш головний файл config/services.yaml використовує простір імен App\ для автовиявлення сервісів і завантажує їх після всіх імпортованих файлів. Якщо імпортований файл (наприклад, config/services/mailer.yaml) визначає сервіс, який також автовиявляється, визначення з services.yaml матиме пріоритет.
Не перекладено далі за текстом оригіналу: Public Versus Private Services, Message Buses and Handlers, Learn More, Standalone Use (service-container-standalone).
Виправити терміни, дописати розділ або взяти нову главу може кожен. Термінологію узгоджуємо в глосарії, щоб переклад лишався однорідним.