Оригинал: Building Maintainable Domain-Driven PHP Applications: A Practical Guide

Перевод для канала Мы ж программист

Я видел, как многие PHP-проекты медленно умирали.

Не потому, что PHP плох.

Не потому, что фреймворки не сработали.

А потому, что бизнес-логика была разбросана повсюду: контроллеры, сервисы, хелперы, трейты, случайные файлы с именем Common.php.

Сначала все работает.
Потом происходит небольшое изменение.
Затем еще одно.
И вдруг никто не хочет трогать код.

Именно здесь тихо вступает в игру Domain-Driven Design (DDD) — не как причудливая архитектура, а как стратегия выживания.

Это не теория.
Именно так вы можете создавать поддерживаемые PHP-приложения с помощью DDD, не сойдя с ума.

Для начала развеем мифы о DDD

DDD — это не:

  • о сложных структурах папок
  • об агрегатах повсюду
  • о написании больше кода
  • об использовании каждого термина DDD, прочитанного вами

DDD — это всего одна вещь:

Хранение бизнес-правил в одном понятном месте и их защита.

Остальное — необязательно.

Основная проблема в большинстве PHP приложений

Типичная структура PHP:

Plaintext
Controller
 └─ calls Service
     └─ calls Model
         └─ calls Helper
             └─ calls AnotherService

Бизнес-логика в итоге оказывается:

  • Половина в контроллере
  • Половина в модели
  • Половина в сервисе
  • Часть в валидации
  • Часть в блоках if-else, скопированных повсюду

Через 6 месяцев:

  • Никто не знает, где находятся правила
  • Ошибки повторяются
  • Тесты становятся мучительными
  • Изменения становятся рискованными


DDD переворачивает это мышление.

Начинайте с домена, не с фреймворка

Перед Laravel, Symfony, Slim, что-угодно… Спросите:

  • Какую проблему решает приложение?
  • Какова основная бизнес-концепция?

Пример. Скажем, вы строите систему управления заказами.

Ваши доменные слова:

  • Order (Заказ)
  • Customer (Заказчик)
  • Money (Деньги)
  • Payment (Платеж)
  • OrderStatus (Статус Заказа)

Это не таблицы базы данных. Это бизнес-идеи.

Структура папок, которая имеет смысл

Вот практическая PHP DDD структура (простая, не академическая):

Plaintext
src/
 ├── Domain/
 │   ├── Order/
 │   │   ├── Order.php
 │   │   ├── OrderId.php
 │   │   ├── OrderStatus.php
 │   │   ├── OrderRepository.php
 │   │   └── Exceptions/
 │   ├── Customer/
 │   └── Shared/
 │       └── Money.php

 ├── Application/
 │   ├── CreateOrder/
 │   │   ├── CreateOrderCommand.php
 │   │   └── CreateOrderHandler.php

 ├── Infrastructure/
 │   ├── Persistence/
 │   │   └── MysqlOrderRepository.php
 │   └── Http/
 │       └── Controllers/

Ключевые идеи:

  • Домен ничего не знает о БД или фреймворке
  • Инфраструктура знает обо всем
  • Приложение их соединяет

Само по себе разделение устраняет 50% хаоса.

Доменная модель должна защищать свои правила

Это сердце DDD.

Плохой подход:

PHP
$order->status = 'paid';

Хороший подход:

PHP
$order->markAsPaid();

Почему?

Потому что правила должны находиться внутри домена, не снаружи.

Пример:

PHP
final class Order
{
    private OrderStatus $status;
    
    public function markAsPaid(): void
    {
        if (!$this->status->canBePaid()) {
            throw new DomainException('Order cannot be paid');
        }
     $this->status = OrderStatus::paid();
    }
}

Теперь:

  • Никакой контроллер не сломает правила
  • Никакой сервис не вклинится
  • Правила живут в одном месте

Это — поддерживаемость.

Value Objects: маленькие вещи, снимающие большую головную боль

Прекратите передавать строки и числа везде.

Плохо:

PHP
function pay(string $amount, string $currency)

Хорошо:

PHP
function pay(Money $money)

Пример:

PHP
final class Money
{
    public function __construct(
        private int $amount,
        private string $currency
    ) {
        if ($amount < 0) {
            throw new InvalidArgumentException('Amount cannot be negative');
        }
    }
    
    public function add(Money $other): Money
    {
        if ($this->currency !== $other->currency) {
            throw new DomainException('Currency mismatch');
        }
        return new Money($this->amount + $other->amount, $this->currency);
    }
}

Количество багов автоматически уменьшается.

Репозитории — интерфейсы, не модели

В DDD:

  • Репозиторий — это контракт
  • Реализация живёт в инфраструктуре

На стороне домена:

PHP
interface OrderRepository
{
    public function save(Order $order): void;
    public function findById(OrderId $id): ?Order;
}

На стороне инфраструктуры:

PHP
final class MysqlOrderRepository implements OrderRepository
{
    public function save(Order $order): void
    {
        // Eloquent / PDO / Doctrine
    }
}

Почему это важно:

  • Вы можете поменять БД позже
  • Тесты становятся проще
  • Домен остается чистым

Слой приложения: здесь живут сценарии

Слой приложения отвечает на вопрос:

Что пользователь сейчас пытается сделать?

Пример: создание заказа.

PHP
final class CreateOrderHandler
{
    public function __construct(
        private OrderRepository $orders
    ) {}
    
    public function handle(CreateOrderCommand $command): void
    {
        $order = Order::create(
            OrderId::generate(),
            $command->customerId()
        );
        $this->orders->save($order);
    }
}

Нет утечки бизнес-логики.
Только оркестрация.

Контроллеры становятся скучными (и это хорошо)

Контроллер теперь:

PHP
public function store(Request $request)
{
    $command = new CreateOrderCommand(
        $request->customer_id
    );
    
    $this->handler->handle($command);
    
    return response()->json(['status' => 'ok']);
}

Тонкий. Чистый. Заменимый.

Тестирование вдруг становится простым

Потому что домен — это чистый PHP:

PHP
public function test_order_cannot_be_paid_twice()
{
    $order = Order::create(...);
    $order->markAsPaid();
    $this->expectException(DomainException::class);
    $order->markAsPaid();
}

Никакой БД. Никакого фреймворка. Быстрые тесты.

Когда DDD — перебор (будем честны)

Не используйте DDD, если:

  • Приложение — простой CRUD
  • Нет реальных бизнес-правил
  • Кратковременный внутренний инструмент

Но однажды:

  • Правила вырастают
  • Баги повторяются
  • Появляется страх перед изменениями

DDD окупается с лихвой.

Немного выводов

  • DDD — это ясность, а не сложность.
  • Держите правила ближе к данным.
  • Защищайте ваш домен, как будто это важно.
  • Фреймворки приходят и уходят, а домен остается.

Это не совершенство.
Это код, который выживает во времени, среди людей и изменений.