IoC контейнер и внедрение зависимостей. Как это работает на примере Laravel и PHP-DI? Взгляд изнутри.

Вы начинаете работать с Laravel или с любым другим современным PHP-фреймворком в целом? Или вы уже используете фреймворк, который уже поддерживает инъекцию зависимостей и имеет контейнер IoC, но вы хотите понять, как эта магия работает за кулисами? Тогда вы находитесь в правильном месте, потому что я попытаюсь разобрать это и объяснить, как контейнер IoC и инъекция зависимостей связаны вместе и работают под капотом.

Определения и предыстория

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

Так что же такое IoC контейнер?

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

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

Но также, просто чтобы быть более конкретным и предоставить более подробную информацию, стоит также упомянуть, что аббревиатура IoC происходит от инверсии управления:

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

Простая инверсия примеров управления:

  • Инъекция зависимостей.
    Внедрение некоторого класса в конструктор – это часть “когда делать ” (так как он вызывается каким-то клиентом). Контейнер, который должен создать экземпляр и внедрить объект, – это часть “что делать”.
  • Обработка событий.
    Отправка нового события – это часть “когда делать ” (так как оно инициируется каким-то клиентом). Прослушиватель событий отвечает на часть “что делать”.

А как насчет внедрения зависимостей?

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

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

На сегодня достаточно теории. Давайте рассмотрим несколько примеров кода.

Инверсия управления, что же на самом деле инвертировано?

Чтобы лучше понять определения и теорию сверху, я думаю, что пришло время показать некоторые примеры кода. Я сделаю все возможное, чтобы быть как можно ближе к реальным ситуационным сценариям, так как лично мне иногда бывает трудно понять примеры с Foo, Bar… и прочие синтетические имена классов

Во-первых, давайте посмотрим на простой PHP-код, где у нас есть контроллер, который вызывает некоторый уровень бизнес-логики для регистрации нового пользователя. Простой код, без инъекции зависимостей.

<?php

// Controller that probably will be an action of and endpoint that should create new user
class RegisterController extends Controller
{
    public function register(): User
    {
        // Validate the incoming request
        $attributes = ['email' => 'fake-mail@gradualcode.com', 'password' => 'not-hashed'];
        // Get the attributes from the request if the given data is valid
        // Open database transaction
        $action = new CreateNewUserAction();

        // Commit database transaction
        return $action->handle($attributes);
    }
}

// Encapsualted logic for creating new user. Simple one
class CreateNewUserAction extends AbstractAction
{
    /**
     * @var PostgresUser
     */
    private $userRepository;

    public function __construct()
    {
        $this->userRepository = new PostgresUser(new User());
    }

    public function handle(array $attributes): User
    {
        $attributes['password'] = BcryptHasher::make($attributes['password']);

        return $this->userRepository->create($attributes);
    }
}
  1. У нас есть клиентский объект RegisterController
  2. RegisterController готов вызвать метод register
  3. Но для того, чтобы RegisterController мог зарегистрировать пользователя, нам необходимо создать его зависимость на класс CreateNewUserAction
  4. CreateNewUserAction придется создать конкретную зависимость на PostgresUser, а также создать экземпляр другой зависимости, универсальной модели User
  5. Для того чтобы хэшировать выбранный пользователем пароль, CreateNewUserAction следует также вызвать статический метод BcryptHasher, который также указывает на то, что CreateNewUserAction зависит от BcryptHasher

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

Давайте рассмотрим тот же пример, но с использованием инъекции зависимостей

<?php

// Controller that probably will be an action of and endpoint that should create new user
class RegisterController extends Controller
{
    public function register(CreateNewUserAction $action): User
    {
        // Validate the incoming request
        $attributes = ['email' => 'fake-mail@gradualcode.com', 'password' => 'not-hashed'];
        // Get the attributes from the request if the given data is valid
        // Open database transaction

        // Commit database transaction

        return $action->handle($attributes);
    }
}

// Encapsualted logic for creating new user. Simple one
class CreateNewUserAction extends AbstractAction
{
    /**
     * @var PostgresUser
     */
    private $userRepository;

    /**
     * @var BcryptHasher
     */
    private $hasher;

    public function __construct(PostgresUser $userRepository, BcryptHasher $hasher)
    {
        $this->userRepository = $userRepository;
        $this->hasher = $hasher;
    }

    public function handle(array $attributes): User
    {
        $attributes['password'] = $this->hasher->make($attributes['password']);

        return $this->userRepository->create($attributes);
    }
}
  1. У нас есть клиентский объект RegisterController
  2. RegisterController имеет CreateNewUserActionas зависимость, поэтому контейнер IoC попытается создать экземпляр CreateNewUserAction
  3. При создании экземпляра CreateNewUserAction контейнер IoC сначала должен будет создать экземпляр PostgresUser и BcryptHasher
  4. После создания экземпляра PostgresUser и BcryptHasher наш объект CreateNewUserAction готов к инъекции со своими зависимостями в метод register внутри RegisterController
  5. RegisterController теперь готов вызвать registerметод

