Сервіс-контейнер

Дата оновлення перекладу 2024-06-10

Сервіс-контейнер

Screencast

Ви віддаєте перевагу відео-урокам? Подивіться Symfony Fundamentals screencast series.

Ваш застосунок повний корисних об'єктів: об'єкт "Mailer" може допомогти вам відправляти електронні листи, в той час як інший об'єкт може допомогти вам зберігати дані у базу даних. Майже все, що "робить" ваш застосунок, насправді виконується одним з цих об'єктів. І кожний раз, коли ви встановлюєте новий пакет, ви отримуєте доступ до нових об'єктів!

У Symfony, ці корисні об'єкти називаються сервісами, і кожний сервіс живе всередині дуже особливого об'єкта під назвою сервіс-контейнер. Контейнер дозволяє вам централізувати те, як створюються об'єкти. Він робить ваше життя легшим, пропагує сильну архітектуру, а також дуже швидкий!

Отримання та використання сервісів

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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!');

        // ...
    }
}

Які ще сервіси існують? Дізнайтеся, запустивши:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ php bin/console debug:autowiring

  # це лише *маленький* приклад виведення...

  Автомонтовані типи
  ==================

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

   Описати екземпляр логера.
   Psr\Log\LoggerInterface (monolog.logger)

   Запросити стек, що контролює життєвий цикл запитів.
   Symfony\Component\HttpFoundation\RequestStack (request_stack)

   RouterInterface - це інтерфейс, який мають реалізовувати усі класи Router.
   Symfony\Component\Routing\RouterInterface (router.default)

  [...]

Коли ви використовуєте ці підказки у своїх методах контролера або всередині ваших власних сервісів , Symfony буде автоматично передавати вам об'єкт сервісу, відповідний до цього типу.

Читаючи документи, ви побачите як використовувати багато різних сервісів, які живуть у контейнері.

Tip

Насправді існує багато інших сервісів у контейнері, і кожний сервіс має у ньому унікальний id, наприклад, request_stack або router.default. Щоб побачити повний список, ви можете виконати php bin/console debug:container. Але у більшості випадків вам не треба буде про це турбуватися. Див. . Див. Як налагоджувати сервіс-контейнер та список сервісів.

Створення/конфігурація сервісів у контейнері

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Service/MessageGenerator.php
namespace App\Service;

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!',
        ];

        $index = array_rand($messages);

        return $messages[$index];
    }
}

Вітаємо! Ви щойно створили ваш перший клас сервісів! Ви можете використати його одразу ж у вашому контролері:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Controller/ProductController.php
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 створюється лише один раз: кожний раз, коли ви запитуєте його, вам повертається один і той самий екземпляр.

Документація припускає, що ви використовуєте наступну конфігурацію сервісу, яка є конфігурацією за замовчуванням для нового проекту:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# config/services.yaml
services:
    # конфігурація дл сервісів за замовчуванням у *цьому* файлі
    _defaults:
        autowire: true      # Автоматично впровпаджує залежності у ваші сервіси.
        autoconfigure: true # Автоматично реєструє ваші сервіси як команди, підписники подій і т.д.

    # робить класи в src/ доступними для використання в якості сервісів
    # створює по сервісу у класі, чий id є повним іменем класу
    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'

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

    # ...

Tip

Значення опцій resource і exclude може бути будь-яким валідним глобальним патерном. Значення опції exclude також може бути масивом глобальних патернів.

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

Якщо ви віддаєте перевагу створенню сервісу вручну, це також можливо: див. .

Обмеження сервісів для конкретного середовища Symfony

Якщо ви використовуєте PHP 8.0 або новіше, ви можете використати PHP-атрибут #[When], щоб зареєструвати клас в якості сервісу лише у деяких середовищах:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use Symfony\Component\DependencyInjection\Attribute\When;

// SomeClass is only registered in the "dev" environment

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

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

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

Впровпадження сервісів/конфігурації у сервіс

Що, якщо вам потрібно отримати доступ до сервісу logger з MessageGenerator? Не проблема! Створіть метод __construct() з аргументом $logger, який має підказку LoggerInterface. Встановіть це у новій властивості $logger та використайте його пізніше:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Service/MessageGenerator.php
namespace App\Service;

