HardТеория6 min

Подвохи: Service Container и DI

Каверзные вопросы по DI, contextual binding, tagged services, facade resolution, deferred providers

Подвохи: Service Container и Dependency Injection

Service Container -- фундамент Laravel. На экзамене это одна из самых сложных тем с множеством ловушек.

Binding: bind vs singleton vs scoped

Разница между типами привязок

<?php

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // bind: NEW instance every time
        $this->app->bind(PaymentGateway::class, StripeGateway::class);

        // singleton: SAME instance for entire request lifecycle
        $this->app->singleton(CartService::class, function ($app) {
            return new CartService($app->make(PaymentGateway::class));
        });

        // scoped: SAME instance within scope, NEW between scopes
        // (useful with Octane, queue workers)
        $this->app->scoped(RequestContext::class, function () {
            return new RequestContext();
        });

        // instance: bind existing object
        $this->app->instance('config.custom', ['key' => 'value']);
    }
}

ЛОВУШКА 1: Какой метод использовать?

<?php

// Question: How many instances of Logger are created?
$this->app->bind(Logger::class, FileLogger::class);

$a = app(Logger::class);
$b = app(Logger::class);
$c = app(Logger::class);

// Answer: 3 instances (bind creates new each time)

// Question: And with singleton?
$this->app->singleton(Logger::class, FileLogger::class);

$a = app(Logger::class);
$b = app(Logger::class);
$c = app(Logger::class);

// Answer: 1 instance (all three are the same object)
$a === $b; // true
$b === $c; // true

ЛОВУШКА 2: scoped vs singleton с Octane

<?php

// With Laravel Octane, singleton persists across requests!
// scoped resets between requests

// DANGEROUS with Octane:
$this->app->singleton(UserContext::class, function () {
    return new UserContext(auth()->user()); // User leaks between requests!
});

// SAFE with Octane:
$this->app->scoped(UserContext::class, function () {
    return new UserContext(auth()->user()); // Resets each request
});

Contextual Binding

<?php

use App\Services\Notifications\EmailNotifier;
use App\Services\Notifications\SmsNotifier;
use App\Services\Notifications\NotifierInterface;
use App\Http\Controllers\OrderController;
use App\Http\Controllers\AlertController;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // When OrderController needs NotifierInterface, give EmailNotifier
        $this->app->when(OrderController::class)
            ->needs(NotifierInterface::class)
            ->give(EmailNotifier::class);

        // When AlertController needs NotifierInterface, give SmsNotifier
        $this->app->when(AlertController::class)
            ->needs(NotifierInterface::class)
            ->give(SmsNotifier::class);
    }
}

ЛОВУШКА 3: Contextual binding с примитивами

<?php

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Binding primitive values (not interfaces!)
        $this->app->when(ReportGenerator::class)
            ->needs('$format')
            ->give('pdf');

        // Using config value
        $this->app->when(MailService::class)
            ->needs('$maxRetries')
            ->giveConfig('mail.max_retries');

        // Using tagged services
        $this->app->when(NotificationManager::class)
            ->needs('$channels')
            ->giveTagged('notification.channels');
    }
}

Экзаменационный вопрос: "Можно ли использовать contextual binding для примитивных типов (string, int)?" Ответ: Да! Через $this->app->when()->needs('$paramName')->give('value'). Обратите внимание: имя параметра с $.

Tagged Services

<?php

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind('report.csv', CsvReportGenerator::class);
        $this->app->bind('report.pdf', PdfReportGenerator::class);
        $this->app->bind('report.excel', ExcelReportGenerator::class);

        // Tag multiple bindings
        $this->app->tag(
            ['report.csv', 'report.pdf', 'report.excel'],
            'reports'
        );
    }
}

// Resolve all tagged services
$generators = app()->tagged('reports');
// Returns iterable of all three generators

foreach ($generators as $generator) {
    $generator->generate($data);
}

ЛОВУШКА 4: tagged() возвращает iterable, а не массив или коллекцию. Для получения массива используйте iterator_to_array().

Facade Resolution

Как работают фасады (под капотом)

<?php

// When you call:
Cache::get('key');

// Laravel actually does:
// 1. Cache extends Facade
// 2. Facade::__callStatic('get', ['key']) is triggered
// 3. getFacadeAccessor() returns 'cache'
// 4. app('cache')->get('key') is called on the resolved instance

// The accessor:
class Cache extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return 'cache'; // This is the container binding key
    }
}

ЛОВУШКА 5: Facade accessor может быть:

  • Строка (binding key в контейнере): 'cache'
  • Класс (FQCN): CacheManager::class
<?php

// String accessor -> resolves from container binding
protected static function getFacadeAccessor(): string
{
    return 'cache'; // app('cache')
}

// Class accessor -> resolves class from container
protected static function getFacadeAccessor(): string
{
    return \Illuminate\Cache\CacheManager::class; // app(CacheManager::class)
}

Real-time Facades

<?php

// Instead of importing the class and injecting it:
use App\Services\PaymentProcessor;

class OrderController
{
    public function __construct(
        private readonly PaymentProcessor $processor,
    ) {}
}

// You can use Real-time Facade:
use Facades\App\Services\PaymentProcessor;

class OrderController
{
    public function store()
    {
        // Works like a facade!
        PaymentProcessor::process($order);

        // And can be mocked:
        // PaymentProcessor::shouldReceive('process')->once();
    }
}

