0Pricing
PHP Academy · Урок

От многоуровневой к чистой архитектуре

Поймите, почему зависимости должны быть направлены внутрь

«От многоуровневой к чистой архитектуре» — бесплатный урок PHP Academy на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения PHP Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс PHP Academy содержит 4 уроков всего.

Почему нужна чистая архитектура

Вы уже знакомы с классическим трёхслойным стеком PHP: Контроллер → Сервис → Репозиторий → База данных. Он работает, но бизнес-логика оказывается связанной с Eloquent, Doctrine, HTTP-запросом и жизненным циклом фреймворка. Чистая архитектура меняет направление зависимостей так, чтобы домен ничего не знал об инфраструктуре. В результате вы получаете тестируемые варианты использования, заменяемые адаптеры и кодовую базу, устойчивую к обновлениям фреймворка.

Правило зависимостей

Единственное правило чистой архитектуры: зависимости исходного кода направлены только внутрь. Внутренние круги (сущности, варианты использования) не должны ссылаться на внешние круги (контроллеры, ORM, фреймворки). Во время выполнения управление проходит наружу через интерфейсы, но на этапе компиляции или импорта внутренний код не импортирует ничего из внешнего.

  • Сущности: корпоративные правила
  • Варианты использования: прикладные правила
  • Адаптеры: контроллеры, представления, шлюзы
  • Фреймворки и драйверы: база данных, HTTP, веб

Пример связанной слоистой архитектуры

Вот такой сервис обычно входит в состав большинства PHP-приложений. Обратите внимание: доменная логика переплетена с Eloquent и HTTP-ответом. Невозможно модульно протестировать правило скидки без базы данных и фреймворка.

<?php
class OrderService
{
    public function place(Request $request)
    {
        $user = User::find($request->user_id); // Eloquent
        $total = 0;
        foreach ($request->items as $i) {
            $total += Product::find($i['id'])->price * $i['qty'];
        }
        if ($user->is_vip) {
            $total *= 0.9; // business rule trapped in infra code
        }
        Order::create(['user_id' => $user->id, 'total' => $total]);
        return response()->json(['total' => $total]);
    }
}

Сущности: домен без фреймворка

Сущность кодирует общие для предприятия правила и ни от чего не зависит. Обычный PHP, без аннотаций и базового класса ORM. Её можно полностью создать в тесте.

<?php
final class Money
{
    public function __construct(public readonly int $cents) {
        if ($cents < 0) throw new InvalidArgumentException('negative money');
    }
    public function multiply(float $factor): self {
        return new self((int) round($this->cents * $factor));
    }
}

final class Order
{
    /** @param array<int,int> $lineCents */
    public function __construct(private array $lineCents, private bool $vip) {}
    public function total(): Money {
        $sum = array_sum($this->lineCents);
        $money = new Money($sum);
        return $this->vip ? $money->multiply(0.9) : $money;
    }
}

echo (new Order([1000, 2000], true))->total()->cents, PHP_EOL; // 2700

Варианты использования владеют сценарием работы

Вариант использования (интерактор) оркестрирует сущности и взаимодействует с внешним миром только через интерфейсы (порты). Он принимает объект передачи данных запроса и возвращает объект передачи данных ответа — никогда не объект HTTP.

<?php
interface OrderRepository {
    public function save(Order $order): void;
}

final class PlaceOrder
{
    public function __construct(private OrderRepository $orders) {}

    public function execute(array $lineCents, bool $vip): int {
        $order = new Order($lineCents, $vip);
        $this->orders->save($order);
        return $order->total()->cents;
    }
}

Граница — это интерфейс

Вариант использования объявляет необходимый ему интерфейс OrderRepository. Интерфейс находится во внутреннем круге, а конкретная реализация на Eloquent или Doctrine — снаружи и зависит от внутреннего кода. Это применение принципа инверсии зависимостей на архитектурной границе.

Направление зависимости исходного кода: EloquentOrderRepository → OrderRepository (интерфейс), но не наоборот.

<?php
// Lives in infrastructure layer, points INWARD to the domain interface
final class EloquentOrderRepository implements OrderRepository
{
    public function save(Order $order): void {
        OrderModel::create(['total' => $order->total()->cents]);
    }
}

Тестирование без инфраструктуры

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

<?php
final class InMemoryOrders implements OrderRepository {
    public array $saved = [];
    public function save(Order $o): void { $this->saved[] = $o; }
}