use Psr\Log\LoggerInterface;

class MessageGenerator
{
    public function __construct(
        private LoggerInterface $logger,
    ) {
    }

    public function getHappyMessage(): string
    {
        $this->logger->info('About to find a happy message!');
        // ...
    }
}

Ось і все! Контейнер автоматично знатиме, що йому потрібно передати сервіс logger при завантаженні MessageGenerator. Звідки він це знає? Автомонтування . Ключом є підказка LoggerInterface у вашому методі __construct() і конфігурація autowire: true у services.yml. Коли ви додаєте підказку до аргументу, контейнер автоматично шукатиме відповідний сервіс. Якщо це не вдасться, ви побчите чітке виключення з пропозицією допомоги.

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

Як вам знати, що слід використати LoggerInterface для підказки? Ви можете або прочитати документи для тієї функцій, яку ви використовуєте, або отримати список автоматичного додавання підказок, викликавши:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ php bin/console debug:autowiring

  # це лише *маленький* приклад виведення...

  Describes a logger instance.
  Psr\Log\LoggerInterface (monolog.logger)

  Request stack that controls the lifecycle of requests.
  Symfony\Component\HttpFoundation\RequestStack (request_stack)

  RouterInterface is the interface that all Router classes must implement.
  Symfony\Component\Routing\RouterInterface (router.default)

  [...]

Управління багатьма сервісами

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// src/Service/SiteUpdateManager.php
namespace App\Service;

use App\Service\MessageGenerator;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;

class SiteUpdateManager
{
    public function __construct(
        private MessageGenerator $messageGenerator,
        private MailerInterface $mailer,
    ) {
    }

    public function notifyOfSiteUpdate(): bool
    {
        $happyMessage = $this->messageGenerator->getHappyMessage();

        $email = (new Email())
            ->from('admin@example.com')
            ->to('manager@example.com')
            ->subject('Site update just happened!')
            ->text('Someone just updated the site. We told them: '.$happyMessage);

        $this->mailer->send($email);

        // ...

        return true;
    }
}

Це використовує сервіси MessageGenerator і Mailer. Це не проблема, ми запитуємо їх використовуючи підказки їх класів та імен інтерфейсів! Тепер цей новий сервіс готовий до використання. У контролері, наприклад, ви можете використовувати підказки нового класу SiteUpdateManager та використати його:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Controller/SiteController.php
namespace App\Controller;

use App\Service\SiteUpdateManager;
// ...

class SiteController extends AbstractController
{
    public function new(SiteUpdateManager $siteUpdateManager): Response
    {
        // ...

        if ($siteUpdateManager->notifyOfSiteUpdate()) {
            $this->addFlash('success', 'Notification mail was sent successfully.');
        }

        // ...
    }
}

Завдяки автомонтуванню та підказкам у __construct(), контейнер створює об'єкт SiteUpdateManager і передає йому правильний аргумент. У більшості випадків це працює ідеально.

Підключення аргументів вручну

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src/Service/SiteUpdateManager.php
  // ...

  class SiteUpdateManager
  {
      // ...
+    private string $adminEmail;

      public function __construct(
          private MessageGenerator $messageGenerator,
          private MailerInterface $mailer,
+        private string $adminEmail
      ) {
      }

      public function notifyOfSiteUpdate(): bool
      {
          // ...

          $email = (new Email())
              // ...
-            ->to('manager@example.com')
+            ->to($this->adminEmail)
              // ...
          ;
          // ...
      }
  }

Якщо ви внесети ці зміни та оновите його, ви побачите помилку:

Cannot autowire service "App\Service\SiteUpdateManager": argument "$adminEmail" of method "__construct()" must have a type-hint or be given a value explicitly. (Неможливо автоматично підключити сервіс "App\Service\SiteUpdateManager": аргумент "$adminEmail" методу "__construct()" повинен мати підказку або точно задане значення).

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

1
2
3
4
5
6
7
8
9
10
11
12
13
# config/services.yaml
services:
    # ... те ж саме, що і раніше

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

    # чітко сконфігуруйте сервіс
    App\Service\SiteUpdateManager:
        arguments:
            $adminEmail: 'manager@example.com'

