2021-06-25

Dependency Injection под капотом Laravel


Многие слышали или читали про механизм Dependency Injection (DI), но мало кто имеет представление каким образом он реализован под капотом известного фреймворка.

Примеры кода на Laravel 5.8

В документации Laravel говорится, что Service Container - это мощный инструмент для управления зависимостями классов... В чем же заключается так называемая "мощь" и благодаря чему она достигнута. Если ответить максимально просто - благодаря Reflection API.

Reflection API

Концепция рефлексии не принадлежит конкретно PHP, она пришла из Computer Science и в языках программирования была введена Брайаном Смитом (в докторской диссертации 1982 года).[2]

Рефлексия - это процесс, во время которого программа может отслеживать и модифицировать собственную структуру и поведение во время выполнения.[1]

Вместе с рефлексией так же можно слышать термин "интроспекция" (т.е. наблюдение, исследование). Интроспекция является частью рефлексии.

В PHP есть модуль Reflection, который представляет API-интерфейс для интроспекции классов, функций и расширений.[3]

$ php -m
[PHP Modules]
...
Reflection

Многие библиотеки и фреймворки PHP используют Reflection API в своих целях, например:

Service Container

Laravel в нашем случае не исключение и его Service Container здесь будет хорошим примером.

В целях упрощения контекста часть логики в коде пропущена или не обсуждается.

Прежде всего важно понимать что представляет из себя класс Illuminate\Foundation\Application и где он начинает работать. Если мы откроем файл public/index.php, то увидим что после vendor/autoload.php сразу подключается bootstrap/app.php.

/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader for
| our application. We just need to utilize it! We'll simply require it
| into the script here so that we don't have to worry about manual
| loading any of our classes later on. It feels great to relax.
|
*/

require __DIR__.'/../vendor/autoload.php';

/*
|--------------------------------------------------------------------------
| Turn On The Lights
|--------------------------------------------------------------------------
|
| We need to illuminate PHP development, so let us turn on the lights.
| This bootstraps the framework and gets it ready for use, then it
| will load up this application so that we can run it and send
| the responses back to the browser and delight our users.
|
*/

$app = require_once __DIR__.'/../bootstrap/app.php';

В bootstrap/app.php создается класс Application.

/*
|--------------------------------------------------------------------------
| 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__)
);

По сути класс Application это и есть Laravel, через который запускаются все его компоненты. Далее важно заметить что класс Application наследует класс Container.

class Application extends Container implements ApplicationContract, HttpKernelInterface
{
    /**
     * The Laravel framework version.
     *
     * @var string
     */
    const VERSION = '5.8.38';

Illuminate\Foundation\Container в своей основе как раз и содержит всю логику по работе с зависимостями. Каждый раз перед вызовом класса, в конструкторе или методе которого есть параметры, Container занимается их разрешением.

Не смотря на то, что Taylor Otwell в Laravel использует компонетны Symfony, для механизма DI он не стал использовать готовые решения а написал свою реализацию. Файлы связанные с логикой Service Container находятся здесь:

/vendor/laravel/framework/src/Illuminate/Container/
BoundMethod.php
composer.json
Container.php
ContextualBindingBuilder.php
EntryNotFoundException.php
LICENSE.md
RewindableGenerator.php

Service Container в Laravel реализует стандарт PSR-11[5]

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

namespace App\Services;

use App\Pepositories\NewsRepository;

class NewsService
{
    private $repository;

    public function __construct(NewsRepository $repository)
    {
        $this->repository = $repository;
    }

    // ...
}

При вызове NewsService мы воспользуемся хелпером app(), чтобы Service Container сам определил наш класс и параметры его конструктора.

$newsService = app(\App\Services\NewsService::class);

app() внутри себя использует функционал класса Container. На первый взгляд кажется непонятным почему хелпер может вернуть \Illuminate\Contracts\Foundation\Application, это объясняется тем, что обращаясь к классу Container мы работаем с ним не напрямую а через объект Application, т.к. в конструкторе последнего регистрируется связь класса Container и объекта Application через метод instance.

Есть разница между вызовом singletone и instance.[4]

if (! function_exists('app')) {
    /**
     * Get the available container instance.
     *
     * @param  string|null  $abstract
     * @param  array   $parameters
     * @return mixed|\Illuminate\Contracts\Foundation\Application
     */
    function app($abstract = null, array $parameters = [])
    {
        if (is_null($abstract)) {
            return Container::getInstance();
        }

        return Container::getInstance()->make($abstract, $parameters);
    }
}

