DI Container в Laravel — как работает внедрение зависимостей
Glossary overview

DI Container в Laravel — как работает внедрение зависимостей

В Laravel не нужно вручную создавать контроллеры и передавать в них зависимости. Всё это делает Service Container — встроенный механизм, который сам создаёт объекты и подставляет нужные классы в конструкторы.

Как это работает

Допустим, есть роут:

Route::post('/orders', [OrderController::class, 'store']);

И контроллер с зависимостью в конструкторе:

class OrderController extends Controller
{
    public function __construct(
        private OrderService $orderService,
    ) {}
 
    public function store(StoreOrderRequest $request): JsonResponse
    {
        $order = $this->orderService->create(...);
 
        return new OrderResource($order);
    }
}

Когда приходит POST-запрос на /orders, Laravel делает примерно следующее:

// Это делает Laravel под капотом, ты этот код НЕ пишешь
$controller = app(OrderController::class);
$controller->store($request);

Вызов app(OrderController::class) запускает контейнер. Контейнер смотрит на конструктор, видит тайп-хинт OrderService и пытается создать этот класс. Если получилось — передаёт его в конструктор контроллера. Это называется auto-resolution (автоматическое разрешение зависимостей).

Когда всё работает автоматически

Если у класса нет зависимостей или все его зависимости — конкретные классы, контейнер справится сам. Никакой регистрации не нужно.

// Сервис без зависимостей — контейнер создаст его сам
class OrderService
{
    public function create(User $user, array $items, string $shippingAddress): Order
    {
        // бизнес-логика
    }
}
// Сервис с зависимостью от конкретного класса — тоже создаст сам
class OrderService
{
    public function __construct(
        private ProductRepository $products,
    ) {}
}

Контейнер увидит, что OrderService требует ProductRepository, создаст сначала его, а потом передаст в OrderService. Цепочка может быть любой глубины — контейнер рекурсивно разрешает все зависимости.

Когда нужна ручная регистрация

Проблема возникает, когда в тайп-хинте стоит интерфейс, а не конкретный класс. Контейнер не знает, какую реализацию подставить — их может быть несколько.

// Сервис зависит от интерфейса
class OrderService
{
    public function __construct(
        private OrderRepositoryInterface $orders,
    ) {}
}

В этом случае нужно вручную сказать контейнеру, какой класс использовать. Это делается в сервис-провайдере:

// app/Providers/AppServiceProvider.php
 
use App\Repositories\OrderRepository;
use App\Repositories\OrderRepositoryInterface;
 
class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(
            OrderRepositoryInterface::class,
            OrderRepository::class
        );
    }
}

Теперь везде, где контейнер встретит тайп-хинт OrderRepositoryInterface, он подставит OrderRepository.

bind vs singleton

bind — каждый раз создаёт новый экземпляр. Если три класса зависят от OrderService, получат три разных объекта.

$this->app->bind(OrderService::class, OrderService::class);

singleton — создаёт объект один раз и переиспользует его во всех местах. Полезно для тяжёлых объектов: HTTP-клиентов, конфигов, кэш-менеджеров.

$this->app->singleton(PaymentGateway::class, StripeGateway::class);

DI работает не только в контроллерах

Тот же механизм работает в методах контроллера (method injection), в конструкторах Job, Event, Listener, Middleware, команд Artisan — везде, где Laravel создаёт объект через контейнер.

// Method injection — Laravel сам подставит StoreOrderRequest
public function store(StoreOrderRequest $request): JsonResponse
{
    // $request уже провалидирован и готов к использованию
}
// DI в Artisan-команде
class SendReportsCommand extends Command
{
    public function __construct(
        private ReportService $reports,
    ) {
        parent::__construct();
    }
}

Полная цепочка разрешения зависимостей

HTTP Request → POST /orders
    ↓
Laravel ищет контроллер → OrderController
    ↓
Контейнер смотрит конструктор → нужен OrderService
    ↓
Контейнер смотрит конструктор OrderService → нужен ProductRepository
    ↓
ProductRepository без зависимостей → создаёт его
    ↓
Передаёт ProductRepository в OrderService → создаёт его
    ↓
Передаёт OrderService в OrderController → создаёт его
    ↓
Вызывает метод store() → видит StoreOrderRequest → создаёт и валидирует
    ↓
Метод выполняется → HTTP Response

Итого

Контроллеры, сервисы и другие классы руками обычно не создаёшь. Laravel сам внедряет всё через DI Container. Тебе достаточно указать тайп-хинт в конструкторе — и зависимость будет доступна. Ручная регистрация через bind или singleton нужна только когда в тайп-хинте стоит интерфейс или когда нужна особая логика создания объекта.