Компонент DependencyInjection (внедрение зависимости)

Дата обновления перевода: 2024-07-03

Компонент DependencyInjection (внедрение зависимости)

Компонент DependencyInjection реализует сервис-контейнер, совместимый с PSR-11, который позволяет вам стандартизировать и централизовать то, как строятся объекты в вашем приложении.

Для вступления во Внедрение зависимостей и сервис-контейнерв, см. Сервис-контейнер.

Установка

1
$ composer require symfony/dependency-injection

Note

Если вы устанавливаете этот компонент вне приложения Symfony, вам нужно подключить файл vendor/autoload.php в вашем коде для включения механизма автозагрузки классов, предоставляемых Composer. Детальнее читайте в этой статье.

Базовое применение

See also

Эта статья объясняет как использовать функции DependencyInjection как независимого компонента в любом приложении PHP. Прочитайте статью Сервис-контейнер для понимания как использовать его в приложениях Symfony.

У вас может быть простой класс вроде следующего Mailer, который вы хотите сделать доступным в качестве сервиса:

1
2
3
4
5
6
7
8
9
10
11
class Mailer
{
    private string $transport;

    public function __construct()
    {
        $this->transport = 'sendmail';
    }

    // ...
}

Вы можете зарегистрировать это в контейнере, как сервис:

1
2
3
4
use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();
$container->register('mailer', 'Mailer');

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

1
2
3
4
5
6
7
8
9
class Mailer
{
    public function __construct(
        private string $transport,
    ) {
    }

    // ...
}

То вы сможете устанавливать выбор транспорта в контейнере:

1
2
3
4
5
6
use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();
$container
    ->register('mailer', 'Mailer')
    ->addArgument('sendmail');

Этот класс теперь намного более гибкий, так как вы отделили выбор транспорта от реализации, и поместили его в контейнер.

То, какой почтовый транспорт вы выбрали, может быть чем-то, о чём стоит знать другим сервисам. Вы можете избежать его изменения в нескольких местах, сделав его параметром в контейнере, а потом ссылаясь на этот параметр в аргументе конструктора сервиса Mailer:

1
2
3
4
5
6
7
use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();
$container->setParameter('mailer.transport', 'sendmail');
$container
    ->register('mailer', 'Mailer')
    ->addArgument('%mailer.transport%');

Теперь, когда сервис mailer находится в контейнере, вы можете внедрить его в качестве зависимости других классов. Если у вас есть класс NewsletterManager, то делается это так:

1
2
3
4
5
6
7
8
9
class NewsletterManager
{
    public function __construct(
        private \Mailer $mailer,
    ) {
    }

    // ...
}

При определении сервиса newsletter_manager, сервис mailer ещё не существует. Используйте класс Reference, чтобы сообщить контейнеру внедрить сервис mailer, когда он инициализирует менеджер новостных сообщений:

1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

$container = new ContainerBuilder();

$container->setParameter('mailer.transport', 'sendmail');
$container
    ->register('mailer', 'Mailer')
    ->addArgument('%mailer.transport%');

$container
    ->register('newsletter_manager', 'NewsletterManager')
    ->addArgument(new Reference('mailer'));

Если NewsletterManager не требовал Mailer, и его внедрение было необязательным, то вы можете использовать внедрение сеттера:

1
2
3
4
5
6
7
8
9
10
11
class NewsletterManager
{
    private \Mailer $mailer;

    public function setMailer(\Mailer $mailer): void
    {
        $this->mailer = $mailer;
    }

    // ...
}

Вы также можете теперь выбрать не внедрять Mailer в NewsletterManager. Если же вы хотите сделать это, то контейнер может вызвать сеттер-метод:

1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

$container = new ContainerBuilder();

$container->setParameter('mailer.transport', 'sendmail');
$container
    ->register('mailer', 'Mailer')
    ->addArgument('%mailer.transport%');

$container
    ->register('newsletter_manager', 'NewsletterManager')
    ->addMethodCall('setMailer', [new Reference('mailer')]);

Потом вы можете получить ваш сервис newsletter_manager из контейнера таким образом:

1
2
3
4
5
6
7
use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();

// ...

$newsletterManager = $container->get('newsletter_manager');

Получение несуществующих сервисов

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

1
2
3
4
5
6
7
8
9
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;

$containerBuilder = new ContainerBuilder();

// ...

// второй аргумент опциональный и определяет что делать, если сервис не существует
$newsletterManager = $containerBuilder->get('newsletter_manager', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE);

Вот все возможные виды поведения:

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

Избегание зависимости вашего кода от контейнера

Несмотря на то, что вы можете извлекать сервисы из контейнера напрямую, лучше это минимизировать. Например, в случае с NewsletterManager, в который вы внедрили сервис mailer, вместо того, чтобы запросить его из контейнера. Вы могли внедрить контейнер, и потом извлечь из него сервис mailer, но он был бы привязан к этому конкретному контейнеру, что усложнило бы повторное использование класса где-либо.

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

Установка контейнера с файлами конфигурации

Кроме установки сервисов, используя PHP, как описано выше, вы также можете использовать файлы конфигурации. Это позволяет вам использовать XML или YAML для написания описаний для сервисов, а не использовать PHP для определения сервисов, как в примере выше. В любых случаях, кроме самых маленьких приложений, логино упорядочить определения сервиса, переместив их в один или более файлов конфигурации. Чтобы сделать это, вам также понадобится установить компонент Config.

Загрузка файла конфигурации XML:

1
2
3
4
5
6
7
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;

$container = new ContainerBuilder();
$loader = new XmlFileLoader($container, new FileLocator(__DIR__));
$loader->load('services.xml');

Загрузка файла конфигурации YAML:

1
2
3
4
5
6
7
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(__DIR__));
$loader->load('services.yaml');

Note

Если вы хотите загрузить файлы конфигурации YAML, то вам также понадобится установить компонент Yaml.

Tip

Если ваше приложение использует нетрадиционные расширения файла (например, ваши файлы XML имеют расширение .config), то вы можете передать тип файла в качестве второго необязательного параметра метода load():

1
2
// ...
$loader->load('services.config', 'xml');

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

1
2
3
4
5
6
7
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;

$container = new ContainerBuilder();
$loader = new PhpFileLoader($container, new FileLocator(__DIR__));
$loader->load('services.php');

Теперь вы можете устанавливать сервисы newsletter_manager и mailer, используя файлы конфигурации:

1
2
3
4
5
6
7
8
9
10
11
12
parameters:
    # ...
    mailer.transport: sendmail

services:
    mailer:
        class:     Mailer
        arguments: ['%mailer.transport%']
    newsletter_manager:
        class:     NewsletterManager
        calls:
            - [setMailer, ['@mailer']]

Узнайте больше