<? phpukraine ДОКУМЕНТАЦІЯ
⌕Пошук по платформі
Документація українською

Переклад офіційної документації українською. Кожен розділ показує стан готовності: недоперекладене позначене відкрито, а не приховане.

ВЕРСІЯ
АКТУАЛЬНА Актуальний реліз. Переклад наздоганяє оригінал, розділи з низьким відсотком позначені у змісті.
ОСНОВИ · ПЕРЕКЛАДЕНО оновлено 18 вересня 2026

Сервіс-контейнер · 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('admin@example.com')
            ->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('manager@example.com')]
        private string $adminEmail,
    ) {
    }

    // ...
}

Атрибут #[Autowire] також може впроваджувати параметри, змінні середовища, вирази і навіть інші сервіси. Читайте більше про це у статті про автопідключення.

Як альтернативу, ви можете задати аргумент явно у конфігурації сервісів:

# config/services.yaml
services:
    # ... так само, як раніше

    # так само, як раніше
    App\:
        resource: '../src/'
        exclude: '../src/{DependencyInjection,Entity,Kernel.php}'

    # явно налаштовуємо сервіс
    App\Service\SiteUpdater:
        arguments:
            $adminEmail: 'manager@example.com'
// 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' => 'manager@example.com',
            ],
        ],
    ],
]);

Завдяки цьому контейнер передасть manager@example.com в аргумент $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', 'manager@example.com');

$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'
            - 'superadmin@example.com'

    site_updater.normal_users:
        class: App\Service\SiteUpdater
        autowire: false
        arguments:
            - '@mailer'
            - 'contact@example.com'

    # Створюємо псевдонім, щоб за замовчуванням, якщо ви вказуєте тип 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'),
                'superadmin@example.com',
            ],
        ],
        'site_updater.normal_users' => [
            'class' => SiteUpdater::class,
            'autowire' => false,
            'arguments' => [
                service('mailer'),
                'contact@example.com',
            ],
        ],

        // створюємо псевдонім, щоб за замовчуванням, якщо ви вказуєте тип 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 матиме пріоритет.

До розділу «Виключення сервісів з автовиявлення» і далі.

Щоб автовиявлення (auto-discovery) не перезаписувало ваші власні визначення сервісів, скористайтеся однією зі стратегій:

  1. Виключіть сервіси з автовиявлення
  2. Перевизначте сервіси в тому самому файлі
  3. Керуйте порядком імпорту

Виключення сервісів з автовиявлення

Змініть визначення App\ так, щоб воно використовувало опцію exclude. Це не дасть Symfony автоматично реєструвати класи, які ви описали вручну в іншому місці:

# config/services.yaml
imports:
    - { resource: services/mailer.yaml }
    # ... інші імпорти

services:
    _defaults:
        autowire: true
        autoconfigure: true

    App\:
        resource: '../src/'
        exclude:
            - '../src/Mailer/'
            - '../src/SpecificClass.php'
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return App::config([
    'imports' => [
        ['resource' => 'services/mailer.php'],
        // ... інші імпорти
    ],
    'services' => [
        'App\\' => [
            'resource' => '../src/',
            'exclude' => [
                '../src/Mailer/',
                '../src/SpecificClass.php',
            ],
        ],
    ],
]);

Перевизначення сервісів у тому самому файлі

Конкретні сервіси можна описати після блоку автовиявлення App\ у тому самому файлі. Пізніші визначення перекриють автоматично зареєстровані:

# config/services.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true

    App\:
        resource: '../src/'

    App\Mailer\MyMailer:
        arguments: ['%env(MAILER_DSN)%']
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use App\Mailer\MyMailer;

return App::config([
    'services' => [
        'App\\' => [
            'resource' => '../src/',
        ],
        MyMailer::class => [
            'arguments' => [
                env('MAILER_DSN'),
            ],
        ],
    ],
]);

Керування порядком імпорту

Винесіть конфігурацію автовиявлення App\ в окремий файл та імпортуйте його перед файлами з конкретнішими сервісами. Тоді окремі визначення сервісів зможуть перекрити автоматично виявлені.

# config/services/autodiscovery.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true

    App\:
        resource: '../../src/'
        exclude:
            - '../../src/Mailer/'

# config/services/mailer.yaml
services:
    App\Mailer\SpecificMailer:
        # ... власна конфігурація

# config/services.yaml
imports:
    - { resource: services/autodiscovery.yaml }
    - { resource: services/mailer.yaml }
    - { resource: services/ }

services:
    # визначення тут перекривають усе з імпортів вище
    # більшість визначень варто тримати всередині імпортованих файлів
// config/services/autodiscovery.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return App::config([
    'services' => [
        'App\\' => [
            'resource' => '../../src/',
            'exclude' => [
                '../../src/Mailer/',
            ],
        ],
    ],
]);

// config/services/mailer.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return App::config([
    'services' => [
        App\Mailer\SpecificMailer::class => [
            // ... власна конфігурація
        ],
    ],
]);

// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return App::config([
    'imports' => [
        ['resource' => 'services/autodiscovery.php'],
        ['resource' => 'services/mailer.php'],
        ['resource' => 'services/'],
    ],
    'services' => [
        // визначення тут перекривають усе з імпортів вище
        // більшість визначень варто тримати всередині імпортованих файлів
    ],
]);

Імпорт конфігурації через розширення контейнера

Конфігурація контейнера зі сторонніх бандлів, зокрема сервіси ядра Symfony, зазвичай завантажується інакше: через розширення контейнера (container extension).

Усередині кожен бандл описує свої сервіси у файлах, схожих на ті, що ви вже бачили. Але ці файли підключаються не директивою imports. Замість цього бандли використовують розширення впровадження залежностей, яке завантажує файли автоматично. Щойно ви вмикаєте бандл, викликається його розширення, і воно здатне завантажити конфігураційні файли сервісів.

Фактично кожен конфігураційний файл у config/packages/ передається розширенню відповідного бандла (наприклад, FrameworkBundle чи TwigBundle) і використовується для подальшого налаштування цих сервісів.

Умовна реєстрація сервісів

Іноді сервіс потрібно реєструвати лише за певних обставин. Наразі єдина підтримувана умова - конфігураційне середовище (configuration environment). Наприклад, щоб зареєструвати сервіс лише в середовищі dev:

use Symfony\Component\DependencyInjection\Attribute\When;

// SomeClass реєструється лише в середовищі "dev"

#[When(env: 'dev')]
class SomeClass
{
    // ...
}

// до одного класу можна застосувати кілька атрибутів When

#[When(env: 'dev')]
#[When(env: 'test')]
class AnotherClass
{
    // ...
}
# config/services.yaml
services:
    App\Service\SomeClass: ~

when@dev:
    services:
        App\Service\AnotherClass: ~
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return static function (ContainerConfigurator $container): void {
    $services = $container->services();

    $services->set(App\Service\SomeClass::class);

    if ('dev' === $container->env()) {
        $services->set(App\Service\AnotherClass::class);
    }
};

Попередження

Секція _defaults діє лише на сервіси, описані в тому самому блоці services. Кожен блок when@<env> має власну область видимості й не успадковує _defaults з основної секції services. Перевизначайте _defaults у кожному блоці when@<env>, де вони вам потрібні:

# config/services.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true
    # ...

when@prod:
    services:
        _defaults:
            autowire: true
            autoconfigure: true
        # ...

Якщо потрібно, навпаки, не реєструвати сервіс у певному середовищі, скористайтеся атрибутом #[WhenNot]:

use Symfony\Component\DependencyInjection\Attribute\WhenNot;

// SomeClass реєструється в усіх середовищах, окрім "dev"

#[WhenNot(env: 'dev')]
class SomeClass
{
    // ...
}

// до одного класу можна застосувати кілька атрибутів WhenNot

#[WhenNot(env: 'dev')]
#[WhenNot(env: 'test')]
class AnotherClass
{
    // ...
}

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

В усіх прикладах цієї статті сервіси використовуються через впровадження залежностей: їх передають аргументами в сервіси й контролери, яким вони потрібні. Але отримати сервіс можна й інакше: контейнер сам по собі є PHP-обʼєктом, тож код, що має до нього доступ, може попросити будь-який сервіс напряму через $container->get('service_id').

