Урок 02

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,
    ) {}
}

Что делает контейнер:

  1. Смотрит на Reflection конструктора.
  2. Видит тип UserRepository.
  3. Ищет привязку — не находит.
  4. Создаёт UserRepository (если это класс).
  5. Если интерфейс — ошибка.

Правило: интерфейс нужно привязать.

Автозависимости с параметрами по умолчанию

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 — изучение.

Дальше — Фасады: магия и реальность.