Механизм Dependency Injection в Laravel

1. Введение

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

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

Если ответить максимально кратко – благодаря Reflection API. Если есть желание понять, как Laravel создает класс и определяет параметры его конструктора, предлагаю прочитать эту статью.

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

2. Что такое рефлексия?

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

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

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

2.1. Модуль Reflection в PHP

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

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

В отличие от других расширений, Reflection является частью ядра PHP и его не нужно настраивать.

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

В PHP с помощью Reflection API вы можете:

Приведу несколько простых примеров для понимания возможностей модуля Reflection.

2.1.1. Reflection в консоли

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

В меню help (в самом конце) есть несколько встроенных функций:

$ php -h    
...
--rf [name]      Show information about function [name].
--rc [name]      Show information about class [name].
--re [name]      Show information about extension [name].
--rz [name]      Show information about Zend extension [name].
--ri [name]      Show configuration for extension [name].

Пример работы --rf для просмотра функции:

$ php --rf strlen

Function [ internal:Core function strlen ] {
  - Parameters [1] {
    Parameter #0 [ required $str ]
  }
}

2.1.2. Reflection в коде

Для более глубокого анализа используются классы, которые начинаются с префикса Reflection.

use ReflectionClass;

class TestService
{
    public function __construct(TestRepository $repository) { /* ... */ }
}

$reflection = new ReflectionClass(TestService::class);
$constructor = $reflection->getConstructor();

if ($constructor) {
    var_dump($constructor->getParameters());
    // array(1) {
    //   [0]=>
    //   object(ReflectionParameter)#3 (1) {
    //     ["name"]=>
    //     string(10) "repository"
    //   }
    // }
}

Также в коде можно использовать функции интроспекции. Но это не Reflection в полном смысле, а простые функции-хелперы, которые дают поверхностную информацию.

class Test {
    function __construct() { /* ... */ }
    function test() { /* ... */ }
}

// Получение имени класса
$class = new Test();
var_dump(get_class($class));
// string(4) "Test"

// Проверка метода класса
var_dump(method_exists('Test', 'test'));
// bool(true)

Сам же модуль Reflection имеет мощный функционал и сервис-контейнер Laravel в нашем случае активно использует его в своей основе.

3. От точки входа до сервис-контейнера

В Laravel, прежде всего, важно понимать что представляет из себя класс Illuminate\Foundation\Application и где он начинает работать.

Если мы откроем файл public/index.php, то увидим что, после vendor/autoload.php сразу подключается bootstrap/app.php.

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

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

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

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

Application – это центральный объект Laravel, через который запускаются все его компоненты. Важно заметить, что класс Application наследует класс Illuminate\Container\Container:

class Application extends Container implements ApplicationContract, HttpKernelInterface

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

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

Сервис-контейнер в Laravel реализует стандарт PSR-11 и добавляет собственную функциональность.

4. Как сервис-контейнер создает объект

Далее сосредоточим внимание на логике работы класса Container и посмотрим, как происходит определение класса, разрешение зависимостей конструктора и создание готового объекта.

4.1. Связь app и Container::make

Предположим, у нас есть UserRepository с параметром модели User в конструкторе.

namespace App\Repositories\UserRepository;

use App\Models\User;

