0Pricing
PHP Academy · Урок

Инверсия зависимостей на практике

Подключайте адаптеры к ядру с помощью инверсии зависимостей

«Инверсия зависимостей на практике» — бесплатный урок 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 — локальная установка не требуется.

Все уроки этого курса

  1. От многоуровневой к чистой архитектуре
  2. Порты и адаптеры
  3. Варианты использования и прикладные службы
  4. Инверсия зависимостей на практике
← Назад к PHP Academy