Механизм 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 в своем функционале, например:
- Менеджер зависимостей Composer
- Статические анализаторы (PHPStan, Psalm)
- Библиотеки для тестирования (PHPUnit, Codeception)
- ORM-библиотеки (Doctrine)
- Фреймворки и микрофреймворки
В PHP с помощью Reflection API вы можете:
- Изучать структуру классов, интерфейсов, трейтов, методов и свойств
- Получать метаданные, такие как имена, модификаторы доступа, типы параметров и возвращаемых значений, а также doc-блоки комментариев
- Динамически создавать экземпляры классов и вызывать методы
Приведу несколько простых примеров для понимания возможностей модуля 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. Общий алгоритм работы
- Нормализация имени через алиасы
- Проверка контекстной привязки
- Проверка кэша синглтонов
- Определение конкретной реализации
- Построение объекта или рекурсия
- Применение расширений и колбэков
- Кэширование и возврат результата
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. Общий алгоритм работы
- Проверка замыкания
- Анализ класса через рефлексию
- Контроль циклических зависимостей (рекурсия)
- Определение конструктора класса и его зависимостей
- Создание объекта
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) контейнер сделает следующие рекурсивные вызовы:
build(C::class)– требуетсяBbuild(B::class)– требуетсяAbuild(A::class)– конструктора нет→возвращает новыйA
Проблема: циклические ссылки, например A → B → A.
Рекурсивный алгоритм ломается, если зависимости образуют цикл:
class A { public function __construct(B $b) { /* ... */ } }
class B { public function __construct(A $a) { /* ... */ } }
При вызове app()->make(A::class)
build(A::class)– требуетсяBbuild(B::class)– требуетсяA- ... бесконечная рекурсия
Решение: ручной контроль зависимостей в коде.
В 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. Источники и ссылки
- Рефлексия в программировании – Википедия
- Procedural reflection in programming languages – MIT Libraries
- Модуль интроспекции кода Reflection – Руководство по PHP
- Service Container – Laravel Docs
- Service Container: Contextual Binding – Laravel Docs
- 5.8/src/Illuminate/Container – Laravel GitHub репозиторий
- PSR-11 – PHP-FIG