class UserRepository
{
    private $model;

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

При передаче нашего класса в хелпер app() сервис-контейнер сам определит его зависимости и подготовит для нас объект.

dd(app(\App\Repositories\UserRepository::class));
// App\Repositories\UserRepository {#239 ▼
//   -model: App\Models\User {#240 ▶}
// }

Хелпер app() внутри себя возвращает класс Container и вызывает метод make() для реализации объекта.

if (! function_exists('app')) {
    function app($abstract = null, array $parameters = [])
    {
        if (is_null($abstract)) {
            return Container::getInstance();
        }

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

Также есть хелпер resolve(), который является оберткой над app().

if (! function_exists('resolve')) {
    function resolve($name, array $parameters = [])
    {
        return app($name, $parameters);
    }
}

4.2. Логика Container::make

Метод make() служит главным инструментом для создания объектов через сервис-контейнер. Его реализация разделена между классами Application и Container, что добавляет в процесс шаги, характерные для работы всего приложения. Но основная логика по реализации классов и разрешению зависимостей находится в классе Container.

public function make($abstract, array $parameters = [])
{
    return $this->resolve($abstract, $parameters);
}

Этот метод – тонкая надстройка: он напрямую вызывает внутренний метод resolve, в котором и происходит вся магия.

4.3. Логика Container::resolve

Метод resolve() – это центральный метод сервис-контейнера. Реализация класса в нем строится на основе входящих параметров и определения последующих правил жизненного цикла класса.

4.3.1. Общий алгоритм работы

  1. Нормализация имени через алиасы
  2. Проверка контекстной привязки
  3. Проверка кэша синглтонов
  4. Определение конкретной реализации
  5. Построение объекта или рекурсия
  6. Применение расширений и колбэков
  7. Кэширование и возврат результата
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
    // 1. Нормализация имени через алиасы
    $abstract = $this->getAlias($abstract);

    // 2. Проверка контекстной привязки
    $needsContextualBuild = ! empty($parameters) || ! is_null(
        $this->getContextualConcrete($abstract)
    );

    // 3. Проверка кэша синглтонов
    if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
        return $this->instances[$abstract];
    }

    $this->with[] = $parameters;

    // 4. Определение конкретной реализации
    $concrete = $this->getConcrete($abstract);

    // 5. Построение объекта или рекурсия.
    if ($this->isBuildable($concrete, $abstract)) {
        $object = $this->build($concrete);
    } else {
        $object = $this->make($concrete);
    }

    // 6. Применение расширений и колбэков
    foreach ($this->getExtenders($abstract) as $extender) {
        $object = $extender($object, $this);
    }

    if ($this->isShared($abstract) && ! $needsContextualBuild) {
        $this->instances[$abstract] = $object;
    }

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

    // 7. Кэширование и возврат результата
    $this->resolved[$abstract] = true;
    array_pop($this->with);

    return $object;
}

4.3.2. Алиасы (Alias)

Через метод getAlias() производится нормализация имени класса, что позволяет обращаться к одному и тому же классу под разными именами. Laravel уже регистрирует в config/app.php свои алиасы для фасадов. Мы можем его дополнить или создать свои через сервис-провайдер.

// Глобальный сервис настроек
class SettingsService { /* ... */ }

// Создаем алиас
$this->app->alias(SettingsService::class, 'settings.service');

4.3.3. Контекстные привязки (Contextual Binding)

Через метод getContextualConcrete() определяется наличие контекстной привязки. Этот механизм позволяет указывать разную реализацию для одного и того же абстрактного типа (например, интерфейса) в зависимости от внедряемого класса.

// Интерфейс и две реализации
interface Logger { /* ... */ }
class FileLogger implements Logger { /* ... */ }
class RedisLogger implements Logger { /* ... */ }

// Класс, зависящий от Logger
class UserController {
    public function __construct(Logger $logger) { /* ... */ }
}

// Контекстная привязка: для UserController используем FileLogger,
// для всех остальных – RedisLogger
$this->app->when(UserController::class)
	->needs(Logger::class)
	->give(FileLogger::class);

$this->app->bind(Logger::class, RedisLogger::class);

// При разрешении UserController сервис-контейнер увидит контекстную привязку
// и создаст FileLogger (а не RedisLogger)

4.3.4. Синглтон (Singleton)

Основная часть сервисов Laravel идет как синглтон (лог, роутер, кеш, БД, очередь и т.д.), а также ядра приложения и обработчик исключений. При необходимости можно создавать свои синглтоны, работая с одним и тем же объектом. Такой объект будет кешироваться в параметре сервис-контейнера instances.

// С замыканием — кастомная логика создания
$this->app->singleton(HttpClient::class, function ($app) {
    return new HttpClient(config('http.timeout'));
});

// Без замыкания — контейнер просто запомнит класс для вызова
$this->app->singleton(HttpClient::class);

4.4. Логика Container::build

Метод build() – это сердце сервис-контейнера Laravel, который использует непосредственно рефлексию. Он отвечает за создание экземпляра конкретного класса (не интерфейса) с автоматическим внедрением его зависимостей через конструктор.

4.4.1. Общий алгоритм работы