Діставати сервіси у такий спосіб у власному коді наполегливо не рекомендується: це приховує справжні залежності ваших класів і зводить нанівець більшість переваг впровадження залежностей, описаних на початку статті. Сенс у цьому є лише в точці входу застосунку (саме тому так робиться в розділі про самостійне використання) та в деяких тестах.

Тому кожен визначений сервіс за замовчуванням приватний. Коли сервіс приватний, ви не можете звернутися до нього напряму через $container->get(); єдиний спосіб його використати - впровадження залежностей. За рекомендованою практикою всі ваші сервіси мають лишатися приватними.

Приватні сервіси особливі тим, що дозволяють контейнеру оптимізувати, чи й як їх створювати. Це підвищує продуктивність контейнера. Ще ви отримуєте кращі помилки: якщо послатися на неіснуючий сервіс, ви побачите зрозуміле повідомлення про помилку при оновленні будь-якої сторінки, навіть якщо проблемний код на цій сторінці не виконувався б.

Примітка

Приватному сервісу все одно можна дати додаткове імʼя (аліас) або зробити його доступним через публічний аліас. Докладніше про аліаси - у статті про автопідключення (autowiring).

Порада

Поширена причина робити сервіси публічними - отримувати деякі з них за id під час виконання (наприклад, щоб вибрати один сервіс чи інший залежно від якогось значення). Замість робити такі сервіси публічними, скористайтеся локатором сервісів (service locator): він дає прямий доступ до наперед визначеного набору сервісів, залишаючи їх приватними.

Якщо жоден із цих варіантів вам не підходить і сервіс все ж треба зробити публічним, перевизначте налаштування public:

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

use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;

#[Autoconfigure(public: true)]
class PublicService
{
    // ...
}
# config/services.yaml
services:
    # ... той самий код, що й раніше

    # явно налаштовуємо сервіс
    App\Service\PublicService:
        public: true
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use App\Service\PublicService;

return App::config([
    'services' => [
        // ... той самий код, що й раніше

        // явно налаштовуємо сервіс
        PublicService::class => [
            'public' => true,
        ],
    ],
]);

Отримання запиту в сервісі

Коли в сервісі потрібен доступ до поточного запиту, впровадьте сервіс RequestStack і візьміть запит із нього:

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

use Symfony\Component\HttpFoundation\RequestStack;

class NewsletterSender
{
    public function __construct(
        private RequestStack $requestStack,
    ) {
    }

    public function anyMethod(): void
    {
        $request = $this->requestStack->getCurrentRequest();
        // ... робимо щось із запитом
    }

    // ...
}

Порада

У контролері обʼєкт Request можна отримати, передавши його аргументом у метод-дію. Подробиці - у розділі controller-request-argument.

Налагодження та перевірка контейнера

Наведені нижче команди показують усю інформацію, збережену в контейнері, що корисно для налагодження проблем із впровадженням залежностей:

# показує всі сервіси, зареєстровані в контейнері (публічні й приватні),
# і PHP-клас, повʼязаний із кожним із них
$ php bin/console debug:container

# додайте цю опцію, щоб показати також "приховані сервіси" (ті, чий ID починається з крапки)
$ php bin/console debug:container --show-hidden

# показує детальну інформацію про окремий сервіс, зокрема його
# аргументи, теги та решту конфігурації
$ php bin/console debug:container App\Service\Mailer

# показує всі сервіси, позначені заданим тегом
$ php bin/console debug:container --tag=kernel.event_listener

# якщо передати неповну назву тега, команда покаже
# інтерактивний список усіх тегів, що збігаються
$ php bin/console debug:container --tag=kernel

# показує всі типи, які можна використовувати як type-hint при автопідключенні
$ php bin/console debug:autowiring

# передайте пошуковий термін, щоб відфільтрувати результати
$ php bin/console debug:autowiring logger

Перевірка визначень сервісів

Команда lint:container виконує додаткові перевірки, щоб переконатися, що контейнер налаштовано правильно. Її корисно запускати перед розгортанням застосунку в продакшн (наприклад, на сервері безперервної інтеграції):

$ php bin/console lint:container

# за бажання можна примусово розвʼязати змінні середовища;
# команда завершиться помилкою, якщо якоїсь із цих змінних бракує
$ php bin/console lint:container --resolve-env-vars

