Инверсия зависимостей на практике
Подключайте адаптеры к ядру с помощью инверсии зависимостей
«Инверсия зависимостей на практике» — бесплатный урок PHP Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения PHP Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс PHP Academy содержит 4 уроков всего.
Инверсия и внедрение
Эти два разных понятия часто смешивают. Внедрение зависимостей — это приём: зависимости передаются извне, а не создаются внутри. Инверсия зависимостей (буква D в SOLID) — это принцип: политика высокого уровня и детали низкого уровня зависят от абстракции, которой владеет модуль высокого уровня. Этот урок посвящён тому, как воплотить второй принцип: связать конкретные адаптеры с ядром, определяющим интерфейсы.
Принцип в точной формулировке
DIP утверждает:
- Модули высокого уровня не должны зависеть от модулей низкого уровня. И те и другие должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Тонкость заключается в следующем: интерфейс принадлежит потребителю, а не реализующему его компоненту. Предметная область объявляет PaymentGateway, а адаптер Stripe соответствует этому интерфейсу, а не наоборот.
Определите абстракцию в ядре
Разместите интерфейс рядом с кодом, которому он нужен, и выразите его в терминах предметной области. Типы Stripe не должны просачиваться внутрь.
<?php
// src/Billing/Application/Port/PaymentGateway.php
interface PaymentGateway
{
public function charge(Money $amount, CardToken $token): ChargeId;
}
final class Money {
public function __construct(public readonly int $cents, public readonly string $currency) {}
}
final class CardToken { public function __construct(public readonly string $value) {} }
final class ChargeId { public function __construct(public readonly string $value) {} }Приведите адаптер в соответствие с ним
Инфраструктурный адаптер реализует интерфейс ядра и преобразует вызовы в SDK поставщика. Стрелка зависимости направлена от адаптера (детали) к абстракции (политике) — инверсия достигнута.
<?php
final class StripePaymentGateway implements PaymentGateway
{
public function __construct(private \Stripe\StripeClient $stripe) {}
public function charge(Money $amount, CardToken $token): ChargeId {
$intent = $this->stripe->paymentIntents->create([
'amount' => $amount->cents,
'currency' => strtolower($amount->currency),
'payment_method' => $token->value,
'confirm' => true,
]);
return new ChargeId($intent->id);
}
}Внедрение через конструктор — стандартный выбор
Внедряйте зависимости через конструктор и указывайте в типе абстракцию. Зависимости становятся явными, неизменяемыми, и о них невозможно забыть. Не используйте внедрение через методы установки и свойства для обязательных зависимостей: такие способы допускают создание наполовину сконструированных объектов.
<?php
final class CheckoutService
{
public function __construct(
private PaymentGateway $payments, // abstraction, not StripeClient
private OrderRepository $orders,
) {}
public function pay(OrderId $id, CardToken $token): ChargeId {
$order = $this->orders->get($id);
$charge = $this->payments->charge($order->total(), $token);
$order->markPaid($charge);
$this->orders->save($order);
return $charge;
}
}Корень композиции
Всё конкретное связывание выполняется ровно в одном месте — в корне композиции, как можно ближе к main()/точке входа. Больше нигде инфраструктура не создаётся напрямую. Только это место знает о существовании Stripe.
<?php
// public/index.php — composition root
$stripe = new \Stripe\StripeClient(getenv('STRIPE_SECRET'));
$gateway = new StripePaymentGateway($stripe);
$orders = new PdoOrderRepository(new PDO(getenv('DB_DSN')));
$checkout = new CheckoutService($gateway, $orders);
// Everything below depends only on abstractions
$controller = new CheckoutController($checkout);Связывание с помощью DI-контейнера
Для нетривиальных приложений контейнер (PHP-DI, Symfony) автоматизирует связывание. Главное действие — сопоставить интерфейсы с реализациями. Автоматическое связывание разрешает конструкторы по типам; Вам нужно лишь объявить соответствие «интерфейс → класс».
<?php
use function DI\autowire;
use function DI\get;
return [
PaymentGateway::class => autowire(StripePaymentGateway::class),
OrderRepository::class => autowire(PdoOrderRepository::class),
\Stripe\StripeClient::class => fn() => new \Stripe\StripeClient(getenv('STRIPE_SECRET')),
PDO::class => fn() => new PDO(getenv('DB_DSN')),
];Замена адаптеров подтверждает принцип
Поскольку ядро зависит только от PaymentGateway, смена поставщика или проверка в автономном режиме требует изменения одной строки связывания. Ниже показана поддельная реализация для модульной проверки — CheckoutService остаётся неизменным и никогда не импортирует Stripe.
<?php
final class FakeGateway implements PaymentGateway {
public array $charges = [];
public function charge(Money $a, CardToken $t): ChargeId {
$this->charges[] = $a;
return new ChargeId('ch_test_' . count($this->charges));
}
}
$fake = new FakeGateway();
$id = $fake->charge(new Money(2500, 'EUR'), new CardToken('tok_visa'));
echo $id->value, ' charges=', count($fake->charges), PHP_EOL; // ch_test_1 charges=1Избегайте ловушки локатора сервисов
Внедрение самого контейнера с последующим извлечением зависимостей внутри методов — это антипаттерн локатора сервисов. Он скрывает зависимости, лишает Вас проверки типов и снова связывает код с контейнером. Явно внедряйте всё необходимое.
<?php
// ANTI-PATTERN: hidden dependencies, container leaks everywhere
final class BadCheckout {
public function __construct(private ContainerInterface $c) {}
public function pay($id, $token) {
$gateway = $this->c->get(PaymentGateway::class); // hidden!
// ...
}
}Где может появляться контейнер
Контейнер допустим ровно в одном слое: в корне композиции и окружающей его инфраструктурной обвязке фреймворка (например, в фабрике контроллеров). Классы предметной области и приложения должны оставаться независимыми от контейнера: они получают обычные объекты через конструкторы и могут создаваться вручную. Хорошая проверка: сможете ли Вы связать всё приложение в одном PHP-файле без контейнера? Если да, Ваши зависимости заданы честно.
Отложенное связывание без локатора сервисов
Иногда зависимость дорого создавать или она нужна только при определённых условиях. Не поддавайтесь искушению внедрить контейнер — вместо этого внедрите замыкание-фабрику. Зависимость остаётся явной и типизированной, а её создание откладывается до фактического использования.
<?php
final class ReportService
{
/** @param Closure():PaymentGateway $gatewayFactory */
public function __construct(private Closure $gatewayFactory) {}
public function refundIfNeeded(bool $needed): void {
if (!$needed) return;
$gateway = ($this->gatewayFactory)(); // built only when required
// $gateway->charge(...) etc.
}
}
// Composition root supplies the factory, not the container
$svc = new ReportService(fn() => new StripePaymentGateway($stripe ?? null));
echo 'lazy dependency wired', PHP_EOL;Быстрая проверка
Кому должен принадлежать интерфейс PaymentGateway?
Итоги
Вы правильно связали адаптеры с ядром:
- Инверсия ≠ внедрение: внедрение — это механизм, а инверсия — владение абстракцией со стороны потребителя.
- Ядро определяет интерфейс, а инфраструктурные адаптеры ему соответствуют.
- Используйте внедрение абстракций через конструктор; выполняйте всё конкретное связывание в одном корне композиции (или в конфигурации контейнера, сопоставляющей интерфейс с классом).
- Замена адаптеров и использование имитаций в проверках сводятся к изменению одной строки.
- Избегайте антипаттерна локатора сервисов — контейнер должен оставаться только на границе системы.
Часто задаваемые вопросы
Урок «Инверсия зависимостей на практике» бесплатный?
Да — полный текст урока «Инверсия зависимостей на практике» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс PHP Academy, подпишись на CoddyKit PRO. Курс PHP Academy содержит 4 уроков всего.
Чему я научусь в уроке «Инверсия зависимостей на практике»?
Подключайте адаптеры к ядру с помощью инверсии зависимостей Ты практикуешь PHP Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать PHP Academy?
Предыдущий опыт не требуется. PHP Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Инверсия зависимостей на практике»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке PHP Academy?
Да. Каждый урок PHP Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- От многоуровневой к чистой архитектуре
- Порты и адаптеры
- Варианты использования и прикладные службы
- Инверсия зависимостей на практике