Документация/Контейнер/Внедрение зависимостей

Внедрение зависимостей

Внедрение зависимостей (также Dependency injection или DI) — механизм подстановки фреймворком зависимостей для конструктора или других методов у создаваемых объектов.

При создании объектов фреймворком, таких, как контроллеры, middlewares, команды и другие, внедрение зависимостей уже назначено при вызове целевого метода (в том числе конструктора).

Согласно механизму DI предполагается, что если указать в зависимостях (аргументах) метода нужные в нём классы или интерфейсы, то фреймворк попытается найти такие соответствия в контейнере, получит из контейнера или самостоятельно создаст объект и подставит его в необходимый аргумент.
При этом, если в контейнере такой сервис найден не будет, то будет произведена попытка создать объект из обычного подходящего класса в проекте, а если у последнего есть свои зависимости в конструкторе, то фреймворк попробует их наполнить аналогичным образом.
При отсутствии подстановочного значения для аргументов, которые имеют значения по умолчанию, будет использовано дефолтное.
Иначе фреймворк вернёт ошибку с информацией, что успешно использовать DI для указанных зависимостей не удалось.


#Реализация DI во фреймворке

Когда объект контроллера или middleware создается на стороне фреймворка, то сначала разрешаются зависимости конструктора, затем вызываемого метода.

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

На следующем примере показаны два метода контроллера с различным присвоением $logger из контейнера через DI.

<?php

namespace App\Controllers;

use 
Hleb\Base\Controller;
use 
Hleb\Reference\LogInterface;

class 
ExampleController extends Controller
{
    public function 
__construct(private readonly LogInterface $logger, array $config = [])
    {
        
parent::__construct($config);
    }

    public function 
first(LogInterface $logger): void
    
{
        
// variant 1
    
}

    public function 
second(): void
    
{
        
// variant 2
        
$logger $this->logger;
    }
}

Аналогичным образом устанавливаются зависимости для middleware.

В командах фреймворка и в событиях (Events) реализовано похожим образом, но только через конструктор:

<?php

namespace App\Commands\Demo;

use 
Hleb\Base\Task;
use 
Hleb\Reference\LogInterface;

class 
ExampleTask extends Task
{
    public function 
__construct(private readonly LogInterface $logger, array $config = [])
    {
        
parent::__construct($config);
    }

    protected function 
run(): int
    
{
        
$logger $this->logger;

        return 
self::SUCCESS_CODE;
    }
}

#Создание объектов с DI

Внедрение зависимостей удобно тем, что при тестировании мы можем создать нужные значения для зависимостей класса. Однако, при создании объекта вручную, было бы неудобно инициализировать самим все его зависимости. Чтобы автоматизировать этот процес, существует класс Hleb\Static\DI фреймворка.

use Hleb\Reference\LogInterface;
use 
Hleb\Static\DI;

// Demo class for insertion.
class Insert
{
}

// Class with dependencies.
class Example
{
    public function 
__construct(private readonly LogInterface $logger)
    {
    }

    public function 
run(Insert $insert): void
    
{
        echo 
$this->logger::class;
        echo 
' & ';
        echo 
$insert::class;
    }
}

$exampleObject DI::object(Example::class);

echo 
DI::method($exampleObject'run'); // Hleb\Reference\LogReference & Insert

Здесь показано, как создать объект класса, в конструкторе которого есть зависимость, а также вызвать нужный метод объекта, в котором также нужно автоматически подставить значение. На примере также есть зависимость не из контейнера (класс Insert), объект которой создается и подстанавливается в метод.

Довольно часто используемый вариант DI c Request и Response (в данном случае получаемых из контейнера):

<?php

namespace App\Controllers;

use 
Hleb\Base\Controller;
use 
Hleb\Reference\Interface\Request;
use 
Hleb\Reference\Interface\Response;

class 
MainController extends Controller
{
    public function 
index(Request $requestResponse $response): Response
    
{
        
// ... //

        
return $response;
    }
}

Из-за существования различных подходов в именовании интерфейсов, получение стандартных сервисов из контейнера может быть как по интерфейсу с окончанием Interface, так и без. Например, Hleb\Reference\RequestInterface аналогичен Hleb\Reference\Interface\Request.


#Автоподстановка для не найденных в контейнере зависимостей

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

Существуют способы для указания, каким путём нужно следовать в таком случае. Через параметр конфигурации `system`.`autowiring.mode` устанавливается режим управления такими зависимостями. Существует режим, в котором можно полностью отключить автоподстановку не найденных в контейнере зависимостей и режим аналогичный этому, но при наличии атрибута AllowAutowire разрешающий использовать объект класса, а также атрибут NoAutowire, запрещающий автопостановку текущего класса, если включен разрешающий режим с поддержкой этого атрибута.


#Управление зависимостями

При помощи специального атрибута DI можно указать в конкретном месте (методе класса), какую именно зависимость с указанным интерфейсом нужно использовать. Если такая зависимость из атрибута будет найдена в контейнере, то будет использована из контейнера. Если нет, то здесь действуют те же правила автоподстановки не найденных в контейнере зависимостей, как если бы она была указана напрямую в методе. Примеры:

<?php

use Hleb\Base\Controller;
use 
Hleb\Constructor\Attributes\Autowiring\DI;

class 
ExampleController extends Controller
{
    public function 
index(
        #[DI(LocalFileStorage::class)]
        
FileSystemInterface $storage,

        #[DI('\App\Notification\JwtAuthenticator')]
        
AuthenticatorInterface $authenticator,

        #[DI(new EmailNotificationSender())]
        
NotificationSenderInterface $notificationSender,
    ) {
        
//...//
    
}
}

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

Получение сервиса Request

Страница создана: @fomiash
К началу страницы