Завдяки цьому, контейнер передаватиме manager@example.com аргументу $adminEmail в __construct при створенні сервісу SiteUpdateManager. Інші аргументи всеодно будуть автоматизовані.

Але хіба це не крихко? На щастя - ні! Якщо ви перейменуєте аргумент $adminEmail на щось інше, наприклад, $mainEmail, ви отримаєте чітке виключення при перезавантаженні наступной сторінки (навіть якщо сторінки не використовують цей сервіс).

Параметри сервісу

На додаток до об'єктів сервісу, контейнер також містить конфігурацію під назвою параметри. Основна стаття про конфігурацію Symfony детально пояснює параметри конфігурації та демонструє всі їхні типи (параметри рядку, булевого значення, масиву, бінарного значення та PHP-констант).

Однак існує інший тип параметра, пов'язаний з сервісами. У конфігурації YAML, будь-який рядок, що починається з @, вважається ID сервісу, а не звичайним рядком. У конфігурації XML використовуйте тип type="service" для параметра, а у конфігурації PHP - функцію service():

1
2
3
4
5
6
7
8
9
10
11
# config/services.yaml
services:
    App\Service\MessageGenerator:
        arguments:
            # це не рядок, а посилання на сервіс під назвою 'logger'
            - '@logger'

            # якщо значення аргументу рядку починається з '@', вам потрібно екранувати його,
            # додавши ще один '@', щоб Symfony не вважала її сервісом
            # наступний приклад буде проаналізований як рядок '@securepassword'
            # - '@@securepassword'

Робота з параметрами контейнера пряма при використанні методів доступу контейнера для параметрів:

1
2
3
4
5
6
7
8
// перевіряє, чи визначено параметр (імена параметрів чутливі до регістру)
$container->hasParameter('mailer.transport');

// отримує значення параметра
$container->getParameter('mailer.transport');

// додає новий параметр
$container->setParameter('mailer.transport', 'sendmail');

Caution

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

Note

Ви можете встановити параметр лише до компіляції контейнера, а не під час його роботи. Щоб дізнатися більше про компіляцію контейнера, див. Компіляція контейнера.

Вибір конкретного сервісу

Сервіс MessageGenerator, створений раніше, вимагає аргументу LoggerInterface:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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 (дізнайтеся більше, чому в ). Але ви можете контролювати це і передати інший логер:

1
2
3
4
5
6
7
8
9
10
11
12
# app/config/services.yml
services:
    # ... той же код, що і раніше

    # чітко сконфігуруйте сервіс
    AppBundle\Service\MessageGenerator:
        arguments:
            # символ '@' важливий: це те, що каже контейнеру, що ви
            # хочете передати *сервіс*, чий id - 'monolog.logger.request',
            # а не просто *рядок* 'monolog.logger.request'

            $logger: '@monolog.logger.request'

Це повідомляє контейнеру, що аргумент $logger для __construct має використовувати сервіс, id якого monolog.logger.request.

Щоб отримати список можливих сервісів логерів, які можна використовувати з автомонтуванням, виконайте:

1
$ php bin/console debug:autowiring logger

Щоб побачити повний список усіх можливих сервісів у контейнері, виконайте:

1
$ php bin/console debug:container

Видалення сервісів