ЛОВУШКА 6: Real-time Facades создаются добавлением Facades\ перед полным именем класса. Это работает для ЛЮБОГО класса, не только зарегистрированного в контейнере. Laravel автоматически генерирует facade-обёртку.

Экзаменационный вопрос: "Нужно ли регистрировать Real-time Facade в Service Provider?" Ответ: Нет! Laravel автоматически resolve-ит класс через контейнер. Регистрация не нужна.

Deferred Providers

<?php

declare(strict_types=1);

namespace App\Providers;

use App\Services\HeavyService;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;

class HeavyServiceProvider extends ServiceProvider implements DeferrableProvider
{
    public function register(): void
    {
        $this->app->singleton(HeavyService::class, function () {
            // This is only executed when HeavyService is actually resolved!
            return new HeavyService(
                config('services.heavy.api_key'),
            );
        });
    }

    // REQUIRED: Tell Laravel which bindings this provider provides
    public function provides(): array
    {
        return [HeavyService::class];
    }
}

ЛОВУШКА 7: Deferred Provider ОБЯЗАН:

  1. Реализовать интерфейс DeferrableProvider
  2. Определить метод provides() со списком предоставляемых биндингов
  3. register() вызывается ТОЛЬКО когда один из binding запрашивается
<?php

// Without DeferrableProvider:
// register() is called on EVERY request (even if HeavyService is not needed)

// With DeferrableProvider:
// register() is called ONLY when HeavyService is resolved
// This improves performance when the service is rarely used

Экзаменационный вопрос: "Когда вызывается boot() у deferred provider?" Ответ: boot() вызывается СРАЗУ ПОСЛЕ register(), когда провайдер наконец загружается (при первом resolve binding). НЕ при загрузке приложения.

Binding Interfaces to Implementations

<?php

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Simple binding
        $this->app->bind(
            \App\Contracts\PaymentGateway::class,
            \App\Services\StripeGateway::class
        );

        // With closure (for complex construction)
        $this->app->bind(
            \App\Contracts\Logger::class,
            function ($app) {
                return new \App\Services\FileLogger(
                    storage_path('logs/app.log'),
                    $app->make('config')->get('app.log_level'),
                );
            }
        );
    }
}

ЛОВУШКА 8: Auto-resolution (автоматическое разрешение)

<?php

// Laravel CAN automatically resolve classes WITHOUT explicit binding
// IF the class has no interface dependency or all dependencies are resolvable

class UserService
{
    public function __construct(
        private readonly UserRepository $repo, // Auto-resolved if concrete class
    ) {}
}

// No binding needed! Laravel creates UserService and UserRepository automatically
$service = app(UserService::class); // Works!

// BUT if you depend on an INTERFACE:
class UserService
{
    public function __construct(
        private readonly UserRepositoryInterface $repo, // Cannot auto-resolve!
    ) {}
}

// This FAILS without explicit binding:
$service = app(UserService::class);
// BindingResolutionException: Target [UserRepositoryInterface] is not instantiable

Container Events

<?php

use Illuminate\Support\Facades\App;

// Called when ANY dependency is resolved
App::resolving(function (mixed $object, $app) {
    // Runs for every resolution
});

// Called when specific class is resolved
App::resolving(UserService::class, function (UserService $service, $app) {
    // Modify $service after creation
    $service->setLogger($app->make(Logger::class));
});

// afterResolving: called after resolving callbacks
App::afterResolving(UserService::class, function (UserService $service) {
    // Final modifications
});

Методы resolve

<?php

// All ways to resolve from container:

// 1. app() helper
$service = app(UserService::class);
$service = app('user.service');

// 2. make() method
$service = app()->make(UserService::class);

// 3. resolve() helper
$service = resolve(UserService::class);

// 4. Automatic injection (constructor/method)
public function __construct(private readonly UserService $service) {}

// 5. makeWith() - with parameters
$service = app()->makeWith(UserService::class, ['config' => $config]);

ЛОВУШКА 9: app() vs app()->make() -- функционально идентичны. Но makeWith() позволяет передать параметры конструктора на лету.

register() vs boot()

<?php

class AppServiceProvider extends ServiceProvider
{
    // REGISTER: only bind things into container
    // Called BEFORE all providers are registered
    // Do NOT use other services here!
    public function register(): void
    {
        $this->app->singleton(UserService::class, function ($app) {
            return new UserService(
                $app->make(UserRepository::class),
            );
        });

        // WRONG in register():
        // Route::get(...); // Routes not available yet!
        // view()->share(...); // Views not registered yet!
    }

    // BOOT: use services, register routes, views, etc.
    // Called AFTER all providers are registered
    // All services are available here
    public function boot(): void
    {
        // Correct place for:
        view()->share('appName', config('app.name'));
        \Illuminate\Database\Eloquent\Model::preventLazyLoading();

        // Publishing config, views, migrations
        $this->publishes([
            __DIR__ . '/../config/custom.php' => config_path('custom.php'),
        ]);
    }
}

ЛОВУШКА 10: В register() нельзя использовать другие сервисы -- они могут быть ещё не зарегистрированы. Используйте boot() для любой логики, зависящей от других сервисов.


Проверь себя

Что произойдёт, если в методе register() Service Provider обратиться к сервису другого провайдера?

Что обязан определить Deferred Service Provider?

Чем scoped() отличается от singleton() при использовании Laravel Octane?

Нужно ли регистрировать Real-time Facade в Service Provider?

Сколько экземпляров Logger будет создано при вызове app(Logger::class) три раза, если Logger зарегистрирован через bind()?