Теперь вы можете определить главное различие и то, что перевернуто с помощью техники IoC. В принципе, метод register не может быть вызван до тех пор, пока все зависимости не будут разрешены, а это означает, что он всегда будет сначала создавать экземпляры зависимостей. Итак, если мы рассматриваем этот пример как дерево, где RegisterController – это корень, а PostgresUser и BcryptHasher – это листья, то сначала листья будут разрешены и созданы, а корень – последним. За это отвечает контейнер IoC.

Я хочу отметить, что вы, вероятно, более знакомы с терминами Dependency Injection Container (DI Container) или Service Container (поскольку он содержит службы (зависимости), которые могут быть введены) вместо контейнера IoC.Как я уже говорил в начале, инверсия управления – это очень общее название, это просто его формы. Но все это относится к одному и тому же, и весь смысл в том, чтобы разделить части “что делать” и “когда делать”.
Так RegisterController не знает,” что делать”, чтобы создать экземпляр CreateNewUserAction, но контейнер IoC (Illuminate\Container\Container) делает это. Контейнер IoC не знает, “когда делать”, так как не знает, когда нужно будет создать CreateNewUserAction для RegisterController.

Именно так мы имеем” что делать “и” когда делать”, разделенные контейнером IoC.

Ну хорошо, мы используем инъекцию зависимостей, но все еще используем конкретные классы. В следующем примере я буду использовать фреймворк Laravel, чтобы предоставить более реальный пример из реальной жизни.

<?php

// Controller that probably will be an action of and endpoint that should create new user
class RegisterController extends Controller
{
    public function register(
        CreateNewUserRequest $request, // Some concrete request object that containes the rules for request validation
        CreateNewUserAction $action, // The action we need to call
        DatabaseManager $database, // Concrete database manager instance. Part of Laravel
        ResponseFactory $responseFactory // Response factory interface. Part of Laravel
    ): User {
        $attributes = $request->validated();

        $database->beginTransaction();
        $user = $action->handle($attributes);
        $database->commit();

        return $responseFactory->json($user, 201);
    }
}

// Encapsualted logic for creating new user. Simple one
class CreateNewUserAction extends AbstractAction
{
    /**
     * @var UserRepository
     */
    private $userRepository; // Interface. The concrete implementation will be decided by the IoC container

    /**
     * @var Hasher Part of Laravel. That is why I didn't include code sample
     */
    private $hasher; // Interface. The concrete implementation will be decided by the IoC container

    public function __construct(UserRepository $userRepository, Hasher $hasher)
    {
        $this->userRepository = $userRepository;
        $this->hasher = $hasher;
    }

    public function handle(array $attributes): User
    {
        $attributes['password'] = $this->hasher->make($attributes['password']);

        return $this->userRepository->create($attributes);
    }
}

// Sample repository interface example. Please do not forget that interfaces are not instantiable.
interface UserRepository
{
    /**
     * Create new user from the given attributes
     * @param array $attributes
     * @return User
     */
    public function create(array $attributes): User;
}

Прямо сейчас мы видим, что мы также полагаемся на интерфейсы, что всегда является хорошей идеей. Это помогает модульному тестированию, замена реализаций также проста. Но как на самом деле создаются интерфейсы? Как контейнер IoC знает, какой экземпляр должен быть возвращен вместо интерфейса?

Замена абстракций (интерфейсы) конкретными классами

Основываясь на последнем примере, кажется, что интерфейсы являются инстанцируемыми, но мы все знаем, что это неверно. Поэтому, прежде чем я начну объяснять интерфейсы, которые взяты из нашего исходного кода (а не из Laravel), давайте посмотрим, например, как Laravel создает экземпляр Illuminate\Contracts\Routing\ResponseFactory, который мы ввели внутрь RegisterController.

Итак, если мы посмотрим на использование ResponseFactory, то увидим, что их очень много, и это ожидаемо, так как это интерфейс, но я бы очень хотел обратить внимание на использование, которые находятся внутри любого ServiceProvider класса. Это в основном набор классов, которые вызываются при загрузке фреймворка, чтобы установить все требования для правильной работы. Вы можете прочитать больше о поставщиках услуг в Laravel.

Чтобы продолжить наше расследование. Фрагмент кода, который нас здесь интересует, находится в Illuminate\Routing\RoutingServiceProvider@registerResponseFactory

<?php

use Illuminate\Contracts\Routing\ResponseFactory as ResponseFactoryContract;
use Illuminate\Contracts\View\Factory as ViewFactoryContract;

...
/**
 * Register the response factory implementation.
 *
 * @return void
 */
protected function registerResponseFactory()
{
    $this->app->singleton(ResponseFactoryContract::class, function ($app) {
        return new ResponseFactory($app[ViewFactoryContract::class], $app['redirect']);
    });
}

Так что же здесь происходит? Мы можем перевести этот registerResponseFactory метод в понятное человеку предложение:
– когда приложению ($this->app) нужно где-то внедрить интерфейс Illuminate\Contracts\Routing\ResponseFactory, верните новый синглтон Illuminate\Routing\ResponseFactory. Не позволяйте названию сбить вас с толку, это два разных класса в двух разных пространствах имен.

Так что это вовсе не волшебство. Где-то в фреймворке есть какой-то фрагмент кода, который, когда его спрашивают, знает какую реализацию использовать если указан конкретный интерфейс. И да, если вы еще раз глянете Illuminate\Routing\ResponseFactory, то увидите, что он реализует интерфейс Illuminate\Contracts\Routing\ResponseFactory.

Вы, вероятно, думаете, что интерфейсы Laravel обрабатываются фреймворком, поэтому нам не нужно беспокоиться о них. Но у нас все еще есть UserRepository, который является интерфейсом.. а также CreateNewUserAction, который является конкретным классом. Нам нужно как-то их разрешить?

Это верно, так как теперь, если мы выставим наш код в качестве конечной точки и попытаемся выполнить его, мы увидим сообщение об ошибке, в котором говорится: Target [App\DAL\User\UserRepository] is not instantiable while building [App\Actions\User\CreateNewUserAction]. А если мы пойдем еще дальше, то просто подтвердим, что register метод даже не вызывается. Он еще не выполнен в соответствии с трассировкой стека ошибок. Поэтому контейнер IOC Laravel проверил, каковы требования для вызова метода register, попытался создать экземпляр зависимостей, и при создании экземпляра CreateNewUserAction он потерпел неудачу, так как интерфейсы (в данном случае UserRepository) не являются экземплярами.

Как мы можем решить эту проблему? Точно так же, как настройка Laravel, нам нужно иметь место, где мы определяем, что должно быть возвращено, когда какой-то клиентский объект (RegisterController в нашем примере) просит ввести какой-то интерфейс. Лучшее место для этого-по дефолту AppServiceProvider.

<?php

namespace App\Providers;

use App\DAL\User\PostgresUser;
use App\DAL\User\UserRepository;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     * @return void
     */
    public function register(): void
    {
        // Any other service provider logic
        $this->registerRepositories();
    }

    /**
     * Bootstrap any application services.
     * @return void
     */
    public function boot(): void
    {
        //
    }

    private function registerRepositories(): void
    {
        $this->app->bind(UserRepository::class, PostgresUser::class);
        // We are literally binding the interface (UserRepository) to a concrete class
        // So when somewhere in the applicatoin the UserRepository should be injected
        // instance of PostgresUser will be return
        
        // I prefer to create a map of interfaces that need binding
        // And just itterate through them. I find it more readable
        // $toBind = [
        //     UserRepository::class => PostgresUser::class,
        // ];

        // foreach ($toBind as $interface => $implementation) {
        //     $this->app->bind($interface, $implementation);
        // }
    }
}

Честно говоря, в реальном проекте я бы выделил в DalServiceProvider то, что отвечает только за привязки этого репозитория. Затем зарегистрировал бы DalServiceProvider в системе AppServiceProvider. Но это не относится к сути примера.

И конкретная реализация содержит логику

<?php

namespace App\DAL\User;

use App\Models\User;

class PostgresUser implements UserRepository
{
    /**
     * @var User
     */
    private $model;

    public function __construct(User $model)
    {
        $this->model = $user;
    }

    public function create(array $attributes): User
    {
        return $this->model->newQuery()->create($attributes);
    }
}

Итак, теперь мы увидели две разные вещи. В Примере, где мы смотрели на ResponseFactory, было ясно, что был создан новый экземпляр, но в нашем случае мы просто даем полное имя конкретного класса (в основном string). Так что же на самом деле там происходит?

Магия за контейнерами IoC

Мы видели две разные реализации и пример того, как заменяются и используются интерфейсы. Сначала мы увидели интерфейс Laravel ResponseFactory, а затем еще один, где нам нужно было определить реализацию для нашего UserRepository. Как мы помним, есть два разных способа, и оба они прекрасно работают. Итак, наконец, давайте посмотрим, что происходит за кулисами.

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

Если мы сравним оба примера, упомянутых выше, то увидим одну общую черту. Оба они вызывают некоторые методы в некотором приложении ($this->app), и мы можем проследить и отследить из нашего DalServiceProvider тип переменной, что это такое.

Мы видим, что наша DalServiceProvider переменная нигде не определена, поэтому она должна исходить из родительского класса Illuminate\Support\ServiceProvider. Если мы откроем этот класс, то в самом верху, как первое определенное свойство, мы увидим, что переменная $app определена, и это должен быть интерфейс типа Illuminate\Contracts\Foundation\Application.

<?php

...
  
abstract class ServiceProvider
{
    /**
     * The application instance.
     *
     * @var \Illuminate\Contracts\Foundation\Application
     */
    protected $app;

...
    
    /**
     * Create a new service provider instance.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @return void
     */
    public function __construct($app)
    {
        $this->app = $app;
    }
    
...
}

Но тогда, мы все еще смотрим на интерфейсы, мы не найдем там никакой логики. Это означает, что это приложение является каким-то конкретным экземпляром, который передается в метод конструктора ServiceProvider, и этот экземпляр должен содержать все наши ответы. Итак, давайте проверим, как создается экземпляр этого поставщика услуг. Поскольку это абстрактный класс, мы найдем способы использования классов, которые наследуют этот класс. После проверки использования вам, вероятно, укажут на Illuminate\Foundation\Application, и если вы посмотрите на интерфейсы Illuminate\Contracts\Foundation\Application, то они будут реализованы в этом классе.

Класс Application в Laravel-это буквально сам Laravel (в некотором смысле). В нем есть вся логика о том, как приложение должно быть загружено, конфигурации, какие поставщики услуг должны быть запущены, какие пути должны быть загружены, в основном все настройки для фреймворка как есть. Так как Application содержит всю эту логику, то это означает, что этот класс всегда должен быть создан первым, так как он должен знать все, что там есть. И если вы проверите файл точки входа Laravel public/index.php, то увидите, что там есть строка, в которой определено приложение: $app=require_once __DIR__.'/../bootstrap/app.php';

<?php

/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| The first thing we will do is create a new Laravel application instance
| which serves as the "glue" for all the components of Laravel, and is
| the IoC container for the system binding all of the various parts.
|
*/

$app = new Illuminate\Foundation\Application(
    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

/*
|--------------------------------------------------------------------------
| Bind Important Interfaces
|--------------------------------------------------------------------------
|
| Next, we need to bind some important interfaces into the container so
| we will be able to resolve them when needed. The kernels serve the
| incoming requests to this application from both the web and CLI.
|
*/

$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

/*
|--------------------------------------------------------------------------
| Return The Application
|--------------------------------------------------------------------------
|
| This script returns the application instance. The instance is given to
| the calling script so we can separate the building of the instances
| from the actual running of the application and sending responses.
|
*/

return $app;

Если Вы прочитаете первый комментарий, то уже поняли, что в Application речь идет о контейнере IoC для Laravel. И это в некотором смысле правильно, так как, имея экземпляр Illuminate\Foundation\Application, вы сможете регистрировать новые биндинги, синглтоны, инстансы (вы можете найти больше о контейнерных функциях Laravel), но настоящая магия находится в родительском классе Illuminate\Container\Container. Итак, наконец-то мы нашли мага, который отвечает за магию за кулисами.

Если вы быстро взглянете на Container, то заметите, что на первый взгляд существует много массивов в качестве свойств. И именно здесь начинается сила контейнера. В этом случае контейнер Laravel имеет множество поддерживаемых функций, поэтому он имеет своего рода один массив свойств для каждой функции. Но ради этого примера давайте попробуем расшифровать метод bind.

Что будет происходить за кулисами – это действительно просто. В основном Container просто добавит новое значение к своему свойству $bindings, и в следующий раз, когда клиентские объекты нуждаются в интерфейсе, контейнер IoC проверит это свойство $bindings и построит для него конкретную реализацию. Давайте посмотрим на примеры

<?php

/**
 * Register a binding with the container.
 *
 * @param  string  $abstract
 * @param  \Closure|string|null  $concrete
 * @param  bool  $shared
 * @return void
 */
public function bind($abstract, $concrete = null, $shared = false)
{

   // Some code is removed, just for simplicity
    
    if (is_null($concrete)) {
        $concrete = $abstract;
    }
    if (! $concrete instanceof Closure) {
        if (! is_string($concrete)) {
            throw new \TypeError(self::class.'::bind(): Argument #2 ($concrete) must be of type Closure|string|null');
        }

        $concrete = $this->getClosure($abstract, $concrete);
    }

    $this->bindings[$abstract] = compact('concrete', 'shared');

    // Some code is removed just for simplicity
}

Итак, в основном будет существовать массив, где ключ – это фактическое имя класса, включая пространство имен класса, а в качестве значений для этого ключа будет использоваться конкретный класс. Поэтому не имеет значения, представляем ли мы его как Closure или мы предоставляем конкретное пространство имен классов, суть в том, чтобы дать некоторую информацию о нашей конкретной реализации этого интерфейса контейнеру IoC.

Прямо сейчас контейнер IoC знает, что делать, когда любой клиентский объект запрашивает интерфейс UserRepository. Единственное, что остается объяснить, – это как построена конкретная реализация.

Пожалуйста, обратите внимание, что я стараюсь не вдаваться в подробности этого вопроса, так как контейнер Laravel, опять же, действительно продвинутый и имеет много функций. Я просто хочу объяснить основные моменты, что делает один класс контейнером IoC.

Классы рефлексии. Настоящая магия

Если вы посмотрите класс Illuminate\Container\Container, вы, вероятно, заметите много вещей, происходящих там. Я попытаюсь разбить его на простые конкретные примеры, которые все еще находятся в этом контейнере IoC.

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

<?php

use App\Http\Controllers\RegisterController;

Route::post('register', RegisterController::class . '@register');

Это означает, что для того, чтобы маршрутизатор вызывал метод register из App\Http\Controllers\RegisterController класса, он должен каким-то образом:

  1. Разрешить зависимости для RegisterController
  2. Разрешить зависимости для CreateNewUserAction
  3. Создать новый CreateNewUserAction экземпляр с разрешенными зависимостями
  4. Создать новый RegisterController экземпляр с разрешенными зависимостями (в данном случае CreateNewUserAction)
  5. Разрешить зависимости для метода RegisterController::register
  6. Вызвать метод register из RegisterController с разрешенными зависимостями

Выглядит знакомо, верно? Инверсия управления, сначала мы решаем листья, затем создаем корень и, наконец, вызываем нужный метод.
Давайте начнем с разрешения зависимостей конструктора для RegisterController

Вся эта магия, которая позволяет контейнеру IoC иметь информацию о том, как разрешить класс, если указано только полное имя класса, основана на использовании ReflectionClass.

У ReflectionClass есть вся информация для данного полного имени класса. Обратите внимание, что это должен быть существующий класс, иначе он вызовет исключение. Существует множество методов и опций, доступных у экземпляра ReflectionClass, но давайте сосредоточимся только на тех, которые входят в сферу интереса этого поста

Первый шаг: проверяем, является ли данный класс инстанцируемым

<?php

// Make reflection instance of the controller
$controllerReflection = new ReflectionClass(App\Http\Controllers\RegisterController::class);

var_dump($controllerReflection->isInstantiable());
// Result: 
bool(true)


// Make reflection instance of the repository interface
$repositoryReflection = new ReflectionClass(App\DAL\User\UserRepository::class);

var_dump($repositoryReflection->isInstantiable());
// Result: 
bool(false)

Объект ReflectionClass может дать нам информацию из любого класса, можем ли мы создать экземпляр этого класса или нет. Точно так же, как у нас была ошибка Target [App\DAL\User\UserRepository] is not instantiable while building [App\Actions\User\CreateNewUserAction, когда мы пытались выполнить код, не сообщая контейнеру IoC, что делать, когда мы просим UserRepository.

Давайте посмотрим, как контейнер Laravel справляется с этим, и как мы получили сообщение об ошибке

<?php

/**
 * Resolve the given type from the container.
 *
 * @param  string  $abstract
 * @param  array  $parameters
 * @param  bool  $raiseEvents
 * @return mixed
 *
 * @throws \Illuminate\Contracts\Container\BindingResolutionException
 */
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
    $abstract = $this->getAlias($abstract);

    $concrete = $this->getContextualConcrete($abstract);

    $needsContextualBuild = ! empty($parameters) || ! is_null($concrete);

    // If an instance of the type is currently being managed as a singleton we'll
    // just return an existing instance instead of instantiating new instances
    // so the developer can keep using the same objects instance every time.
    if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
        return $this->instances[$abstract];
    }

    $this->with[] = $parameters;

    if (is_null($concrete)) {
        $concrete = $this->getConcrete($abstract);
    }

    // We're ready to instantiate an instance of the concrete type registered for
    // the binding. This will instantiate the types, as well as resolve any of
    // its "nested" dependencies recursively until all have gotten resolved.
    if ($this->isBuildable($concrete, $abstract)) {
        $object = $this->build($concrete);
    } else {
        $object = $this->make($concrete);
    }

    // Some code is removed, just for simplicity.

    return $object;
}