Далее мы сосредоточим внимание только над классом Container и разберем часть методов которые отрабатывают в большинстве ситуаций. 

В Container отработают следующие методы:

  1. make
  2. resolve
  3. getConcrete
  4. build
  5. resolveDependencies

Метод make примет в качетсве параметра $abstract адрес нашего класса App\Services\NewsService и передаст его в метод resolve, который определит что входящий класс конечный или это интерфейс или абстрактный класс. В нашем случае класс конечный, но если бы мы передали интерфей или абстрактный класс, то нам бы обязательно пришлось на уровне ServiceProvider связать нашу зависимость через вызов $this->app->bind чтоб Container смог его найти, иначе мы получим ошибку.

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

В методе resolve отработает getConcrete и build.

/**
 * 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);

    $needsContextualBuild = ! empty($parameters) || ! is_null(
        $this->getContextualConcrete($abstract)
    );

    // 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;

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

    // If we defined any extenders for this type, we'll need to spin through them
    // and apply them to the object being built. This allows for the extension
    // of services, such as changing configuration or decorating the object.
    foreach ($this->getExtenders($abstract) as $extender) {
        $object = $extender($object, $this);
    }

    // If the requested type is registered as a singleton we'll want to cache off
    // the instances in "memory" so we can return it later without creating an
    // entirely new instance of an object on each subsequent request for it.
    if ($this->isShared($abstract) && ! $needsContextualBuild) {
        $this->instances[$abstract] = $object;
    }

    if ($raiseEvents) {
        $this->fireResolvingCallbacks($abstract, $object);
    }

    // Before returning, we will also set the resolved flag to "true" and pop off
    // the parameter overrides for this build. After those two things are done
    // we will be ready to return back the fully constructed class instance.
    $this->resolved[$abstract] = true;

    array_pop($this->with);

    return $object;
}

getConcrete проверит наш класс на уровень абстракции и вернет его адрес. Тут можно обратить внимание на параметр $this->bindings, который хранит все зависимости (адреса) на момент регистрации классов через метод bind. Laravel это делает при каждом запуске.

/**
 * Get the concrete type for a given abstract.
 *
 * @param  string  $abstract
 * @return mixed   $concrete
 */
protected function getConcrete($abstract)
{
    if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
        return $concrete;
    }

    // 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;
}

build уже начнет использовать Reflection API. Это фундаментальный метод в котором и происходит интроспекция класса, определяется его конструктор и все входящие параметры.

/**
 * Instantiate a concrete instance of the given type.
 *
 * @param  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());
    }

    $reflector = new ReflectionClass($concrete);

    // 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->buildStack[] = $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);
}

В нашем случае NewsService имеет конструктор с параметром NewsRepository, значит он будет проверен на наличие зависимостей и также пропущен через Container.

/**
 * Resolve all of the dependencies from the ReflectionParameters.
 *
 * @param  array  $dependencies
 * @return array
 *
 * @throws \Illuminate\Contracts\Container\BindingResolutionException
 */
protected function resolveDependencies(array $dependencies)
{
    $results = [];

    foreach ($dependencies as $dependency) {
        // If this dependency has a override for this particular build we will use
        // that instead as the value. Otherwise, we will continue with this run
        // of resolutions and let reflection attempt to determine the result.
        if ($this->hasParameterOverride($dependency)) {
            $results[] = $this->getParameterOverride($dependency);

            continue;
        }

        // If the class is null, it means the dependency is a string or some other
        // primitive type which we can not resolve since it is not a class and
        // we will just bomb out with an error since we have no-where to go.
        $results[] = is_null($dependency->getClass())
                        ? $this->resolvePrimitive($dependency)
                        : $this->resolveClass($dependency);
    }

    return $results;
}

Зависимости constructor условно делятся на 2 типа: class, primitive.

В итоге, в переменную $newsService будет передан готовый объект и вся цепочка зависимостей классов будет автоматически разрешена.

На первый взгляд логика класса Container кажется сложной, но по мере ее понимания становится ясно как работает Service Container под капотом Laravel. Если кому-то этого покажется мало, то можно обратить внимание на механику работы Router и понять как Laravel определяет свои маршруты и обрабатывает входящие параметры для методов контроллера с помощью Reflection API.

Источники:

  1. Рефлексия в программировании
  2. MIT - Procedural reflection in programming languages
  3. PHP Reflection
  4. Laravel 5.8 Service Container
  5. PSR-11