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

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

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

Получение и использование Сервисов

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

// src/Controller/ProductController.php // ... use Psr\Log\LoggerInterface; /** * @Route("/products") */ public function list(LoggerInterface $logger) { $logger->info('Look! I just used a service'); // ... }

Какие еще Сервисы доступны? Чтобы узнать достаточно выполнить в консоли:

$ 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)

  Interface for the session.
  Symfony\Component\HttpFoundation\Session\SessionInterface (session)

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

  [...]

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

В документации по Symfony вы узнаете, как использовать множество различных Сервисов, которые находятся в контейнере.

Создание / настройка Сервисов в контейнере

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

// src/Service/MessageGenerator.php namespace App\Service; class MessageGenerator { public function getHappyMessage() { $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]; } }

Поздравляем! Вы только что создали свой первый класс-сервис! Вы можете использовать его непосредственно внутри вашего контроллера:

use App\Service\MessageGenerator; public function new(MessageGenerator $messageGenerator) { // благодаря тайп-хинту контейнер создаст экземпляр класса // new MessageGenerator и передаст его вам! // ... $message = $messageGenerator->getHappyMessage(); $this->addFlash('success', $message); // ... }

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

Автоматическая загрузка Сервисов в конфиг-файле services.yaml

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

# config/services.yaml services: # default configuration for services in *this* file _defaults: autowire: true # Automatically injects dependencies in your services. autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. public: false # Allows optimizing the container by removing unused services; this also means # fetching services directly from the container via $container->get() won't work. # The best practice is to be explicit about your dependencies anyway. # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name App\: resource: '../src/*' exclude: '../src/{Entity,Migrations,Tests,Kernel.php}' # ...

Благодаря этой конфигурации вы можете автоматически использовать любые классы из каталога src/ в качестве Cлужбы без необходимости настраивать ее вручную. Если вы предпочитаете подключать свой сервис вручную, это разумеется также возможно.


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

Что делать, если вам нужен доступ к Сервису logger изнутри MessageGenerator? Нет проблем! Создайте __construct() метод с
аргументом $logger , который имеет тайп-хинт LoggerInterface. В приватное свойство $this->logger в качестве значения передайте только что созданный объект и используйте его позже в других методах класса:

// src/Service/MessageGenerator.php // ... use Psr\Log\LoggerInterface; class MessageGenerator { private $logger; public function __construct(LoggerInterface $logger) { $this->logger = $logger; } public function getHappyMessage() { $this->logger->info('About to find a happy message!'); // ... } }

Контейнер автоматически узнает, что передал Службу logger при создании экземпляра MessageGenerator. Как он знает, как это сделать? Autowiring. Тайп-хинт LoggerInterface в вашем методе __construct() и настройка конфигурации autowire: true в файле services.yaml. Когда вы в аргументе метода используете подсказку в виде указания тайп-хинта, то контейнер автоматически находит соответствующий сервис. Если это будет невозможно сделать, то вы увидите исключение Exception с полезным ясным описанием.

Кстати, этот подход добавления зависимостей в ваш метод __construct() называется внедрением зависимостей . Пожалуй это страшный термин для такой простой концепции.

Как вы должны знать, что нужно использовать LoggerInterface для тайп-хинта? Вы можете либо прочитать документацию для любой функции, которую вы используете, либо получить список автоматически настраиваемых подсказок для тайп-хинта, выполнив:

$ php bin/console debug:autowiring

  # this is just a *small* sample of the output...

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

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

  Interface for the session.
  Symfony\Component\HttpFoundation\Session\SessionInterface (session)

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

  [...]

Обработка нескольких Сервисов

Предположим, вы хотите отправлять электронное письмо администратору сайта каждый раз, когда производится обновление сайта. Для этого вы создаете новый класс:

// src/Updates/SiteUpdateManager.php namespace App\Updates; use App\Service\MessageGenerator; class SiteUpdateManager { private $messageGenerator; private $mailer; public function __construct(MessageGenerator $messageGenerator, \Swift_Mailer $mailer) { $this->messageGenerator = $messageGenerator; $this->mailer = $mailer; } public function notifyOfSiteUpdate() { $happyMessage = $this->messageGenerator->getHappyMessage(); $message = (new \Swift_Message('Site update just happened!')) ->setFrom('admin@example.com') ->setTo('manager@example.com') ->addPart( 'Someone just updated the site. We told them: '.$happyMessage ); return $this->mailer->send($message) > 0; } }

Наш новый класс SiteUpdateManager для корректной работы нуждается в двух Сервисах: MessageGenerator и в Swift_Mailer. Это не проблема! Фактически, наш новый Сервис уже готов к использованию. Например, в контроллере вы можете создать новый объект класса SiteUpdateManager и использовать его:

// src/Controller/SiteController.php // ... use App\Updates\SiteUpdateManager; public function new(SiteUpdateManager $siteUpdateManager) { // ... if ($siteUpdateManager->notifyOfSiteUpdate()) { $this->addFlash('success', 'Notification mail was sent successfully.'); } // ... }

Благодаря autowiring и вашей подсказке тайп-хинт в методе __construct(), контейнер создает объект класса SiteUpdateManager и передает ему правильный аргумент. В большинстве случаев это работает отлично.

Подключение аргументов вручную

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

// src/Updates/SiteUpdateManager.php // ... class SiteUpdateManager { // ... private $adminEmail; public function __construct(MessageGenerator $messageGenerator, \Swift_Mailer $mailer, $adminEmail) { // ... $this->adminEmail = $adminEmail; } public function notifyOfSiteUpdate() { // ... $message = \Swift_Message::newInstance() // ... ->setTo($this->adminEmail) // ... ; // ... } }

Если вы сделаете эти изменения выше и попробуете запустить скрипт, то вы увидите ошибку:

Cannot autowire service «AppUpdatesSiteUpdateManager»: argument «$adminEmail» of method «__construct()» must have a type-hint or be given a value explicitly.

И в этой ошибке есть смысл! Контейнер не знает, какое значение вы хотите передать здесь в переменную $adminemail. Нет проблем! В вашем файле конфигурации вы можете явно установить значение для этого аргумента:

# config/services.yaml services: # ... # same as before App\: resource: '../src/*' exclude: '../src/{Entity,Migrations,Tests}' # explicitly configure the service App\Updates\SiteUpdateManager: arguments: $adminEmail: 'manager@example.com'

Благодаря этому контейнер предаст значение manager@example.com в переменную $adminEmail в аргументе метода __construct при создании Сервиса SiteUpdateManager. При этом, другие аргументы по-прежнему будут автоматически подключены по тайп-хинту.

При этом вы можете не беспокоиться о корректности работы приложения! Потому что, если вы переименуете аргумент $adminEmail во что-то другое (или например опечатаетесь), например,новое название аргумента станет $mainEmail — то при перезагрузке следующей страницы вы получите явное исключение Exception с описанием проблемы (даже если эта страница не использует этот некорректный Сервис).

Параметры Сервисов

Помимо хранения объектов Сервисов, контейнер также содержит конфигурацию с именем parameters. Чтобы создать параметр, в конфигурационном файле services.yaml добавьте ключ parameters и укажите в аргументе его использование с помощью синтаксиса %parameter_name%:

# config/services.yaml parameters: admin_email: manager@example.com services: # ... App\Updates\SiteUpdateManager: arguments: $adminEmail: '%admin_email%'

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

Затем вы можете получать параметры в Сервисах:

class SiteUpdateManager { // ... private $adminEmail; // в $adminEmail будет передано значение 'manager@example.com' public function __construct($adminEmail) { $this->adminEmail = $adminEmail; } }

Вы также можете получить параметры непосредственно из контейнера:

public function new() { // ... // this shortcut ONLY works if you extend the base AbstractController $adminEmail = $this->getParameter('admin_email'); // this is the equivalent code of the previous shortcut: // $adminEmail = $this->container->get('parameter_bag')->get('admin_email'); }

Выбор конкретного Сервиса

Сервис MessageGenerator, созданный ранее, требует для работы аргумент с тайп-хинтом LoggerInterface:

// src/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 в данном случае. Но вы можете контролировать это и передать другой логер:

# config/services.yaml services: # ... same code as before # explicitly configure the service App\Service\MessageGenerator: arguments: # the '@' symbol is important: that's what tells the container # you want to pass the *service* whose id is 'monolog.logger.request', # and not just the *string* 'monolog.logger.request' $logger: '@monolog.logger.request'

Это говорит контейнеру, что аргумент $logger в методе __construct() должен использовать сервис с идентификатором monolog.logger.request.

Для получения полного списка всех возможных служб в контейнере запустите:

$ php bin/console debug:container

Привязка аргументов по имени или типу

Вы также можете использовать ключевое слово bind для привязки определенных аргументов по имени или типу:

# 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' # ...

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

Конфигурация с помощью bind также может быть применена к конкретным Сервисам или при загрузке множества Сервисов одновременно (то есть импорте множества Сервисов одновременно с ресурсом).

Получение параметров контейнера как Сервис

Если какой-либо службе или контроллеру требуется много параметров контейнера, существует более простая альтернатива связать их все с помощью опции services._defaults.bind. Для этого указываеся Тайп-хинт любого из аргументов конструктора с помощью ParameterBagInterface или нового ContainerBagInterface и Сервис получит все параметры контейнера в объекте ParameterBag:

// src/Service/MessageGenerator.php // ... use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; class MessageGenerator { private $params; public function __construct(ParameterBagInterface $params) { $this->params = $params; } public function someMethod() { // Можно получить любой параметр из $ this-> params, который хранит все параметры контейнера $sender = $this->params->get('mailer_sender'); // ... } }

Опция autowire

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

Опция autoconfigure

В конфигурационном файле по умолчанию services.yaml , приведенном выше, имеетcя autoconfigure: true в блоке _defaults, это означает, что настройка применяется ко всем Сервисам, определенным в этом файле. С этим параметром контейнер автоматически применит определенную конфигурацию к вашим сервисам, основываясь на классе вашего сервиса. В основном это используется для автоматической простановки Тегов для ваших Сервисов.

Например, чтобы создать расширение Twig, вам нужно создать класс, зарегистрировать его как сервис и тегировать его как twig.extension.

Но, с настройкой autoconfigure: true, вам не нужен тег. В самом деле, если вы используете конфигурационный файл services.yaml с конфигурацией по умолчанию, вам не нужно делать ничего : услуга будет автоматически загружена. Затем autoconfigure добавит тег twig.extension для вас, потому что ваш класс реализует интерфейс Twig\Extension\ExtensionInterface. И благодаря autowire, вы даже можете добавить аргументы в конструктор без какой-либо дополнительной конфигурации.

Сервисы: публичные и приватные

В настройках по умолчанию конфигурационного файла services.yaml в блоке _defaults для каждого Сервиса, определенного в этом файле указана настройка public: false.

Что это означает? Когда Сервис является публичным, то вы можете получить к нему доступ непосредственно из объекта контейнер, который доступен из любого контроллера , если тот расширяет базовый класс Controller:

use App\Service\MessageGenerator; // ... public function new() { // there IS a public "logger" service in the container $logger = $this->container->get('logger'); // this will NOT work: MessageGenerator is a private service $generator = $this->container->get(MessageGenerator::class); }

Хорошей практикой является создавать Сервисы приватными (именно так настроено по умолчанию). Также вы не должны использовать метод $container->get() для получения публичных Сервисов.

Если Вам действительно нужно сделать Сервис публичным, то для этого просто переопределите настройку public для конкретного Сервиса:

# config/services.yaml services: # ... same code as before # explicitly configure the service App\Service\MessageGenerator: public: true

Импорт множества Сервисов из одного источника

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

# config/services.yaml services: # ... # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name App\: resource: '../src/*' exclude: '../src/{Entity,Migrations,Tests}'

Это можно использовать, чтобы быстро сделать многие классы доступными в качестве Cервисов и применить некоторые настройки по умолчанию. id каждого сервиса будет задано его полным именем класса. Вы можете переопределить любой Сервис, который импортирован, используя его идентификатор (имя класса) ниже. Если вы переопределяете Сервис, ни один из параметров (например public) не наследуется от импорта (но переопределенный Сервис все еще наследует _defaults).

Вы также можете exclude определенные пути (из них автоматически не будут импортироваться Сервисы). Это необязательно, но немного повысит производительность в dev среде: исключенные пути не отслеживаются, поэтому их изменение не приведет к перестройке контейнера.


На самом деле импорт означает, что все классы «доступны для использования в качестве сервисов» без необходимости ручной настройки.

Определение нескольких Сервисов с использованием одинакового пространства имен

Если вы определяете Сервисы, используя формат конфигурации YAML, то пространство имен PHP используется в качестве ключа для каждой конфигурации, поэтому вы не можете определить различные конфигурации Сервисов для классов в одном и том же пространстве имен:

# config/services.yaml services: App\Domain\: resource: '../src/Domain/*' # ...

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

# 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 все Сервисы и (как правило) аргументы были явно настроены: не было возможности загружать Сервисы автоматически, а автоматическое подключение (autowiring) было гораздо реже.

Обе эти функции являются необязательными. И даже если вы используете их, в некоторых случаях вы можете вручную подключить сервис. Например, предположим, что вы хотите зарегистрировать 2 службы для класса SiteUpdateManager — каждый с разным адресом электронной почты администратора. В этом случае каждый должен иметь уникальный идентификатор сервиса id:

# config/services.yaml services: # ... # this is the service's id site_update_manager.superadmin: class: App\Updates\SiteUpdateManager # you CAN still use autowiring: we just want to show what it looks like without autowire: false # manually wire all arguments arguments: - '@App\Service\MessageGenerator' - '@mailer' - 'superadmin@example.com' site_update_manager.normal_users: class: App\Updates\SiteUpdateManager autowire: false arguments: - '@App\Service\MessageGenerator' - '@mailer' - 'contact@example.com' # Create an alias, so that - by default - if you type-hint SiteUpdateManager, # the site_update_manager.superadmin will be used App\Updates\SiteUpdateManager: '@site_update_manager.superadmin'

В конфигурационном файле выше регистрируются два Сервиса: site_update_manager.superadmin и site_update_manager.normal_users. Благодаря алиасу, если вы введете тайп-хинт SiteUpdateManager, то будет передан первый (site_update_manager.superadmin). Если вы хотите передать второй Сервис, вам нужно будет вручную подключить этот Сервис.

Если вы не создаете алиас и загружаете все Сервисы из src /, то будут созданы три Сервиса (автоматический сервис + ваши два Сервиса), и автоматически загруженный Сервис будет передаваться — по умолчанию — при вводе тайп-хинта SiteUpdateManager. Вот почему создание алиасов это хорошая идея.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *