В 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 нужна только когда в тайп-хинте стоит интерфейс или когда нужна особая логика создания объекта.