Виконання цих перевірок під час кожної компіляції контейнера шкодить продуктивності. Тому їх реалізовано у вигляді проходів компілятора (compiler passes) CheckTypeDeclarationsPass і CheckAliasValidityPass, які вимкнені за замовчуванням і вмикаються лише при запуску команди lint:container. Якщо втрата продуктивності вас не турбує, ці проходи компілятора можна ввімкнути у своєму застосунку.

Використання контейнера в самостійних PHP-застосунках

Контейнер сервісів і всі можливості, показані в цій статті, надає компонент DependencyInjection, який реалізує сумісний із PSR-11 контейнер. У застосунках, не побудованих на фреймворку Symfony, спершу встановіть компонент:

$ composer require symfony/dependency-injection

Реєстрація та отримання сервісів

У самостійних застосунках немає ні файлу config/services.yaml, ні конфігурації за замовчуванням. Натомість ви самі створюєте контейнер і реєструєте сервіси за допомогою класу ContainerBuilder. Візьмімо класи MessageGenerator і FormatterInterface зі вступу до цієї статті:

use App\Formatter\TextFormatter;
use App\Service\MessageGenerator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

$container = new ContainerBuilder();

// реєструємо сервіс форматера
$container->register('app.text_formatter', TextFormatter::class);

// реєструємо генератор повідомлень і вказуємо контейнеру впровадити сервіс
// форматера першим аргументом конструктора; клас Reference змушує контейнер
// впровадити сервіс, а не переданий рядок
$container->register('app.message_generator', MessageGenerator::class)
    ->addArgument(new Reference('app.text_formatter'));

// просимо в контейнера сервіс: він створює форматер, потім
// генератор і нарешті впроваджує перший у другий
$messageGenerator = $container->get('app.message_generator');
$message = $messageGenerator->getHappyMessage();

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

use App\Service\MessageGenerator;
use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();
$container->setParameter('app.locale', 'es');

$container->register('app.message_generator', MessageGenerator::class)
    // синтаксис %...% каже контейнеру впровадити значення параметра
    ->addArgument('%app.locale%');

// з параметрами можна працювати й напряму
// (назви параметрів чутливі до регістру)
$container->hasParameter('app.locale');
$container->getParameter('app.locale');

Примітка

Параметр можна задати лише до компіляції контейнера, а не під час виконання. Про компіляцію йдеться далі в цьому розділі.

Крім аргументів конструктора, можна налаштувати й інші типи впровадження, наприклад виклик сетерів (доступні типи описані в /service_container/injection_types):

use App\Service\MessageGenerator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

$container = new ContainerBuilder();

$container->register('app.message_generator', MessageGenerator::class)
    ->addMethodCall('setLogger', [new Reference('logger')]);

Отримання сервісів, яких не існує

За замовчуванням спроба отримати неіснуючий сервіс призводить до винятку. Цю поведінку можна змінити, передавши другий аргумент у метод get():

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;

$container = new ContainerBuilder();

// ...

$messageGenerator = $container->get('app.message_generator', ContainerInterface::NULL_ON_INVALID_REFERENCE);

Ось усі можливі варіанти поведінки:

  • ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE: кидає виняток під час компіляції (поведінка за замовчуванням);
  • ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE: кидає виняток під час виконання, при спробі звернутися до відсутнього сервісу;
  • ContainerInterface::NULL_ON_INVALID_REFERENCE: повертає null;
  • ContainerInterface::IGNORE_ON_INVALID_REFERENCE: ігнорує команду, що обгортає звернення до посилання (наприклад, пропускає сетер, якщо сервісу не існує);
  • ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE: ігнорує або повертає null для неініціалізованих сервісів чи некоректних посилань.

Налаштування контейнера через конфігураційні файли

Крім опису сервісів у PHP, як у попередніх прикладах, самостійні застосунки можуть використовувати конфігураційні файли. Для цього потрібно додатково встановити компонент Config:

$ composer require symfony/config

Далі створіть завантажувач для формату ваших конфігураційних файлів і завантажте їх у контейнер:

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

$container = new ContainerBuilder();

// завантаження конфігураційного файлу YAML (для цього треба також встановити
// компонент Yaml: composer require symfony/yaml)
$loader = new YamlFileLoader($container, new FileLocator(__DIR__));
$loader->load('services.yaml');