За потреби сервіс можна вилучити з сервіс-контейнера. Це може бути корисно наприклад, щоб зробити сервіс недоступним у деякому :ref:`середовищі конфігурації <configuration-environments>' (наприклад, у середовищі test):

1
2
3
4
5
6
7
8
9
10
// config/services_test.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use App\RemovedService;

return function(ContainerConfigurator $containerConfigurator) {
    $services = $containerConfigurator->services();

    $services->remove(RemovedService::class);
};

Тепер контейнер не міститиме App\RemovedService в середовищі test.

Впровадження замикання в якості аргументу

Можливо впровадити викличне в якості аргументу сервісу. Давайте додамо аргумент до нашого конструктора MessageGenerator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Service/MessageGenerator.php
namespace App\Service;

use Psr\Log\LoggerInterface;

class MessageGenerator
{
    private string $messageHash;

    public function __construct(
        private LoggerInterface $logger,
        callable $generateMessageHash,
    ) {
        $this->messageHash = $generateMessageHash();
    }
    // ...
}

Далі ми додамо новий викличний сервіс, щоб згенерувати хеш повідомлення:

1
2
3
4
5
6
7
8
9
10
// src/Hash/MessageHashGenerator.php
namespace App\Hash;

class MessageHashGenerator
{
    public function __invoke(): string
    {
        // Обчислити та повернути хеш повідомлення
    }
}

Наша конфігурація виглядає так:

1
2
3
4
5
6
7
8
9
# config/services.yaml
services:
    # ... той же код, що і раніше

    # чітко сконфігурувати сервіс
    App\Service\MessageGenerator:
        arguments:
            $logger: '@monolog.logger.request'
            $generateMessageHash: !closure '@App\Hash\MessageHashGenerator'

See also

Замикання можна впровадити за допомогою автомонтування та його спеціальних атрибутів.

Зв'язування аргументів за іменем або типом

Ви також можете використати ключове слово bind, щоб зв'язати конкретні аргументи за іменем або типом:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# config/services.yaml
services:
    _defaults:
        bind:
            # передайте це значення будь-якому аргументу $adminEmail для будь-якого сервісу,
            # визначеного в цьому файлі (включно з аргументами контролера)
            $adminEmail: 'manager@example.com'

            # передайте цей сервіс будь-якому аргументу $requestLogger для будь-якого сервісу,
            # визначеного в цьому файлі
            $requestLogger: '@monolog.logger.request'

            # передайте цей сервіс будь-якій підказці LoggerInterface для будь-якого сервісу,
            # визначеного в цьому файлі
            Psr\Log\LoggerInterface: '@monolog.logger.request'

            # за бажанням ви можете винзачити ім'я та тип аргументу для співставлення
            string $adminEmail: 'manager@example.com'
            Psr\Log\LoggerInterface $requestLogger: '@monolog.logger.request'
            iterable $rules: !tagged_iterator app.foo.rule

    # ...

Помістивши ключ bind під _defaults, ви можете вказати значення будь-якого аргументу для будь-якого сервісу, визначеного у цьому файлі! Ви можете зв'язати аргументи за іменем (наприклад, $adminEmail), за типом (наприклад, Psr\Log\LoggerInterface) або і за тим, і за тим (наприклад, Psr\Log\LoggerInterface $requestLogger).

Конфігурація bind також може бути застосована до конкретних сервісів або при завантаженні багатьох сервісів одночасно (тобто ).

Абстрактні аргументи сервісу

Іноді значення деяких аргументів сервісу не можуть бути визначені у файлах конфігурації, оскільки вони обчислюються під час виконання за допомогою розширення пакету </bundles/extension>`.

У цих випадках ви можете використовувати тип аргументу abstract для визначення принаймні назви аргументу та короткого опису його призначення:

1
2
3
4
5
6
7
8
9
# config/services.yaml
services:
    # ...

    App\Service\MyService:
        arguments:
            $rootNamespace: !abstract 'should be defined by Pass'

    # ...

Якщо ви не заміните значення абстрактного аргументу під час виконання, буде викликано RuntimeException з повідомленням на кшталт Аргумент «$rootNamespace» сервісу «App\Service\MyService» є абстрактним: має бути визначений за допомогою Pass.

Опція автомонтування

Вище, файд services.yml має autowire: true у розділі _defaults, так що це відноситься до усіх сервісів, визначених у цьому файлі. З цим налаштуванням ви можете додавати підказки до аругментів у методі __construct() ваших сервісів, і контейнер автоматично передаватиме вам правильні аргументи. Весь цей запис було написано без автомонтування.

Щоб дізнатися більше про автомонтування, див. Автоматичне визначення залежностей сервісу (автомонтування).

Опція автоконфігурації

Вище, файд services.yml має autowire: true у розділі _defaults, так що це відноситься до усіх сервісів, визначених у цьому файлі. З цим налаштуванням контейнер автоматично застосовуватиме визначену конфігурацію до ваших сервісів, засновуючись на класі вашого сервісу. Це частіше за все використовується для автотегування ваших сервісів.

Наприклад, щоб створити розширення Twig, вам потрібно створити клас, зареєструвати його в якості сервісу, та тегувати його twig.extension:

