Сервис-контейнер и провайдеры в Laravel: частые ошибки
Почему singleton, зарегистрированный не там, живёт дольше запроса, зачем boot отделён от register и как перестать плодить лишние провайдеры. Разбор типичных ошибок на Laravel 11.x.

Вы регистрируете сервис как singleton, локально всё работает, а в очереди воркер начинает отдавать данные прошлого задания. Или ловите ошибку «Target class [App\Services\Foo] does not exist», хотя класс на месте. Оба случая — про непонимание того, как сервис-контейнер разрешает зависимости и когда именно вызываются методы провайдера. Разберём это на Laravel 11.x: где чаще всего ошибаются, что именно ломается и как переписать правильно.
Речь только про регистрацию сервисов и жизненный цикл провайдеров. Deferred-провайдеры для очередей и события контейнера — смежные темы, их упомяну одной строкой, но разворачивать не буду.
Что вообще делает контейнер
Сервис-контейнер — это фабрика, которая знает, как собрать объект со всеми его зависимостями. Когда вы просите класс через конструктор контроллера или через app(), контейнер смотрит: есть ли явная привязка (binding) для этого имени. Если есть — вызывает замыкание из привязки. Если нет — пытается собрать класс сам через рефлексию конструктора.
Провайдер — место, где вы описываете эти привязки. У него два метода с разным смыслом:
- register — только регистрация привязок в контейнере. Ничего не резолвить, не трогать другие сервисы, не обращаться к конфигу через фасады, которые могут быть ещё не готовы.
- boot — вызывается после того, как отработали
registerвсех провайдеров. Здесь уже можно резолвить сервисы, вешать слушатели событий, регистрировать директивы Blade и макросы.
Половина ошибок с провайдерами — это игнорирование границы между этими двумя методами.
Термин: binding1
1 Binding — привязка имени (обычно интерфейса или строкового ключа) к способу создания объекта. Контейнер хранит замыкание-фабрику и вызывает его, когда кто-то запрашивает это имя.
Ошибка 1: логика в register вместо boot
Типичный код, который иногда работает, а иногда падает:
// НЕПРАВИЛЬНО: резолвим сервис прямо в register
public function register(): void
{
// Config-фасад может быть не готов, а Router — тем более
$this->app->make('router')->middleware('throttle');
}
В момент вызова register другие провайдеры могут быть ещё не зарегистрированы. Вы обращаетесь к сервису, привязку которого создаёт провайдер, чей register ещё не отработал. Отсюда плавающие «class does not exist» и «call to a member function on null».
Правильно — разнести:
// register: только описываем, как создать объект
public function register(): void
{
$this->app->singleton(PaymentGateway::class, function ($app) {
return new PaymentGateway(
$app['config']->get('services.payment.key')
);
});
}
// boot: здесь всё уже собрано, можно действовать
public function boot(): void
{
Blade::directive('money', fn ($expr) => "<?php echo money($expr); ?>");
}
Замыкание в singleton ленивое: код внутри выполнится только когда кто-то реально запросит PaymentGateway, а не в момент регистрации. Поэтому обращение к конфигу внутри замыкания безопасно.
Ошибка 2: singleton, который живёт дольше запроса
Это главная ловушка в очередях и в Octane. В обычном веб-запросе контейнер создаётся заново на каждый HTTP-запрос, поэтому singleton «живёт» ровно один запрос — разница с bind почти незаметна. А вот воркер очереди (queue:work) держит процесс живым и обрабатывает задания одно за другим в одном экземпляре приложения.
Если в singleton вы накапливаете состояние — оно перетечёт из одного задания в следующее.
// ОПАСНО в очереди: коллекция копится между заданиями
$this->app->singleton(ReportCollector::class);
// внутри задания
app(ReportCollector::class)->add($row);
// следующее задание получит тот же объект с чужими строками
Разница между способами привязки:
| Способ | Когда создаётся | Сколько живёт |
|---|---|---|
bind | при каждом резолве | отдельный экземпляр на каждый make |
singleton | при первом резолве | до конца жизни контейнера (запрос или весь процесс воркера) |
scoped | при первом резолве в рамках лайфсайкла | до сброса контейнера — в Octane пересоздаётся между запросами |
Правило простое: если сервис хранит состояние, привязанное к одному запросу или заданию, — не делайте его глобальным singleton. Для сервисов без состояния (клиент API, форматтер) singleton уместен и экономит создание объекта.
Singleton в Laravel — это не «один на всё приложение навсегда», а «один на время жизни этого контейнера». В веб-запросе это доли секунды, в воркере — часы. Пишите так, будто ваш сервис переживёт много запросов, потому что однажды он их переживёт.
Как поймать это заранее
Запустите обработку задания дважды подряд в одном воркере (php artisan queue:work --once дважды не поможет — процесс перезапускается; используйте обычный queue:work и киньте два задания в очередь). Если результаты второго задания «помнят» первое — у вас утечка состояния через singleton.
Ошибка 3: привязка к классу вместо интерфейса
Смысл контейнера — подменяемость. Если вы всюду тайп-хинтите конкретный класс, вы теряете возможность подменить его в тестах или сменить реализацию.
// Контракт
interface SmsSender
{
public function send(string $phone, string $text): void;
}
// Привязка в register
$this->app->bind(SmsSender::class, SmscSender::class);
Теперь в контроллере вы просите SmsSender, а в тесте подменяете его на заглушку через $this->app->instance(SmsSender::class, $fake) или $this->mock(...). Без интерфейса пришлось бы переписывать сигнатуры.
Ошибка 4: провайдер, который не нужен
Новички создают отдельный провайдер под каждый сервис. Это лишние файлы и лишний код, который выполняется на каждый запрос. Большинство привязок спокойно живёт в AppServiceProvider. Отдельный провайдер оправдан, когда:
- сервис можно отложить — тогда делаете deferred-провайдер (реализуете
DeferredProviderи методprovides()), и егоregisterне вызывается, пока сервис реально не понадобится; - это самостоятельный модуль или пакет со своей группой привязок, роутов и конфигов;
- логики регистрации так много, что она мусорит в
AppServiceProvider.
Если у провайдера пустой boot и одна строка в register — перенесите строку в AppServiceProvider и удалите файл.
Регистрация провайдера в Laravel 11
В Laravel 11 список провайдеров переехал: файла config/app.php с массивом providers по умолчанию нет. Провайдеры регистрируются в bootstrap/providers.php, а команда php artisan make:provider добавляет туда запись автоматически. Если вы копируете инструкцию под Laravel 10 или раньше — там провайдер добавляли в config/app.php. Проверьте версию перед тем, как искать нужный файл.
Ошибка 5: contextual binding вместо копипасты
Одному сервису нужна одна реализация, другому — другая. Люди дублируют классы вместо контекстной привязки:
$this->app->when(PhotoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('s3');
});
$this->app->when(DocumentController::class)
->needs(Filesystem::class)
->give(fn () => Storage::disk('local'));
Оба контроллера просят один интерфейс Filesystem, но получают разные диски. Без контекстной привязки пришлось бы городить два интерфейса-обёртки.
Если не получилось
- «Target class [...] does not exist» — контейнер не нашёл ни привязки, ни класса для сборки. Проверьте неймспейс и что класс есть, а если это интерфейс — что для него есть
bindв провайдере. - «Target [Interface] is not instantiable» — вы просите интерфейс, но привязки к конкретной реализации нет. Добавьте
bind(Interface::class, Impl::class). - Изменения в провайдере не подхватываются — если запускали
php artisan config:cacheили деплой кешировал конфиг и события, сбросьте:php artisan optimize:clear. - Данные «утекают» между заданиями очереди — ищите
singletonс накоплением состояния, см. ошибку 2. - Слушатель событий не срабатывает — регистрировали его в
registerвместоboot; вregisterсистема событий может быть ещё не поднята.
Что делать дальше
Пройдитесь по своим провайдерам с одним вопросом к каждой привязке: хранит ли этот сервис состояние и переживёт ли он воркер очереди. Всё, что накапливает данные, — либо bind, либо scoped. Затем проверьте, что в register нет обращений к другим сервисам, а вся такая логика ушла в boot. Дальше стоит почитать раздел документации Laravel про Container и Service Providers для вашей версии — там же описаны deferred-провайдеры и события резолва (resolving), которые полезны для профилирования того, что и когда создаётся.
Разбор научной статьи: Сложный текст — понятным языком, с терминами
Спросить за 12 ₽Источники: Laravel Documentation — Service Container, Laravel Documentation — Service Providers
Частые вопросы
У меня Laravel 10, а не 11 — где регистрировать провайдер?
Чем singleton отличается от bind, если в обычном запросе разницы почти нет?
Не сломается ли прод, если я перенесу код из register в boot?
Можно ли резолвить сервис через app() внутри register?
Как в тесте подменить сервис, привязанный в провайдере?
Что такое scoped-привязка и когда она нужна?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.