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

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

Screencast

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

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

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

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

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

// src/Controller/ProductController.php
namespace App\Controller;

use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class ProductController
{
    /**
     * @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
$ 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)

  [...]

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

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

Tip

Насправді існує багато інших сервісів у контейнері, і кожний сервіс має у ньому унікальний id, наприклад, request_stack або router.default. Щоб побачити повний список, ви можете виконати php bin/console debug:container. Але у більшості випадків вам не треба буде про це турбуватися. Див. Choose a Specific Service. Див. How to Debug the Service Container & List Services.

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

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

// 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];
    }
}

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

// src/Controller/ProductController.php
use App\Service\MessageGenerator;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route("/products/new")
 */
public function new(MessageGenerator $messageGenerator): Response
{
    // завдяки підказці контейнер викличе
    // новий і передасть його вам!
    // ...

    $message = $messageGenerator->getHappyMessage();
    $this->addFlash('success', $message);
    // ...
}

Коли ви запитаєте сервіс MessageGenerator, контейнер створить новий об’єкт MessageGenerator та поверне його (дивіться пояснення нижче). Але якщо ви ніколи не запитаєте цей сервіс, він ніколи не буде створений: економія пам’яті та швидкості. В якості бонусу, сервіс MessageGenerator створюється лише один раз: кожний раз, коли ви запитуєте його, вам повертається один і той самий екземпляр.

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

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

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

use Psr\Log\LoggerInterface;

class MessageGenerator
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $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 адміністратору кожний раз, коли сайт оновлюється. Щоб зробити це, створіть новий клас:

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

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

class SiteUpdateManager
{
    private $messageGenerator;
    private $mailer;

    public function __construct(MessageGenerator $messageGenerator, MailerInterface $mailer)
    {
        $this->messageGenerator = $messageGenerator;
        $this->mailer = $mailer;
    }

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

        $email = (new Email())
            ->from('[email protected]')
            ->to('[email protected]')
            ->subject('Site update just happened!')
            ->text('Someone just updated the site. We told them: '.$happyMessage);

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

        // ...

        return true;
    }
}

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

// src/Controller/SiteController.php
namespace App\Controller;

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

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

        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 $adminEmail;

-    public function __construct(MessageGenerator $messageGenerator, MailerInterface $mailer)
+    public function __construct(MessageGenerator $messageGenerator, MailerInterface $mailer, string $adminEmail)
      {
          // ...
+        $this->adminEmail = $adminEmail;
      }

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

          $email = (new Email())
              // ...
-            ->to('[email protected]')
+            ->to($this->adminEmail)
              // ...
          ;
          // ...
      }
  }

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

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

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

  • YAML
     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: '[email protected]'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <!-- config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <!-- ...  те ж саме, що і раніше -->
    
            <!-- Те ж саме, що і раніше -->
    
            <prototype namespace="App\"
                resource="../src/*"
                exclude="../src/{DependencyInjection,Entity,Tests,Kernel.php}"
            />
    
            <!-- Чітко сконфігуруйте сервіс -->
            <service id="App\Service\SiteUpdateManager">
                <argument key="$adminEmail">[email protected]</argument>
            </service>
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    use App\Service\SiteUpdateManager;
    
    return function(ContainerConfigurator $configurator) {
        // ...
    
        // те ж саме, що і раніше
        $services->load('App\\', '../src/*')
            ->exclude('../src/{DependencyInjection,Entity,Tests,Kernel.php}');
    
        $services->set(SiteUpdateManager::class)
            ->arg('$adminEmail', '[email protected]')
        ;
    };
    

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

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

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

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

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

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    # config/services.yaml
    services:
        App\Service\MessageGenerator:
            arguments:
                # це не рядок, а посилання на сервіс під назвою 'logger'
                - '@logger'
    
                # якщо значення аргументу рядку починається з '@', вам потрібно екранувати його,
                # додавши ще один '@', щоб Symfony не вважала її сервісом
                # наступний приклад буде проаналізований як рядок '@securepassword'
                # - '@@securepassword'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    <!-- config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="App\Service\MessageGenerator">
                <argument type="service" id="logger"/>
            </service>
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    use App\Service\MessageGenerator;
    
    return function(ContainerConfigurator $configurator) {
        $services = $configurator->services();
    
        $services->set(MessageGenerator::class)
            // У версіях до Symfony 5.1, функція service() називалася ref()
            ->args([service('logger')])
        ;
    };
    

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

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

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

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