/**
 * Get the concrete type for a given abstract.
 *
 * @param  string  $abstract
 * @return mixed
 */
protected function getConcrete($abstract)
{
    // If we don't have a registered resolver or concrete for the type, we'll just
    // assume each type is a concrete name and will attempt to resolve it as is
    // since the container should be able to resolve concretes automatically.
    if (isset($this->bindings[$abstract])) {
        return $this->bindings[$abstract]['concrete'];
    }

    return $abstract;
}

/**
 * Determine if the given concrete is buildable.
 *
 * @param  mixed  $concrete
 * @param  string  $abstract
 * @return bool
 */
protected function isBuildable($concrete, $abstract)
{
    return $concrete === $abstract || $concrete instanceof Closure;
}

Таким образом, изначально происходит то, что контейнер IoC попытается найти любой псевдоним, определение экземпляров или привязки какого-либо рода, чтобы найти конкретную реализацию, которая должна быть создана. Поскольку мы никогда не регистрировали этот интерфейс, метод getConcrete будет возвращать то же пространство имен UserRepository, в результате чего метод isBuildable будет возвращать true, так $concrete как это буквально то же значение, что и $abstract

<?php

/**
 * Instantiate a concrete instance of the given type.
 *
 * @param  \Closure|string  $concrete
 * @return mixed
 *
 * @throws \Illuminate\Contracts\Container\BindingResolutionException
 */
