Сервис-контейнер в Laravel: как устроено внутри
Что происходит, когда вы вызываете app()->make() или пишете зависимость в конструкторе контроллера. Разбираем механизм резолвинга, привязки и рефлексии в Laravel 11.x.

Вы добавляете Illuminate\Http\Request $request в конструктор контроллера — и объект приходит сам. Пишете bind() в сервис-провайдере — и по всему приложению начинает подставляться нужная реализация интерфейса. Всё это делает один класс: Illuminate\Container\Container. Разберём, как он превращает имя класса в готовый объект, где хранит привязки и что происходит при рекурсивном разрешении зависимостей. Версия — Laravel 11.x, класс контейнера живёт в пакете illuminate/container.
Что вообще резолвит контейнер
Контейнер — это реестр «как построить объект по его имени». Имя — обычно строка с полным именем класса или интерфейса, иногда произвольный алиас. На запрос make('App\Services\Report') контейнер отдаёт экземпляр. Разница с обычным new в том, что контейнер сам заглядывает в конструктор, находит там другие зависимости и строит их тоже — рекурсивно, до самого низа графа.
Есть три способа задать, как строить объект:
- Без привязки вообще. Если класс не абстрактный и конструктор состоит из типизированных зависимостей, контейнер построит его через рефлексию, ничего не спрашивая.
- Привязка через
bind(). Вы говорите: для этого имени вызывай вот эту замыкание-фабрику или строй вот этот конкретный класс. Каждыйmake()даёт новый экземпляр. - Синглтон через
singleton(). То же, но результат кешируется: второйmake()отдаёт тот же объект.
Где всё это хранится
Внутри контейнера несколько массивов-реестров. Основные, за которые стоит держаться при отладке:
$bindings— карта «имя → [фабрика, флаг shared]». Сюда пишутbind()иsingleton().$instances— уже собранные синглтоны и всё, что положили черезinstance().$aliases— карта коротких имён на реальные.$contextual— контекстные привязки: «когда класс A просит интерфейс B, дай ему реализацию C».
Когда вы зовёте app() без аргументов, вы получаете сам контейнер — в Laravel это объект Application, наследник Container.
Что происходит при make()
Публичный make() — тонкая обёртка над защищённым resolve(). Именно resolve() содержит логику. Схематично порядок такой:
- Имя прогоняется через
getAlias()— если это алиас, он разворачивается в реальное имя. - Если для этого имени есть готовый экземпляр в
$instancesи запрос без параметров — он возвращается сразу. - Иначе контейнер вычисляет «конкретную реализацию»: смотрит в
$bindings. Нашёл замыкание — вызовет его. Не нашёл — считает, что имя и есть искомый класс, и пойдёт строить его сам. - Если реализацию можно построить (
isBuildable()), вызываетсяbuild(). Здесь и начинается рефлексия. - Применяются extenders (
extend()), объект помечается разрешённым, срабатывают колбэкиresolving()/afterResolving(). - Если привязка была shared — результат кладётся в
$instances.
Как build() читает конструктор
Метод build() берёт имя класса и создаёт ReflectionClass. Дальше три ветки:
- Класс не инстанцируется (интерфейс без привязки, абстрактный класс) — летит
BindingResolutionExceptionс текстом вродеTarget [...] is not instantiable. - У класса нет конструктора — просто
new $concrete. - Конструктор есть — контейнер берёт его параметры и для каждого решает, что подставить.
Решение по каждому параметру принимает resolveDependencies(). Логика по одному параметру:
- Есть ли значение в переданных вручную параметрах (
make($class, ['foo' => ...])) — берём его. - Параметр типизирован именем класса или интерфейса — контейнер рекурсивно зовёт
make()для этого типа. - Тип — скаляр (int, string) без значения по умолчанию — контейнер не умеет его придумать, летит исключение, что зависимость неразрешима.
- Есть значение по умолчанию — берётся оно.
Контейнер не гадает про скаляры. Если в конструкторе сервиса стоит int $timeout без значения по умолчанию и без привязки — вы получите ошибку резолвинга, а не молчаливый ноль. Это частая причина «почему у меня не создаётся класс».
Контекстные привязки: одному классу — одна реализация, другому — другая
Обычный bind(LoggerInterface::class, FileLogger::class) действует глобально. Но иногда один потребитель должен получать другую реализацию. За это отвечает реестр $contextual и синтаксис:
// В сервис-провайдере, Laravel 11.x
use App\Reports\PdfReport;
use App\Reports\HtmlReport;
use App\Contracts\Report;
use App\Http\Controllers\ExportController;
use App\Http\Controllers\PreviewController;
$this->app->when(ExportController::class)
->needs(Report::class)
->give(PdfReport::class);
$this->app->when(PreviewController::class)
->needs(Report::class)
->give(HtmlReport::class);
Когда контейнер строит ExportController и доходит до параметра типа Report, он сначала проверяет контекст текущей сборки — стек buildStack хранит, что мы сейчас внутри именно этого контроллера, — и берёт контекстную привязку вместо глобальной.
Автоматическое разрешение и его цена
Магия «просто типизируй конструктор» стоит вызовов рефлексии. На каждый неизвестный класс контейнер создаёт ReflectionClass, перебирает параметры конструктора, читает их типы. В рамках одного HTTP-запроса это редко заметно: граф зависимостей контроллера — единицы, максимум десятки классов.
Проблема возникает в другом месте: где вы в цикле руками зовёте make() на тысячи итераций и каждый раз строите новый объект через рефлексию. Тогда стоит либо привязать сервис синглтоном, либо вынести создание из цикла.
Как измерить, сколько времени уходит на резолвинг: не выдумывайте цифры под своё железо — снимите их сами. Laravel Telescope пишет запись на каждое разрешение из контейнера при включённом watcher, а более точную картину по времени даст профилировщик — Xdebug с флеймграфом или Blackfire. Смотрите на суммарное время внутри Container::build и на число вызовов.
Синглтон против bind по цене
| Способ | Экземпляр на make() | Рефлексия | Когда брать |
|---|---|---|---|
singleton() | один на весь запрос | один раз | сервис без состояния запроса: клиент API, репозиторий |
bind() | новый каждый раз | на каждый make() | объект с состоянием, который нельзя переиспользовать |
instance() | тот, что положили | нет | уже готовый объект, построенный вручную |
| без привязки | новый каждый раз | на каждый make() | простой класс с зависимостями, редкий вызов |
Где механизм ломается
Граничные случаи, о которые спотыкаются даже опытные:
- Циклическая зависимость. A требует B, B требует A. Контейнер не детектирует цикл красиво — вы получите бесконечную рекурсию и падение по памяти или лимиту вложенности. Лечится разрывом связи: инъекция через сеттер, ленивое разрешение через замыкание или
Illuminate\Contracts\Container\Containerв конструкторе с ручнымmake()в нужный момент. - Синглтон, держащий состояние запроса. Классическая беда в очередях и Octane: положили в синглтон объект, который хранит текущего пользователя или Request, и в следующем запросе он всё ещё там. При работе с
artisan queue:workи Octane это утечка данных между задачами. - Скалярные зависимости без привязки. Уже упоминали:
string $apiKeyв конструкторе не разрешится сам. Нужна контекстная привязкаgiveConfig()либо фабрика черезbind(). - Разрешение по интерфейсу без привязки. Типизировали параметр интерфейсом, но не сделали
bind()— контейнер не может инстанцировать интерфейс и падает сis not instantiable.
Что смотреть дальше
Если хотите разобрать резолвинг руками — включите Telescope и постройте пару контроллеров, наблюдая записи Container watcher. Дальше стоит прочитать исходник Illuminate\Container\Container: методы resolve(), build() и resolveDependencies() занимают меньше сотни строк и читаются за один вечер. Отдельная большая тема — как сервис-провайдеры регистрируют привязки в фазе register() и почему нельзя резолвить сервисы в register() до того, как все провайдеры отработали; здесь мы её только обозначаем. В проде следите за составом синглтонов при переходе на Octane — это первое, что там ломается.
Репетитор 1—11 класс и ВУЗ: Математика и физика методом наведения на мысль, помощь с ДЗ.
Спросить за 8 ₽Источники: Laravel — Service Container (официальная документация), illuminate/container на GitHub
Частые вопросы
У меня Laravel 10, а не 11 — механизм тот же?
Почему класс не создаётся, хотя я его нигде не привязывал?
Когда синглтон опасен?
Дорого ли обходится автоматический резолвинг через рефлексию?
Как понять, что именно резолвит контейнер во время запроса?
Что делать с циклической зависимостью?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.