Service Container (IoC-контейнер)
Что такое Service Container
Service Container (Inversion of Control Container) -- это мощный инструмент для управления зависимостями классов и выполнения внедрения зависимостей (Dependency Injection). Это сердце Laravel-приложения.
<?php
declare(strict_types=1);
// The Application class IS the Service Container
// Illuminate\Foundation\Application extends Illuminate\Container\Container
// Access the container:
$container = app(); // Global helper
$container = \Illuminate\Container\Container::getInstance();
$container = $this->app; // In service providers
$container = resolve('SomeClass'); // Resolve from container
Ключевой концепт: Inversion of Control (IoC) означает, что вместо того чтобы класс сам создавал свои зависимости, они внедряются извне (контейнером). Это обеспечивает слабую связанность, тестируемость и гибкость.
Автоматическое разрешение (Zero-Configuration Resolution)
Одна из самых мощных возможностей контейнера -- автоматическое разрешение зависимостей без явной регистрации.
<?php
declare(strict_types=1);
namespace App\Services;
// No binding needed — container resolves this automatically
final class OrderService
{
public function __construct(
private readonly PaymentGateway $payment,
private readonly InventoryService $inventory,
private readonly NotificationService $notifications,
) {}
public function processOrder(Order $order): void
{
$this->inventory->reserve($order);
$this->payment->charge($order);
$this->notifications->sendConfirmation($order);
}
}
// Container uses PHP Reflection API to:
// 1. Inspect constructor parameters
// 2. Determine type hints
// 3. Recursively resolve each dependency
// 4. Create instance with all dependencies injected
// This works WITHOUT any explicit binding:
$service = app(OrderService::class);
// Container automatically creates PaymentGateway, InventoryService,
// NotificationService, and injects them into OrderService
<?php
declare(strict_types=1);
// Automatic resolution ONLY works for concrete classes
// It CANNOT resolve:
// - Interfaces (no way to know which implementation)
// - Abstract classes
// - Scalar types (string, int, bool)
// - Union/intersection types (ambiguous)
// Example that FAILS without binding:
namespace App\Services;
use App\Contracts\PaymentGatewayInterface;
final class CheckoutService
{
public function __construct(
private readonly PaymentGatewayInterface $gateway, // Interface — needs binding
private readonly string $currency, // Scalar — needs binding
) {}
}
Для экзамена: Контейнер использует PHP Reflection API для анализа конструктора. Автоматическое разрешение работает ТОЛЬКО для конкретных классов с типизированными параметрами конструктора. Интерфейсы, абстрактные классы и скалярные типы требуют явной привязки.
Привязки (Bindings)
bind() -- новый экземпляр каждый раз
<?php
declare(strict_types=1);
namespace App\Providers;
use App\Contracts\PaymentGatewayInterface;
use App\Services\StripePaymentGateway;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Simple binding: interface → implementation
$this->app->bind(
PaymentGatewayInterface::class,
StripePaymentGateway::class,
);
// Binding with closure (for complex construction)
$this->app->bind(PaymentGatewayInterface::class, function ($app) {
return new StripePaymentGateway(
apiKey: config('services.stripe.secret'),
currency: config('services.stripe.currency', 'usd'),
logger: $app->make(\Psr\Log\LoggerInterface::class),
);
});
// IMPORTANT: bind() creates a NEW instance EACH TIME it's resolved
$a = app(PaymentGatewayInterface::class);
$b = app(PaymentGatewayInterface::class);
// $a !== $b → different instances
}
}
singleton() -- один экземпляр на весь жизненный цикл
<?php
declare(strict_types=1);
namespace App\Providers;
use App\Services\CartService;
use App\Services\CacheManager;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Singleton: one instance for the entire request lifecycle
$this->app->singleton(CartService::class, function ($app) {
return new CartService(
session: $app->make('session.store'),
);
});
// Simple singleton (class resolves itself)
$this->app->singleton(CacheManager::class);
// Interface to implementation singleton
$this->app->singleton(
\App\Contracts\CacheInterface::class,
\App\Services\RedisCacheService::class,
);
// Same instance returned every time
$a = app(CartService::class);
$b = app(CartService::class);
// $a === $b → same instance
}
}
Ловушка экзамена:
bind()создаёт НОВЫЙ экземпляр при каждом разрешении.singleton()создаёт экземпляр при ПЕРВОМ разрешении и затем возвращает его при всех последующих. Выбор между ними зависит от того, должен ли объект хранить состояние, разделяемое между вызовами.
scoped() -- один экземпляр на запрос
<?php
declare(strict_types=1);
namespace App\Providers;
use App\Services\RequestContext;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Scoped: one instance per request
// Similar to singleton, but flushed between requests
// in long-running contexts (Octane, queue workers)
$this->app->scoped(RequestContext::class, function ($app) {
return new RequestContext(
ip: request()->ip(),
userAgent: request()->userAgent(),
);
});
// IMPORTANT: In traditional PHP (php-fpm), scoped() and singleton()
// behave identically because each request is a fresh process.
// The difference matters in:
// - Laravel Octane (persistent process)
// - Queue workers (long-running process)
// - Laravel Reverb (WebSocket server)
}
}
Для Senior: Метод
scoped()особенно важен при использовании Laravel Octane, где процесс PHP не завершается между запросами. Singleton-зависимости в Octane будут сохранять состояние между запросами, что может привести к утечке данных. Scoped-привязки автоматически сбрасываются.
instance() -- привязка существующего объекта
<?php
declare(strict_types=1);
namespace App\Providers;
use App\Services\AppConfig;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Bind an already created instance
$config = new AppConfig(
version: '2.0.0',
features: ['dark-mode', 'notifications'],
);
$this->app->instance(AppConfig::class, $config);
// The exact same object is returned every time
$resolved = app(AppConfig::class);
// $resolved === $config → true
// Useful for:
// - Testing (inject mocks)
// - Pre-configured objects
// - External service instances
}
}
Контекстные привязки
Контекстные привязки позволяют разрешать разные реализации одного интерфейса в зависимости от того, куда они внедряются.
<?php
declare(strict_types=1);
namespace App\Providers;
use App\Contracts\FileStorageInterface;
use App\Services\LocalStorage;
use App\Services\S3Storage;
use App\Services\PhotoUploader;
use App\Services\DocumentUploader;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// When PhotoUploader needs FileStorageInterface → give S3Storage
$this->app->when(PhotoUploader::class)
->needs(FileStorageInterface::class)
->give(S3Storage::class);
// When DocumentUploader needs FileStorageInterface → give LocalStorage
$this->app->when(DocumentUploader::class)
->needs(FileStorageInterface::class)
->give(LocalStorage::class);
// Contextual binding with closure
$this->app->when(ReportGenerator::class)
->needs(FileStorageInterface::class)
->give(function ($app) {
return new S3Storage(
bucket: config('filesystems.disks.reports.bucket'),
);
});
// Binding scalar values contextually
$this->app->when(StripePaymentGateway::class)
->needs('$apiKey')
->give(config('services.stripe.secret'));
// Using giveConfig shorthand
$this->app->when(StripePaymentGateway::class)
->needs('$apiKey')
->giveConfig('services.stripe.secret');
}
}
<?php
declare(strict_types=1);
namespace App\Services;
use App\Contracts\FileStorageInterface;
final class PhotoUploader
{
public function __construct(
private readonly FileStorageInterface $storage, // Will receive S3Storage
) {}
}
final class DocumentUploader
{
public function __construct(
private readonly FileStorageInterface $storage, // Will receive LocalStorage
) {}
}
Для экзамена: Контекстные привязки -- один из самых мощных механизмов контейнера. Метод
when()->needs()->give()позволяет разрешать разные реализации одного интерфейса в зависимости от потребителя. МетодgiveConfig()-- удобный шорткат для привязки конфигурационных значений.
Тегирование (Tagging)
<?php
declare(strict_types=1);
namespace App\Providers;
use App\Reports\CsvReportExporter;
use App\Reports\PdfReportExporter;
use App\Reports\XlsxReportExporter;
use App\Contracts\ReportExporterInterface;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Register multiple implementations
$this->app->bind('report.csv', CsvReportExporter::class);
$this->app->bind('report.pdf', PdfReportExporter::class);
$this->app->bind('report.xlsx', XlsxReportExporter::class);
// Tag them for batch resolution
$this->app->tag(
['report.csv', 'report.pdf', 'report.xlsx'],
'report.exporters'
);
}
}
<?php
declare(strict_types=1);
namespace App\Services;
use App\Contracts\ReportExporterInterface;
final class ReportService
{
/** @var ReportExporterInterface[] */
private array $exporters;
public function __construct()
{
// Resolve all tagged services
$this->exporters = iterator_to_array(app()->tagged('report.exporters'));
}
public function export(Report $report, string $format): string
{
foreach ($this->exporters as $exporter) {
if ($exporter->supports($format)) {
return $exporter->export($report);
}
}
throw new \InvalidArgumentException("Unsupported format: {$format}");
}
}
Расширение привязок (Extending)
<?php
declare(strict_types=1);
namespace App\Providers;
use App\Services\PaymentGateway;
use App\Services\LoggingPaymentGateway;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(PaymentGateway::class);
// Extend the resolved instance (Decorator pattern)
$this->app->extend(PaymentGateway::class, function ($gateway, $app) {
// Wrap the original service with a decorator
return new LoggingPaymentGateway(
gateway: $gateway,
logger: $app->make(\Psr\Log\LoggerInterface::class),
);
});
// extend() is called after the service is resolved
// but before it's returned to the consumer
// Perfect for the Decorator pattern
}
}
Разрешение зависимостей
Способы получения сервисов из контейнера
<?php
declare(strict_types=1);
use App\Services\OrderService;
// Method 1: app() helper
$service = app(OrderService::class);
// Method 2: resolve() helper
$service = resolve(OrderService::class);
// Method 3: app()->make()
$service = app()->make(OrderService::class);
// Method 4: app()->makeWith() — with runtime parameters
$service = app()->makeWith(OrderService::class, [
'currency' => 'EUR',
]);
// Method 5: Constructor injection (PREFERRED)
final class CheckoutController
{
public function __construct(
private readonly OrderService $orderService,
) {}
}
// Method 6: Method injection (in controllers)
final class CheckoutController
{
public function store(Request $request, OrderService $orderService): Response
{
// $orderService is injected by the container
}
}
// Method 7: Facade (static proxy to container instance)
// OrderService::process($order);
// → Actually calls: app(OrderService::class)->process($order)
Проверка привязок
<?php
declare(strict_types=1);
// Check if a binding exists
$exists = app()->bound(OrderService::class); // true/false
// Check if instance is already resolved (singleton)
$resolved = app()->resolved(OrderService::class); // true/false
// Get the alias for an abstract
$alias = app()->getAlias(OrderService::class);
// Check if abstract is a shared (singleton) binding
$isShared = app()->isShared(OrderService::class);
Привязки интерфейсов (Interface Binding)
<?php
declare(strict_types=1);
// The MOST IMPORTANT use of the container:
// Binding interfaces to implementations
// Contract (Interface):
namespace App\Contracts;
interface NotificationChannelInterface
{
public function send(User $user, string $message): void;
}
// Implementation 1:
namespace App\Services\Notifications;
final class EmailNotificationChannel implements NotificationChannelInterface
{
public function send(User $user, string $message): void
{
// Send email
}
}
// Implementation 2:
final class SmsNotificationChannel implements NotificationChannelInterface
{
public function send(User $user, string $message): void
{
// Send SMS
}
}
// Binding:
$this->app->bind(
NotificationChannelInterface::class,
EmailNotificationChannel::class,
);
// Now any class type-hinting NotificationChannelInterface
// will receive EmailNotificationChannel
// To switch to SMS — change ONE line in ServiceProvider
// No changes needed in consuming classes (Open/Closed Principle)
Контейнерные события
<?php
declare(strict_types=1);
namespace App\Providers;
use App\Services\OrderService;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Execute callback when a specific class is resolved
$this->app->resolving(OrderService::class, function ($service, $app) {
// Called every time OrderService is resolved
// Useful for setting up the service
$service->setDefaultCurrency(config('app.currency'));
});
// Execute callback when ANY class is resolved
$this->app->resolving(function ($object, $app) {
// Called for every resolution
// Be careful — this fires A LOT
});
// After resolving callback
$this->app->afterResolving(OrderService::class, function ($service) {
// Called after the service is fully resolved
// and after resolving callbacks
});
}
}
Паттерн использования в тестах
<?php
declare(strict_types=1);
namespace Tests\Feature;
use App\Contracts\PaymentGatewayInterface;
use App\Services\FakePaymentGateway;
use Tests\TestCase;
final class CheckoutTest extends TestCase
{
public function test_successful_checkout(): void
{
// Replace binding for testing
$this->app->instance(
PaymentGatewayInterface::class,
new FakePaymentGateway(shouldSucceed: true),
);
// Or use mock
$mock = $this->mock(PaymentGatewayInterface::class);
$mock->shouldReceive('charge')
->once()
->andReturn(true);
$response = $this->post('/checkout', [
'product_id' => 1,
'quantity' => 2,
]);
$response->assertStatus(200);
}
public function test_swap_binding(): void
{
// swap() replaces the binding for the current test
$this->swap(PaymentGatewayInterface::class, new FakePaymentGateway());
// partialMock() creates a partial mock
$this->partialMock(PaymentGatewayInterface::class, function ($mock) {
$mock->shouldReceive('charge')->andReturn(true);
});
}
}