public function build($concrete)
{
    // If the concrete type is actually a Closure, we will just execute it and
    // hand back the results of the functions, which allows functions to be
    // used as resolvers for more fine-tuned resolution of these objects.
    if ($concrete instanceof Closure) {
        return $concrete($this, $this->getLastParameterOverride());
    }

    try {
        $reflector = new ReflectionClass($concrete);
    } catch (ReflectionException $e) {
        throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
    }

    // If the type is not instantiable, the developer is attempting to resolve
    // an abstract type such as an Interface or Abstract Class and there is
    // no binding registered for the abstractions so we need to bail out.
    if (! $reflector->isInstantiable()) {
        return $this->notInstantiable($concrete);
    }

    // ... This is just part of the Laravel's Container build method
}

/**
 * Throw an exception that the concrete is not instantiable.
 *
 * @param  string  $concrete
 * @return void
 *
 * @throws \Illuminate\Contracts\Container\BindingResolutionException
 */
protected function notInstantiable($concrete)
{
    if (! empty($this->buildStack)) {
        $previous = implode(', ', $this->buildStack);

        $message = "Target [$concrete] is not instantiable while building [$previous].";
    } else {
        $message = "Target [$concrete] is not instantiable.";
    }

    throw new BindingResolutionException($message);
}
  1. Контейнер пытался разрешить UserRepository.
  2. Это фактическое значение App\DAL\User\UserRepository , и это строка, так что проверка, если это Closure, вернется false
  3. Класс уже существует, поэтому исключение не создается
  4. Но проверка isInstantiable вернет false, так как мы пытаемся создать экземпляр интерфейса
  5. Вы можете увидеть формат ошибки в строке 49 на примере кода

Но после того, как мы исправим эту проблему и свяжем UserRepositoryPostgresUser классом внутри appServiceProvider, переменная $concrete будет PostgresUser, а также это будет пытаться построить PostgresUser результирующее значение isInstaintable для возврата true. Итак, поскольку у нас есть инстанцируемое имя класса, контейнер попытается теперь создать его экземпляр и разрешить его.

Второй шаг: поиск зависимостей конструктора

Как уже упоминалось выше, мы должны разрешить зависимости App\DAL\User\PostgresUser и создать их объекты. Контейнер IoC теперь знает, что этот класс может быть создан, но он еще не знает, каковы зависимости для него.

ReflectionClass также может дать информацию по этому вопросу, используя метод getContructor(), который вернет экземпляр ReflectionMethod, а он будет содержать всю информацию, связанную с методом PostgresUser::__construct.

<?php

$repositoryReflection = new ReflectionClass(App\DAL\User\PostgresUser::class);

$repositoryReflection->getConstructor();
// Result
ReflectionMethod {#3262
     +name: "__construct",
     +class: "App\DAL\User\PostgresUser",
     parameters: {
       $user: ReflectionParameter {#3240
         +name: "user",
         position: 0,
         typeHint: "App\Models\User",
      },
     },
     modifiers: "public",
}

$repositoryReflection->getConstructor()->getParameters();
// Result
[
     ReflectionParameter {#3249
       +name: "user",
       position: 0,
       typeHint: "App\Models\User",
     },
]

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

Так что давайте посмотрим, как контейнер Laravel справится с этим

<?php

/**
 * Instantiate a concrete instance of the given type.
 *
 * @param  \Closure|string  $concrete
 * @return mixed
 *
 * @throws \Illuminate\Contracts\Container\BindingResolutionException
 */
public function build($concrete)
{   
    // If the concrete type is actually a Closure, we will just execute it and
    // hand back the results of the functions, which allows functions to be
    // used as resolvers for more fine-tuned resolution of these objects.
    if ($concrete instanceof Closure) {
        return $concrete($this, $this->getLastParameterOverride());
    }

    try {
        $reflector = new ReflectionClass($concrete);
    } catch (ReflectionException $e) {
        throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
    }

    // If the type is not instantiable, the developer is attempting to resolve
    // an abstract type such as an Interface or Abstract Class and there is
    // no binding registered for the abstractions so we need to bail out.
    if (! $reflector->isInstantiable()) {
        return $this->notInstantiable($concrete);
    }

    $constructor = $reflector->getConstructor();

    // If there are no constructors, that means there are no dependencies then
    // we can just resolve the instances of the objects right away, without
    // resolving any other types or dependencies out of these containers.
    if (is_null($constructor)) {
        array_pop($this->buildStack);

        return new $concrete;
    }

    $dependencies = $constructor->getParameters();

    // Once we have all the constructor's parameters we can create each of the
    // dependency instances and then use the reflection instances to make a
    // new instance of this class, injecting the created dependencies in.
    try {
        $instances = $this->resolveDependencies($dependencies);
    } catch (BindingResolutionException $e) {
        array_pop($this->buildStack);

        throw $e;
    }

    array_pop($this->buildStack);

    return $reflector->newInstanceArgs($instances);
}