Однак з autoconfigure: true, вам не потрібний тег. Насправді, якщо ви використовуєте конфігурацію Symfony Standard Edition services.yml , вам не потрібно нічого робити: сервіс буде завантажено автоматично. Потім, autoconfigure додасть тег twig.extension за вас, так як ваш клас реалізує Twig_ExtensionInterface. І завдякиautowire ви навіть можете додати аргументи-конструктори без будь-якої конфігурції.

Автоконфігурація також працює з атрибутами. Деякі атрибути, такі як AsMessageHandler, AsEventListener та AsCommand зареєстровано для автоконфігурації. До будь-якого класу, що використовує ці атрибути, буде застосовано теги.

Перевірка дотримання стандартів кодування визначень сервісу

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

1
$ php bin/console lint:container

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

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

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

Якщо вам потрібно отримувати сервіси ліниво, замість використання публічних сервісів, вам варто розглянути використання локатора сервісів .

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

1
2
3
4
5
6
7
# config/services.yaml
services:
    # ... той же код, що і раніше

    # чітко сконфігуруйте сервіс
    App\Service\PublicService:
        public: true

Одночасний імпорт багатьох сервісів за допомогою джерела

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

1
2
3
4
5
6
7
8
9
# config/services.yaml
services:
    # ... те ж саме, що і раніше

    # робить класи в src/ доступними для використання в якості сервісів
    # створює по сервісу в класі, чий id є повним іменем класу
    App\:
        resource: '../src/*'
        exclude: '../src/{DependencyInjection,Entity,Tests,Kernel.php}'

Tip

Значеннями опцій resource та exclude можуть бути будь-який валідний glob pattern. Якщо ви хочете виключити лише декілька сервісів, ви можете використати атрибут Exclude безпосередньо у вашому класі, щоб виключити його.

Це може бути використано для того, щоб швидко зробити багато класів доступними в якості сервісів, та застосувати якусь конфігурацію за замовчуванням. Id кожного сервісу - це його повністю кваліфіковане ім'я класу. Ви також можете переписати будь-який сервіс, який було імпортовано, використовуючи його id (ім'я класу) нижче (наприклад, див. ). Якщо ви перепишете сервіс, жодні з опцій (наприклад, public) не будуть унаслілудвані з імпорту (але переписаний сервіс наслідує з _defaults).

Ви також можете exclude (виключити) певні шляхи. Це не обов'язково, але трохи покращить продуктивніть середовища dev: виключені шляхи не відстежуються, так що їх зміна не призведе до перебудови контейнера.

Note

Чекайте, це що, означає, що кожний клас у src/AppBundle зареєстрований як сервіс? Навіть модель або класи сутностей? Насправді, ні. Якщо у вас є public: false у вашому ключі _defaults (або ви можете додати його у конкретному імпорті), всі імпортовані сервіси є приватними. Завдяки цьому, всі класи в src/AppBundle, які не чітко використовуються як сервіси, автоматично видаляються з фінального контейнера. У дійсності, імпорт просто означає, що всі класи доступні для використання в якості сервісів, без необхіжності ручноїконфігурації.

Визначення багатьох сервісів, що використовують один простір імен

Якщо ви визначаєте сервіси, використовуючи формат конфігурації YAML, простір імен PHP використовується в якості ключа кожної конфігурації, тому ви не можете визначити різні конфігурації сервісів для класів під одним простором імен:

1
2
3
4
5
# config/services.yaml
services:
    App\Domain\:
        resource: '../src/Domain/*'
        # ...

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

1
2
3
4
5
6
7
8
9
10
11
# config/services.yaml
services:
    command_handlers:
        namespace: App\Domain\
        resource: '../src/Domain/*/CommandHandler'
        tags: [command_handler]

    event_subscribers:
        namespace: App\Domain\
        resource: '../src/Domain/*/EventSubscriber'
        tags: [event_subscriber]

Явна конфігурація сервісів та аргументів