$repo = new InMemoryOrders();
$useCase = new PlaceOrder($repo);
$total = $useCase->execute([1000, 2000], true);

assert($total === 2700);
assert(count($repo->saved) === 1);
echo "PASS total=$total saved=" . count($repo->saved) . PHP_EOL;

Контроллеры становятся тонкими адаптерами

Теперь контроллер — это адаптер: он преобразует HTTP в вызов варианта использования, а результат — обратно в HTTP-ответ. В нём нет бизнес-правил. Замените REST на CLI или обработчик очереди — вариант использования останется неизменным.

<?php
final class OrderController
{
    public function __construct(private PlaceOrder $placeOrder) {}

    public function store(Request $request): JsonResponse {
        $total = $this->placeOrder->execute(
            lineCents: $request->input('lineCents'),
            vip: (bool) $request->input('vip'),
        );
        return new JsonResponse(['total' => $total], 201);
    }
}

Кричащая архитектура

Структура каталогов должна говорить о домене, а не о фреймворке. Не размещайте Controllers/ и Models/ на верхнем уровне. Организуйте код по бизнес-возможностям, чтобы новый разработчик сразу видел, что делает приложение.

  • src/Ordering/Domain/ — сущности, объекты-значения
  • src/Ordering/Application/ — варианты использования, интерфейсы портов
  • src/Ordering/Infrastructure/ — репозитории Eloquent, контроллеры HTTP

Каждый ограниченный контекст — это каталог верхнего уровня; фреймворк находится на границах.

Обеспечение правила зависимостей

Без инструментов дисциплина ослабевает. Используйте deptrac или phparkitect в CI, чтобы сборка завершалась ошибкой, если доменный слой импортирует инфраструктурный. Так правило становится гарантией на этапе компиляции, а не надеждой на проверку кода.

# deptrac.yaml
deptrac:
  layers:
    - name: Domain
      collectors: [{ type: directory, value: src/.*/Domain/.* }]
    - name: Application
      collectors: [{ type: directory, value: src/.*/Application/.* }]
    - name: Infrastructure
      collectors: [{ type: directory, value: src/.*/Infrastructure/.* }]
  ruleset:
    Domain: []                       # Domain may depend on nothing
    Application: [Domain]
    Infrastructure: [Application, Domain]

Пересечение границ с объектами передачи данных

Чтобы сущности не просачивались наружу, данные через границу передаются в виде простого объекта передачи данных, а не сущности или модели ORM. Вариант использования возвращает плоскую структуру, которую адаптер может сериализовать, поэтому доменный объект никогда не покидает ядро, а внешний слой не получает доступа к внутреннему состоянию.

<?php
final class OrderSummary // boundary DTO, no behavior, no domain types
{
    public function __construct(
        public readonly string $orderId,
        public readonly int $totalCents,
    ) {}
}

final class PlaceOrderV2 {
    public function __construct(private OrderRepository $orders) {}
    public function execute(array $lineCents, bool $vip): OrderSummary {
        $order = new Order($lineCents, $vip);
        $this->orders->save($order);
        return new OrderSummary('ord_1', $order->total()->cents);
    }
}

Быстрая проверка

Какое направление зависимостей допускается согласно Правилу зависимостей?

Итоги

Вы перешли от связанного слоистого стека к чистой архитектуре:

  • Правило зависимостей: зависимости исходного кода направлены только внутрь.
  • Сущности содержат корпоративные правила в PHP, не зависящем от фреймворка.
  • Варианты использования оркестрируют работу через интерфейсы портов, возвращая объекты передачи данных, а не HTTP.
  • Контроллеры и репозитории ORM — это внешние адаптеры, зависящие от внутреннего кода (DIP).
  • Структура должна говорить о домене, а такие инструменты, как deptrac, обеспечивают соблюдение правила в CI.

Часто задаваемые вопросы

Урок «От многоуровневой к чистой архитектуре» бесплатный?

Да — полный текст урока «От многоуровневой к чистой архитектуре» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс PHP Academy, подпишись на CoddyKit PRO. Курс PHP Academy содержит 4 уроков всего.

Чему я научусь в уроке «От многоуровневой к чистой архитектуре»?

Поймите, почему зависимости должны быть направлены внутрь Ты практикуешь PHP Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать PHP Academy?

Предыдущий опыт не требуется. PHP Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.

Сколько времени занимает урок «От многоуровневой к чистой архитектуре»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке PHP Academy?

Да. Каждый урок PHP Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

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

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