Они используют один и тот же метод для получения всей информации о конкретном классе, который должен быть создан (в нашем примере PostgresUser), и извлечения определенных зависимостей. Поскольку это инверсия управления, сначала требуется разрешить зависимости, чтобы иметь все необходимое для создания экземпляра PostgresUser. Я хотел бы сделать это короче, так как контейнер Laravel действительно продвинут, поэтому то, как они решают зависимости, в основном связано с итерацией через весь массив $dependencies(который является массивом ReflectionParameter). Каждая зависимость имеет свой typeHint который также может быть передан через тот же поток, который мы объясняем сейчас (в основном цикл, который проходит через каждую зависимость и повторяет все, что мы объясняем сейчас, проверяет, есть ли интерфейс, получает конкретный класс, пытается построить его).

Шаг третий: создание экземпляра конкретного класса

После того, как все $dependencies разрешены, у нас есть новая переменная $instances, которая называется массивом экземпляров, взятых из $dependencies. В нашем случае это массив, содержащий экземпляр класса App\Models\User.

Теперь мы готовы создать новый экземпляр класса. Метод newInstanceArgs принимает массив в качестве аргумента и будет использовать этот массив для создания нового экземпляра reflected класса. Это черный ящик для нас, так как он является родной частью PHP, но я бы предположил, что за кулисами происходит что-то вроде new PostgresUser(...$instances);, если мы переведем это в пример PHP (конечно, я слишком упростил это, там должно быть больше, чем просто одна строка). Подробнее об операторе splat вы можете прочитать здесь.

После этого шага мы создали интерфейс PostgresUser, используя интерфейс UserRepository. Это означает, что если нам в какой-то момент времени нужно изменить реализацию для UserRepository и мы решим использовать MongoDbUser вместо PostgresUser, мы можем сделать это, не меняя CreateNewUserAction класс вообще, так как он использует абстракцию для зависимости, а не конкретный класс. Единственные изменения , которые необходимо сделать, – это заменить привязку в AppServiceProvider классе и, конечно же, создать новый MongoDbUser класс, реализующий UserRepository интерфейс.

На этом этапе мы знаем, как резолвятся интерфейсы и конкретные классы с помощью конструктора. Таким образом, просто чтобы сделать краткое резюме, для того, чтобы маршрутизатор вызывал метод register из App\Http\Controllers\RegisterController класса, контейнер IoC, как объяснено выше, будет:

  1. Разрешит зависимости для RegisterController
  2. Разрешит зависимости для CreateNewUserAction
  3. Создаст новый CreateNewUserAction экземпляр с разрешенными зависимостями
  4. Создаст новый RegisterController экземпляр с разрешенными зависимостями (в данном случае CreateNewUserAction)
  5. Разрешит зависимости для RegisterController::register
  6. Вызовит RegisterController::register с разрешенными зависимостями

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

Шаг четвертый: вызов метода register

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

Точно так же, как для класса мы можем получить всю необходимую информацию, мы можем сделать то же самое для методов. Есть два способа сделать это, и оба они возвращают экземпляр ReflectionMethod

<?php

use App\Http\Controllers\RegisterController;

// 1. Getting the information using the class that has the method
$controllerReflection = new ReflectionClass(RegisterController::class);
$methodReflection = $controllerReflection->getMethod('register');

// 2. Direct instance of the ReflectionModel
$methodReflection = new ReflectionMethod(RegisterController::class, 'register');

// Result is the same for both examples
ReflectionMethod {#3337
   +name: "register",
   +class: "App\Http\Controllers\RegisterController",
   returnType: "App\Models\User",
   parameters: {
     $request: ReflectionParameter {#3384
       +name: "request",
       position: 0,
       typeHint: "App\Http\Requests\User\CreateNewUserRequest",
     },
     $action: ReflectionParameter {#3245
       +name: "action",
       position: 1,
       typeHint: "App\Actions\User\CreateNewUserAction",
     },
     $responseFactory: ReflectionParameter {#3339
       +name: "responseFactory",
       position: 2,
       typeHint: "Illuminate\Contracts\Routing\ResponseFactory",
     },
   },
   modifiers: "public",
}

На данный момент у нас есть вся необходимая информация. Если мы придерживаемся примера с маршрутизатором, вызывающим действие контроллера, то маршрутизатор теперь может легко вызвать метод действия контроллера (в данном случае register). У нас есть экземпляр controller, и у нас есть аргументы метода register.

PSR-11: Container interface

Стоит также отметить, что в этом примере я использовал сервисный контейнер Laravel только для того, чтобы привести пример, так как у меня есть большой опыт использования фреймворка Laravel (начиная с 4.1), поэтому я чувствую себя комфортно, объясняя это. Поскольку наличие IoC действительно распространено в большинстве фреймворков в наши дни, PHP Framework Interop Group (PHP-FIG) пытается создать общие стандарты между фреймворками и библиотеками, чтобы обеспечить повторное использование различных фреймворков и библиотек. Для контейнера IoC одна из рекомендаций стандартов PHP посвящена ContainerInterface (вы можете изучить интерфейс PSR-11: Container).