Caution

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

Note

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

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

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

// src/AppBundle/Service/MessageGenerator.php
// ...

use Psr\Log\LoggerInterface;

class MessageGenerator
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }
    // ...
}

Однак, існує багато сервісів у контейнері, які реалізують LoggerInterface, наприклад, logger, monolog.logger.request, monolog.logger.php, та ін. Як контейнер знає, які використовувати?

У таких ситуаціях, контейнер зазвичай сконфігурований так, щоб автоматично обирати один з сервісів - у цьому випадку logger (дізнайтеся більше, чому в Использование псведонимов для включения автомонтирования). Але ви можете контролювати це і передати інший логер:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    # app/config/services.yml
    services:
        # ... той же код, що і раніше
    
        # чітко сконфігуруйте сервіс
        AppBundle\Service\MessageGenerator:
            arguments:
                $logger: '@monolog.logger.request'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    <!-- app/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <!-- ... той же код, що і раніше -->
    
            <!-- Чітко сконфігуруйте сервіс -->
            <service id="AppBundle\Service\MessageGenerator">
                <argument key="$logger" type="service" id="monolog.logger.request" />
            </service>
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    use App\Service\MessageGenerator;
    
    return function(ContainerConfigurator $configurator) {
        // ... той же код, що і раніше
    
        // чітко сконфігуруйте сервіс
        $services->set(SiteUpdateManager::class)
            ->arg('$logger', service('monolog.logger.request'))
        ;
    };
    

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

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

1
$ php bin/console debug:container

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

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

  • YAML
     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: '[email protected]'
    
                # передайте цей сервіс будь-якому аргументу $requestLogger для будь-якого сервісу,
                # визначеного в цьому файлі
                $requestLogger: '@monolog.logger.request'
    
                # передайте цей сервіс будь-якій підказці LoggerInterface для будь-якого сервісу,
                # визначеного в цьому файлі
                Psr\Log\LoggerInterface: '@monolog.logger.request'
    
                # за бажанням ви можете винзачити ім'я та тип аргументу для співставлення
                string $adminEmail: '[email protected]'
                Psr\Log\LoggerInterface $requestLogger: '@monolog.logger.request'
                iterable $rules: !tagged_iterator app.foo.rule
    
        # ...
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    <!-- config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <defaults autowire="true" autoconfigure="true" public="false">
                <bind key="$adminEmail">[email protected]</bind>
                <bind key="$requestLogger"
                    type="service"
                    id="monolog.logger.request"
                />
                <bind key="Psr\Log\LoggerInterface"
                    type="service"
                    id="monolog.logger.request"
                />
    
                <!-- за бажанням ви можете винзачити ім'я та тип аргументу для співставлення -->
                <bind key="string $adminEmail">[email protected]</bind>
                <bind key="Psr\Log\LoggerInterface $requestLogger"
                    type="service"
                    id="monolog.logger.request"
                />
                <bind key="iterable $rules"
                    type="tagged_iterator"
                    tag="app.foo.rule"
                />
            </defaults>
    
            <!-- ... -->
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    use App\Controller\LuckyController;
    use Psr\Log\LoggerInterface;
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    
    return function(ContainerConfigurator $configurator) {
        $services = $configurator->services()
            ->defaults()
                // передайте це значення будь-якому аргументу $adminEmail для будь-якого сервісу,
                // визначеного в цьому файлі (включно з аргументами контролера)
                ->bind('$adminEmail', '[email protected]')
    
                // передайте цей сервіс будь-якому аргументу $requestLogger для будь-якого сервісу,
                // визначеного в цьому файлі
                ->bind('$requestLogger', service('monolog.logger.request'))
    
                // передайте цей сервіс будь-якій підказці LoggerInterface для будь-якого сервісу,
                // визначеного в цьому файлі
                ->bind(LoggerInterface::class, service('monolog.logger.request'))
    
                // за бажанням ви можете винзачити ім'я та тип аргументу для співставлення
                ->bind('string $adminEmail', '[email protected]')
                ->bind(LoggerInterface::class.' $requestLogger', service('monolog.logger.request'))
                ->bind('iterable $rules', tagged_iterator('app.foo.rule'))
        ;
    
        // ...
    };
    

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

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

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

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

Щоб дізнатися більше про автомонтування, див. Defining Services Dependencies Automatically (Autowiring).

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

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

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

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

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

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