автомонтування <services-autowire>` є необов'язковим. І навіть якщо ви використовуєте їх, можуть бути деякі випадки, коли ви хочете підключити сервіс вручну. Наприклад, припустимо, що ви хочете зареєструвати 2 сервіси для класу SiteUpdateManager - кожен з різними email-адресами адміністратора. У цьому випадку кожен з них повинен мати унікальний ідентифікатор сервісу:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# config/services.yaml
services:
    # ...

    # це id сервісу
    site_update_manager.superadmin:
        class: App\Service\SiteUpdateManager
        # ви все ще МОЖЕТЕ використати автомонтування: ми просто хочемо показати, як це виглядає без нього
        autowire: false
        # вручну підключіть всі аргументи
        arguments:
            - '@App\Service\MessageGenerator'
            - '@mailer'
            - 'superadmin@example.com'

    site_update_manager.normal_users:
        class: App\Service\SiteUpdateManager
        autowire: false
        arguments:
            - '@App\Service\MessageGenerator'
            - '@mailer'
            - 'contact@example.com'

    # Створіть псевдонім, щоб за замовчуванням, якщо ви використовуєте підказку SiteUpdateManager,
    # використовувався site_update_manager.superadmin
    App\Service\SiteUpdateManager: '@site_update_manager.superadmin'

У цьому випадку, зареєстровані два сервіси: site_update_manager.superadmin і site_update_manager.normal_users. Завдяки псевдоніми, якщо ви додасте підказку SiteUpdateManager, буде передано перше (site_update_manager.superadmin).

Якщо ви хочете передати друге, то вам потрібно вручну підключити сервіс .

Caution

Якщо ви не створите псевдонім і завантажуєте всі свої сервіси з src/AppBundle , тоді три сервіси будуть створені (автоматичний сервіс + два ваших), та автоматично завантажений сервіс буде передано - за замовчуванням - коли ви додасте підказку SiteUpdateManager. Тому створення псевдоніму - гарна ідея.

При використанні замикань PHP для конфігурації ваших сервісів можна автоматично вставити поточне значення середовища, додавши аргумент рядка з ім'ям $env до замикання:

1
2
3
4
5
6
7
// config/packages/my_config.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return function(ContainerConfigurator $containerConfigurator, string $env): void {
    // `$env` заповнюється автоматично, тому ви можете сконфігурувати ваші
    // сервіси, залежно від того, в якому ви середовищі
};

Генерування адаптерів для функціональних інтерфейсів

Функціональні інтерфейси - це інтерфейси з одним методом. Концептуально вони дуже схожі на замикання, за винятком того, що їхній єдиний метод має ім'я. Крім того, їх можна використовувати як підказки типу у вашому коді.

Атрибут AutowireCallable можна використовувати для генерування адаптера для функціонального інтерфейсу. Припустимо, у вас є наступний функціональний інтерфейс:

1
2
3
4
5
6
7
// src/Service/MessageFormatterInterface.php
namespace App\Service;

interface MessageFormatterInterface
{
    public function format(string $message, array $parameters): string;
}

У вас також є сервіс, який визначає багато методів і один з них такий самий метод format() попереднього інтерфейсу:

1
2
3
4
5
6
7
8
9
10
11
12
// src/Service/MessageFormatterInterface.php
namespace App\Service;

class MessageUtils
{
    // інші методи...

    public function format(string $message, array $parameters): string
    {
        // ...
    }
}

Завдяки атрибуту #[AutowireCallable] тепер ви можете вставити цей сервіс MessageUtils як реалізацію функціонального інтерфейсу:

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

use App\Service\MessageFormatterInterface;
use App\Service\MessageUtils;
use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;

class Mailer
{
    public function __construct(
        #[AutowireCallable(service: MessageUtils::class, method: 'format')]
        private MessageFormatterInterface $formatter
    ) {
    }

    public function sendMail(string $message, array $parameters): string
    {
        $formattedMessage = $this->formatter->format($message, $parameters);

        // ...
    }
}

Замість використання атрибуту #[AutowireCallable] ви також можете згенерувати адаптер для функціонального інтерфейсу через конфігурацію:

1
2
3
4
5
6
7
8
# config/services.yaml
services:

    # ...

    app.message_formatter:
        class: App\Service\MessageFormatterInterface
        from_callable: [!service {class: 'App\Service\MessageUtils'}, 'format']

При цьому Symfony створить клас (який також називається адаптером), що реалізує MessageFormatterInterface, який буде перенаправляти виклики MessageFormatterInterface::format() до методу вашого базового сервісу MessageUtils::format(), з усіма його аргументами.

Learn more