Laravel: Dependency Injection — перестаём создавать объекты руками
Зачем это нужно
Знакомая картина: внутри сервиса нужна отправка письма, лог или платёжный шлюз — и мы честно пишем new MailService() прямо в методе. Работает. Пока не нужно заменить реализацию, протестировать логику без реальных запросов или переиспользовать класс в другом месте. Тогда выясняется: класс намертво привязан к тому, что сам и создал.
Dependency Injection (DI, внедрение зависимостей) — это когда нужные объекты класс не создаёт сам, а получает снаружи. В Laravel «снаружи» — это сервис-контейнер: он смотрит на типы в конструкторе и подставляет готовые экземпляры.
Аналогия: бариста не выращивает кофейные зёрна — ему их приносят. Сменился поставщик — бариста не меняется.
Классика без DI
phpclass OrderService
{
public function placeOrder(array $data): void
{
$payment = new PaymentService(); // создаём сами
$payment->pay($data['amount']);
$repo = new OrderRepository(); // и это тоже
$repo->save($data);
}
}
Три проблемы:
- жёсткая привязка: захотели Stripe вместо внутреннего сервиса — правим
OrderService; - тесты: чтобы проверить логику заказа, придётся дёргать реальную платёжку и базу;
- неявные зависимости: по сигнатуре класса не видно, что ему нужно для работы.
С DI через конструктор
phpclass OrderService
{
public function __construct(
private PaymentInterface $payment,
private OrderRepository $orders,
) {}
public function placeOrder(array $data): void
{
$this->payment->pay($data['amount']);
$this->orders->save($data);
}
}
Теперь зависимости видны в конструкторе, а кто именно их создаёт — решает контейнер. Регистрируем привязку интерфейса к реализации:
php// AppServiceProvider::register()
$this->app->bind(PaymentInterface::class, StripePaymentService::class);
Дальше магия Laravel: в контроллере просто объявляем тип — и контейнер собирает всю цепочку сам.
phpclass OrderController extends Controller
{
public function __construct(private OrderService $orderService) {}
}
При создании контроллера Laravel создаст OrderService, а для него — StripePaymentService и репозиторий. Вложенность любой глубины, руками ничего передавать не нужно. Тот самый случай, когда вылезает ArgumentCountError — это как раз когда конструктор ждёт зависимость, а объект создают вручную без аргументов.
Подмена для тестов
Главный выигрыш DI — подменяемость. В тесте регистрируем фейк вместо реального шлюза:
php$this->app->bind(PaymentInterface::class, FakePaymentService::class);
Логика заказа проверяется без единого реального запроса к платёжной системе.
Подводный камень: Service Locator и статика
DI ломают двумя способами, и оба выглядят безобидно:
php// ПЛОХО: создание внутри метода
public function doSomething(): void
{
$client = new MailClient(); // зависимость спрятана, тест не подменить
}
// ПЛОХО: app() внутри класса — это Service Locator, не DI
public function doSomething(): void
{
app(MailClient::class)->send(...); // зависимость видна только внутри метода
}
Правило простое: зависимости — в конструктор (или аргументы метода), new для сервисов — нигде. Ещё две грабли: не внедряйте через контейнер примитивы (строки, числа — их передавайте аргументами) и следите за циклическими зависимостями (A требует B, B требует A — контейнер не соберёт такую конструкцию, надо выносить общую логику в третий сервис).
Мини-шпаргалка
php// Регистрация
$this->app->bind(Interface::class, Implementation::class); // новый экземпляр на каждый запрос
$this->app->singleton(Interface::class, Implementation::class); // один на всё приложение
// Внедрение
public function __construct(private PaymentInterface $payment) {} // конструктор
public function handle(PaymentInterface $payment) {} // метод (job, middleware)
// Подмена в тесте
$this->app->bind(PaymentInterface::class, FakePayment::class);
И три вывода:
- DI — это передача зависимостей снаружи через конструктор, а не создание их через
newвнутри класса. - Сервис-контейнер Laravel сам читает типы и собирает цепочку объектов — от контроллера до самого нижнего сервиса.
- Главная выгода — тестируемость и гибкость: реализацию подменяют одной строкой bind, не трогая бизнес-логику.