// завантаження конфігураційного файлу PHP
$loader = new PhpFileLoader($container, new FileLocator(__DIR__));
$loader->load('services.php');

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

# services.yaml
parameters:
    app.locale: 'es'

services:
    app.text_formatter:
        class: App\Formatter\TextFormatter

    app.message_generator:
        class: App\Service\MessageGenerator
        arguments: ['@app.text_formatter']
// services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use App\Formatter\TextFormatter;
use App\Service\MessageGenerator;

return App::config([
    'parameters' => [
        'app.locale' => 'es',
    ],
    'services' => [
        'app.text_formatter' => [
            'class' => TextFormatter::class,
        ],
        'app.message_generator' => [
            'class' => MessageGenerator::class,
            'arguments' => [service('app.text_formatter')],
        ],
    ],
]);

Компіляція контейнера та найкращі практики

Перед використанням контейнера викличте метод compile(), щоб розвʼязати параметри, оптимізувати визначення сервісів і перевірити їхню коректність:

// ...
$container->compile();

$messageGenerator = $container->get('app.message_generator');

Процес компіляції є ще й точкою розширення контейнера: ви можете зареєструвати проходи компілятора, які змінюють визначення сервісів до того, як контейнер буде заморожено. Докладніше про них - у /service_container/compiler_passes.

Насамкінець: хоч сервіси й можна брати з контейнера напряму, краще звести це до мінімуму. Звертайтеся до контейнера якомога рідше, у точці входу застосунку, а всюди інде покладайтеся на впровадження через конструктор. Інакше ваші класи стануть завʼязаними на конкретний обʼєкт контейнера, а це ускладнює їх повторне використання й тестування.

Дамп скомпільованого контейнера для продуктивності

Компілювати контейнер на кожен запит повільно. Тому застосунки Symfony кешують скомпільований контейнер у var/cache/ і перекомпільовують його лише тоді, коли конфігурація змінилася. У самостійних застосунках того самого результату можна досягти, зробивши дамп скомпільованого контейнера у PHP-файл класом PhpDumper і перевикористовуючи цей файл у наступних запитах:

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;

$file = __DIR__.'/cache/container.php';

if (file_exists($file)) {
    require_once $file;
    $container = new ProjectServiceContainer();
} else {
    $container = new ContainerBuilder();
    // ...
    $container->compile();

    $dumper = new PhpDumper($container);
    file_put_contents($file, $dumper->dump());
}

ProjectServiceContainer - стандартна назва класу задампленого контейнера; змінити її можна опцією class методу dump().

Порада

Функція file_put_contents() не атомарна, що може спричинити проблеми в продакшні з кількома одночасними запитами. Скористайтеся методом dumpFile() з компонента Filesystem або класом ConfigCache, показаним у наступному прикладі.

Попередній приклад ніколи не перебудовує контейнер, тож після кожної зміни конфігурації файл кешу доводиться видаляти вручну. Натомість візьміть клас ConfigCache з компонента Config, щоб кешований контейнер перебудовувався автоматично. Будівник контейнера відстежує всі ресурси, використані для його налаштування (конфігураційні файли, класи розширень, проходи компілятора тощо), а ConfigCache за ними визначає, що кеш застарів, щойно будь-який із цих файлів змінюється:

use Symfony\Component\Config\ConfigCache;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;

// у режимі налагодження кеш перебудовується щоразу, коли змінюється якийсь ресурс конфігурації;
// поза режимом налагодження завжди використовується кешований контейнер, якщо він існує
$isDebug = true;

$file = __DIR__.'/cache/container.php';
$containerConfigCache = new ConfigCache($file, $isDebug);

if (!$containerConfigCache->isFresh()) {
    $container = new ContainerBuilder();
    // ...
    $container->compile();

    $dumper = new PhpDumper($container);
    $containerConfigCache->write(
        $dumper->dump(['class' => 'MyCachedContainer']),
        $container->getResources()
    );
}

require_once $file;
$container = new MyCachedContainer();

Дізнатися більше

Стан перекладу

Перекладаємо з офіційної документації, розділ за розділом, і не ховаємо недоперекладене. Помітили неточність у терміні чи реченні: напишіть, виправимо.

46%
Готовності
2
У роботі
14
Ще не перекладено
Глосарій термінів