  1. Проверка замыкания
  2. Анализ класса через рефлексию
  3. Контроль циклических зависимостей (рекурсия)
  4. Определение конструктора класса и его зависимостей
  5. Создание объекта
public function build($concrete)
{
    // 1. Проверка замыкания
    if ($concrete instanceof Closure) {
        return $concrete($this, $this->getLastParameterOverride());
    }

    // 2. Анализ класса через рефлексию
    $reflector = new ReflectionClass($concrete);

    if (! $reflector->isInstantiable()) {
        return $this->notInstantiable($concrete);
    }

    // 3. Контроль циклических зависимостей (рекурсия)
    $this->buildStack[] = $concrete;

    // 4. Определение конструктора класса и его зависимостей
    $constructor = $reflector->getConstructor();

    if (is_null($constructor)) {
        array_pop($this->buildStack);

        return new $concrete;
    }

    $dependencies = $constructor->getParameters();

    try {
        $instances = $this->resolveDependencies($dependencies);
    } catch (BindingResolutionException $e) {
        array_pop($this->buildStack);

        throw $e;
    }

    array_pop($this->buildStack);

    // 5. Создание объекта
    return $reflector->newInstanceArgs($instances);
}

4.4.2. Использование рефлексии

Сервис-контейнер использует встроенные методы ReflectionClass для анализа класса и создания объекта.

// Получает информацию о классе
$reflector = new ReflectionClass($concrete);

// Проверяет возможность инициализации
if (! $reflector->isInstantiable()) {
	return $this->notInstantiable($concrete);
}

// Получает конструктор и его параметры
$constructor = $reflector->getConstructor();
$dependencies = $constructor->getParameters();

// Разрешает зависимости конструктора
$instances = $this->resolveDependencies($dependencies);

// Возвращает объект, передавая в конструктор уже подготовленные зависимости
return $reflector->newInstanceArgs($instances);

4.4.3. Разрешение зависимостей: классы и примитивы

Метод resolveDependencies() пробегает по всем параметрам конструктора и для каждого вызывает resolvePrimitive() для примитивов или resolveClass() для типизированных зависимостей (классов, интерфейсов).

protected function resolveDependencies(array $dependencies)
{
    $results = [];

    foreach ($dependencies as $dependency) {
        if ($this->hasParameterOverride($dependency)) {
            $results[] = $this->getParameterOverride($dependency);

            continue;
        }

        $results[] = is_null($dependency->getClass())
                        ? $this->resolvePrimitive($dependency)
                        : $this->resolveClass($dependency);
    }

    return $results;
}

4.4.4. Использование рекурсии

При разрешении зависимостей сервис-контейнер вызывает сам себя, например С → B → A.

class A { /* ... */ }
class B { public function __construct(A $a) { /* ... */ } }
class C { public function __construct(B $b) { /* ... */ } }

При вызове app()->make(C::class) контейнер сделает следующие рекурсивные вызовы:

  1. build(C::class) – требуется B
  2. build(B::class) – требуется A
  3. build(A::class) – конструктора нет возвращает новый A

Проблема: циклические ссылки, например A → B → A.

Рекурсивный алгоритм ломается, если зависимости образуют цикл:

class A { public function __construct(B $b) { /* ... */ } }
class B { public function __construct(A $a) { /* ... */ } }

При вызове app()->make(A::class)

  1. build(A::class) – требуется B
  2. build(B::class) – требуется A
  3. ... бесконечная рекурсия

Решение: ручной контроль зависимостей в коде.

В Laravel для этого случая не предусмотрена блокировка, а параметр buildStack просто собирает массив зависимостей, что можно увидеть при дебаге вызова app()->make(A::class).

$this->buildStack[] = $concrete;

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

5. Заключение

Теперь мы увидели, каким образом Laravel использует DI в своей основе и что происходит внутри сервис-контейнера.

Логика работы весьма обширна и я лишь показал пример рефлексии для реализации класса с параметрами конструктора. Есть еще логика обработки маршрутов, в частности, методов контроллера:

// app/Http/Controllers/NewsController.php
namespace App\Http\Controllers;

use App\Repositories\NewsRepository;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class NewsController extends Controller
{
    public function index(
        Request $request,
        NewsRepository $newsRepository
    ): Response { /* ... */ }
}

// routes/web.php
Route::get('/news', 'NewsController@index');

Но это выходит за рамки моей статьи. Кому интересно, можете заглянуть в сторону Illuminate\Support\Facades\Route и Illuminate\Container\BoundMethod у сервис-контейнера.

Напоследок повторюсь: рефлексия – достаточно мощный инструмент и имеет свои сильные и слабые стороны.

Работая с массовым созданием классов, подобно сервис-контейнеру Laravel, важно помнить о производительности. Сама рефлексия в этом случае может работать медленней прямого вызова new ClassName. Laravel активно использует кеширование конфигов, маршрутов и прочих элементов, чтобы не создавать каждый раз кучу объектов через рефлексию.

6. Источники и ссылки

  1. Рефлексия в программировании – Википедия
  2. Procedural reflection in programming languages – MIT Libraries
  3. Модуль интроспекции кода Reflection – Руководство по PHP
  4. Service Container – Laravel Docs
  5. Service Container: Contextual Binding – Laravel Docs
  6. 5.8/src/Illuminate/Container – Laravel GitHub репозиторий
  7. PSR-11 – PHP-FIG