1
$ php bin/console lint:container

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

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

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

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

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

  • YAML
    1
    2
    3
    4
    5
    6
    7
    # config/services.yaml
    services:
        # ... той же код, що і раніше
    
        # чітко сконфігуруйте сервіс
        App\Service\PublicService:
            public: true
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    <!-- config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <!-- ... той же код, що і раніше -->
    
            <!-- Чітко сконфігуруйте сервіс -->
            <service id="App\Service\PublicService" public="true"></service>
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    use App\Service\PublicService;
    
    return function(ContainerConfigurator $configurator) {
        // ... той же код, що і раніше
    
        // чітко сконфігуруйте сервіс
        $services->set(Service\PublicService::class)
            ->public()
        ;
    };
    

Deprecated since version 5.1: Починаючи з Symfony 5.1, більше неможливо автомонтувати сервіс-контейнер з допомогою підказки Psr\Container\ContainerInterface.

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

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

  • YAML
    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}'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    <!-- config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <!-- ... те ж саме, що і раніше -->
    
            <prototype namespace="App\" resource="../src/*" exclude="../src/{DependencyInjection,Entity,Tests,Kernel.php}"/>
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    return function(ContainerConfigurator $configurator) {
        // ...
    
        // робить класи в src/ доступними для використання в якості сервісів
        // створює по сервісу в класі, чий id є повним іменем класу
        $services->load('App\\', '../src/*')
            ->exclude('../src/{DependencyInjection,Entity,Tests,Kernel.php}');
    };
    

Tip

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

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

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

Note

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

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

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

  • YAML
    1
    2
    3
    4
    5
    # config/services.yaml
    services:
        App\Domain\:
            resource: '../src/Domain/*'
            # ...
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    <!-- config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <prototype namespace="App\Domain"
                resource="../src/App/Domain/*"/>
    
            <!-- ... -->
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    // config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $defaults = new Definition();
    
    // $this - это ссылка на текущий загрузчик
    $this->registerClasses(
        $defaults,
        'App\\Domain\\',
        '../src/App/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]

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

До появи Symfony 3.3, всі сервіси та (зазвичай) аргументи, були чітко сконфігуровані: було неможливо завантажити сервіси автоматично, а автомонтування було набагато менше поширене.

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

  • YAML
     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'
                - '[email protected]'
    
        site_update_manager.normal_users:
            class: App\Service\SiteUpdateManager
            autowire: false
            arguments:
                - '@App\Service\MessageGenerator'
                - '@mailer'
                - '[email protected]'
    
        # Створіть псевдонім, щоб за замовчуванням, якщо ви використовуєте підказку SiteUpdateManager,
        # використовувався site_update_manager.superadmin
        App\Service\SiteUpdateManager: '@site_update_manager.superadmin'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <!-- config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <!-- ... -->
    
            <service id="site_update_manager.superadmin" class="App\Service\SiteUpdateManager" autowire="false">
                <argument type="service" id="App\Service\MessageGenerator"/>
                <argument type="service" id="mailer"/>
                <argument>[email protected]</argument>
            </service>
    
            <service id="site_update_manager.normal_users" class="App\Service\SiteUpdateManager" autowire="false">
                <argument type="service" id="App\Service\MessageGenerator"/>
                <argument type="service" id="mailer"/>
                <argument>[email protected]</argument>
            </service>
    
            <service id="App\Service\SiteUpdateManager" alias="site_update_manager.superadmin"/>
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    use App\Service\MessageGenerator;
    use App\Service\SiteUpdateManager;
    
    return function(ContainerConfigurator $configurator) {
        // ...
    
        // site_update_manager.superadmin - це id сервісу
        $services->set('site_update_manager.superadmin', SiteUpdateManager::class)
            // ви все ще МОЖЕТЕ використати автомонтування: ми просто хочемо показати, як це виглядає без нього
            ->autowire(false)
            // вручну підключіть всі аргументи
            ->args([
               service(MessageGenerator::class),
               service('mailer'),
               '[email protected]',
            ]);
    
        $services->set('site_update_manager.normal_users', SiteUpdateManager::class)
            ->autowire(false)
            ->args([
                service(MessageGenerator::class),
                service('mailer'),
                '[email protected]',
            ]);
    
        // Створіть псевдонім, щоб за замовчуванням, якщо ви використовуєте підказку SiteUpdateManager,
        // використовувався site_update_manager.superadmin
        $services->alias(SiteUpdateManager::class, 'site_update_manager.superadmin');
    };
    

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

Caution

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

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

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.