Service Container: глубокое погружение
Что это
Service Container Laravel — реализация PSR-11 и DI-контейнера.
Файл: Illuminate\Container\Container.
Три способа регистрации
bind
Новый объект каждый раз:
$this->app->bind(PaymentGateway::class, StripeGateway::class);
С замыканием:
$this->app->bind(PaymentGateway::class, function ($app) {
return new StripeGateway(
$app['config']['services.stripe.key'],
);
});
singleton
Один объект:
$this->app->singleton(Cache::class, RedisCache::class);
Замыкание:
$this->app->singleton(Logger::class, function ($app) {
return new FileLogger(storage_path('logs/app.log'));
});
instance
Готовый объект:
$config = new Config([...]);
$this->app->instance('config', $config);
Автоматическое разрешение
Laravel сам создаст:
class UserController
{
public function __construct(
private UserRepository $repo,
private Logger $logger,
) {}
}
Что делает контейнер:
- Смотрит на Reflection конструктора.
- Видит тип
UserRepository. - Ищет привязку — не находит.
- Создаёт
UserRepository(если это класс). - Если интерфейс — ошибка.
Правило: интерфейс нужно привязать.
Автозависимости с параметрами по умолчанию
class Report
{
public function __construct(
private int $limit = 100,
) {}
}
// Контейнер использует default
$report = app(Report::class);
Примитивы
Проблема: контейнер не знает, что передать для int:
class Service
{
public function __construct(
private int $apiKey, // не сработает
) {}
}
Решение — контекстная привязка:
$this->app->when(Service::class)
->needs('$apiKey')
->give(fn() => config('services.api.key'));
$apiKey — имя параметра.
Contextual Binding
Разные реализации в разных местах:
$this->app->when(ReportController::class)
->needs(Storage::class)
->give(LocalStorage::class);
$this->app->when(ApiController::class)
->needs(Storage::class)
->give(S3Storage::class);
Контроллер ReportController получит LocalStorage.
Tagging
Группировка сервисов:
$this->app->tag([
EmailNotifier::class,
SmsNotifier::class,
PushNotifier::class,
], 'notifiers');
Получение:
$notifiers = $this->app->tagged('notifiers');
Через атрибут:
use Illuminate\Container\Attributes\Tag;
final class NotifierChain
{
public function __construct(
#[Tag('notifiers')] iterable $notifiers,
) {}
}
Aliases
Псевдонимы:
$this->app->alias(Cache::class, 'cache');
$cache = $this->app->make('cache'); // то же
Extending
Декорирование:
$this->app->extend(Logger::class, function (Logger $logger, $app) {
return new LoggingDecorator($logger);
});
При следующем разрешении — обёрнутый.
Rebinding
Callback при перепривязке:
$this->app->rebinding('config', function ($app, $config) {
$app->make(Settings::class)->reload();
});
Проверка
if ($this->app->bound(Logger::class)) { }
if ($this->app->resolved(Logger::class)) { }
$abstract = Logger::class;
$this->app->getAlias($abstract);
$this->app->isShared($abstract);
Application extends Container
Laravel Application наследует Container:
class Application extends Container
{
// добавляет path, environment, version
}
Так что $app — и Application, и Container.
Замыкания с $app
В фабрике:
$this->app->bind(Report::class, function ($app) {
return new Report(
$app->make(Logger::class),
$app['config']['report.limit'],
);
});
$app — тот же контейнер.
Автозависимости в контроллерах
Method injection:
class UserController
{
public function store(
StoreUserRequest $request,
UserService $service, // автоматически
): Response { }
}
Контейнер создаёт UserService.
Кеширование
Laravel кеширует:
- Bindings.
- Aliases.
- Instances.
Не кешируется разрешение параметров.
Ошибки
BindingResolutionException:
Target [App\Contracts\PaymentGateway] is not instantiable.
Причины:
- Интерфейс не привязан.
- Класс абстрактный.
- Конструктор требует примитив.
Решение: bind в сервис-провайдере.
Циклические зависимости
A требует B, B требует A
Laravel кинет исключение.
Решение: разорвать через events, или позднее связывание.
Практика
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Contracts\Logger;
use App\Contracts\Cache;
use App\Services\FileLogger;
use App\Services\RedisCache;
use App\Services\DatabaseLogger;
use App\Http\Controllers\AdminController;
use App\Http\Controllers\ApiController;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Простая привязка
$this->app->bind(Logger::class, FileLogger::class);
// Singleton
$this->app->singleton(Cache::class, RedisCache::class);
// Фабрика с зависимостями
$this->app->bind('analytics', function ($app) {
return new \App\Services\Analytics(
$app->make(Logger::class),
$app['config']['analytics.key'],
);
});
// Contextual binding
$this->app->when(AdminController::class)
->needs(Logger::class)
->give(DatabaseLogger::class);
// Tag
$this->app->tag([
\App\Services\EmailNotifier::class,
\App\Services\SmsNotifier::class,
], 'notifiers');
}
public function boot(): void
{
//
}
}
Проверка через tinker
php artisan tinker
app()->bound(App\Contracts\Logger::class); // true
$logger = app(App\Contracts\Logger::class);
$cache = app(App\Contracts\Cache::class);
$cache2 = app(App\Contracts\Cache::class);
$cache === $cache2; // true (singleton)
Итог
- Container — ядро Laravel.
bind,singleton,instance— регистрация.- Auto-resolve через Reflection.
- Contextual binding — разные реализации.
- Tagging — группировка.
- Extend — декорирование.
BindingResolutionException— интерфейс не привязан.php artisan tinker— изучение.
Дальше — Фасады: магия и реальность.