Основная цель этого PSR состоит в том, чтобы позволить одному фреймворку воспользоваться преимуществами контейнера IoC по личному выбору и ссылкам. Даже сервисный контейнер Laravel реализует этот стандарт, где у них есть свой собственный интерфейс контейнера (Illuminate\Contracts\Container\Container), который расширяет определенный PSR-11 интерфейс.

<?php

namespace Illuminate\Contracts\Container;

use Closure;
use Psr\Container\ContainerInterface;

interface Container extends ContainerInterface // The ContainerInterface is from the PSR-11 package
{
    // Hidden methods
}

И реализованные методы в реальной конкретной реализации Laravel Illuminate\Container\Container

<?php

namespace Illuminate\Container;

use Illuminate\Contracts\Container\Container as ContainerContract;

class Container implements ArrayAccess, ContainerContract
{
    
    /**
     *  {@inheritdoc}
     */
    public function get($id)
    {
        try {
            return $this->resolve($id);
        } catch (Exception $e) {
            if ($this->has($id)) {
                throw $e;
            }

            throw new EntryNotFoundException($id, $e->getCode(), $e);
        }
    }
    
    // Rest of the methods are hidden for simplicity
    // This is just to illustrate that laravel's container is following PSR-11
    
    /**
     *  {@inheritdoc}
     */
    public function has($id)
    {
        return $this->bound($id);
    }
}

PHP-DI

Давайте также рассмотрим еще один популярный контейнер IoC PHP-DI и его реализацию

<?php

declare(strict_types=1);

namespace DI;

use Invoker\InvokerInterface;
use Psr\Container\ContainerInterface;

/**
 * Dependency Injection Container.
 *
 * @api
 *
 * @author Matthieu Napoli <matthieu@mnapoli.fr>
 */
class Container implements ContainerInterface, FactoryInterface, InvokerInterface
{
    
    /**
     * Returns an entry of the container by its name.
     *
     * @param string $name Entry name or a class name.
     *
     * @throws DependencyException Error while resolving the entry.
     * @throws NotFoundException No entry found for the given name.
     * @return mixed
     */
    public function get($name)
    {
        // If the entry is already resolved we return it
        if (isset($this->resolvedEntries[$name]) || array_key_exists($name, $this->resolvedEntries)) {
            return $this->resolvedEntries[$name];
        }

        $definition = $this->getDefinition($name);
        if (! $definition) {
            throw new NotFoundException("No entry or class found for '$name'");
        }

        $value = $this->resolveDefinition($definition);

        $this->resolvedEntries[$name] = $value;

        return $value;
    }
    
    // Rest of the methods and properties are hidden due to simplicity
    // This is just to show how the PHP-DI Container is using PSR-11
    
    
    /**
     * Test if the container can provide something for the given name.
     *
     * @param string $name Entry name or a class name.
     *
     * @throws InvalidArgumentException The name parameter must be of type string.
     * @return bool
     */
    public function has($name)
    {
        if (! is_string($name)) {
            throw new InvalidArgumentException(sprintf(
                'The name parameter must be of type string, %s given',
                is_object($name) ? get_class($name) : gettype($name)
            ));
        }

        if (array_key_exists($name, $this->resolvedEntries)) {
            return true;
        }

        $definition = $this->getDefinition($name);
        if ($definition === null) {
            return false;
        }

        return $this->definitionResolver->isResolvable($definition);
    }
}

Есть много других контейнеров IoC, которые популярны и широко используются. Это всего лишь два примера, которые я использовал, чтобы объяснить использование контейнеров IoC. Их цель одна и та же: подготовка, управление и внедрение зависимостей приложений. реализации различаются.

Итог

Контейнер IoC, контейнер DI или сервисный контейнер-как бы вы их ни называли, все они относятся к одному и тому же. На мой взгляд, это самый точный термин DI Container, поскольку он относится к тому, что именно является целью контейнера. Ни в одном из этих контейнеров нет магии (или любой другой функции в целом, на мой взгляд), только много часов и усилий, чтобы дать это ощущение, так как он действительно прост и удобен в использовании. И все эти реализации в основном основаны на Reflection*классах PHP, которые содержат всю информацию о классах, интерфейсах, методах и т. д.

Оригинал: Stefan Brankovikj

Перевод: Максим Гречушников

Рейтинг
( 7 оценок, среднее 5 из 5 )
Maxyc Webber/ автор статьи
Мне 35 лет. Опыт профессиональной разработки 15 лет. Занимаюсь разработкой и поддержкой корпоративных систем автоматизации бизнеса, а также высоконагруженными проектами. Мне нравится решать нестандартные проблемы бизнеса. Имею опыт формирования команд под проект, налаживания процесса разработки, коммуникации программистов и заказчиков. Есть опыт работы с зарубежными заказчиками (ОАЭ, Польша, Германия, Швейцария).
Понравилась статья? Поделиться с друзьями:
Комментарии: 1
  1. Heyder

    Spasibo. Ochen polezniy material :idea:

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

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.