Дата обновления перевода 2021-06-09

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

Screencast

Вы предпочитаете видео-уроки? Посмотрите Symfony Fundamentals screencast series.

Ваше приложение полно полезных объектов: объект “Mailer” может помочь вам отправлять email’ы, в то время, как другой объект, может помочь вам сохранять данные базу данных. Почти все, что “делает” ваше приложение, на самом деле выполняется одним из этих объектов. И каждый раз, когда вы устанавливаете новый пакет, вы получаете доступ к новым объектам!

В Symfony, эти полезные объекты называются сервисами и каждый сервис живет внутри очень особенного объекта под названием сервис-контейнер. Контейнер позволяет вам централизовать то, как создаются объекты. Он делает вашу жизнь легче, пропагандрует сильную архитектуру, а также он очень быстрый!

Извлечение и использование сервисов

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

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

use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response;

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;

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

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

Когда вы запросите сервис MessageGenerator, контейнер создаст новый объект MessageGenerator и вернет его (смотрите объяснение ниже). Но если вы никогда не запросите этот сервис, он никогда не будет создан: экономия памяти и скорости. В качестве бонуса, сервис MessageGenerator создается только единожды: каждый раз, когда вы запрашиваете его, вам возващается один и тот же экземпляр.

Внедрение сервисов/конфигурации в сервис

Что, если вам нужно получить доступ к сервису logger из MessageGenerator? Не проблема! Cоздайте метод __construct() с аргументом $logger, который имеет типизирование LoggerInterface. Установите это d новом свойстве $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. Откуда он это знает? Autowiring. Ключом является титпизирование 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” метода method “__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 (узнайте больше, почему в Использование псведонимов для включения автомонтирования). Но,вы можете контролировать это и передать другой 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 также может быть применена к конкретным сервисам или при загрузке множества сервисов одновременно (т.e. Importing Many Services at once with resource).

Опция автомонтирования

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

Чтобы узнать больше об автомонтировании, см Defining Services Dependencies Automatically (Autowiring).

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

Выше, файл services.yml имеет autoconfigure: 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.