Оригинал: 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:
Controller
└─ calls Service
└─ calls Model
└─ calls Helper
└─ calls AnotherServiceБизнес-логика в итоге оказывается:
- Половина в контроллере
- Половина в модели
- Половина в сервисе
- Часть в валидации
- Часть в блоках if-else, скопированных повсюду
Через 6 месяцев:
- Никто не знает, где находятся правила
- Ошибки повторяются
- Тесты становятся мучительными
- Изменения становятся рискованными
DDD переворачивает это мышление.
Начинайте с домена, не с фреймворка
Перед Laravel, Symfony, Slim, что-угодно… Спросите:
- Какую проблему решает приложение?
- Какова основная бизнес-концепция?
Пример. Скажем, вы строите систему управления заказами.
Ваши доменные слова:
- Order (Заказ)
- Customer (Заказчик)
- Money (Деньги)
- Payment (Платеж)
- OrderStatus (Статус Заказа)
Это не таблицы базы данных. Это бизнес-идеи.
Структура папок, которая имеет смысл
Вот практическая PHP DDD структура (простая, не академическая):
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.
Плохой подход:
$order->status = 'paid';Хороший подход:
$order->markAsPaid();Почему?
Потому что правила должны находиться внутри домена, не снаружи.
Пример:
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: маленькие вещи, снимающие большую головную боль
Прекратите передавать строки и числа везде.
Плохо:
function pay(string $amount, string $currency)Хорошо:
function pay(Money $money)Пример:
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:
- Репозиторий — это контракт
- Реализация живёт в инфраструктуре
На стороне домена:
interface OrderRepository
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
}На стороне инфраструктуры:
final class MysqlOrderRepository implements OrderRepository
{
public function save(Order $order): void
{
// Eloquent / PDO / Doctrine
}
}Почему это важно:
- Вы можете поменять БД позже
- Тесты становятся проще
- Домен остается чистым
Слой приложения: здесь живут сценарии
Слой приложения отвечает на вопрос:
Что пользователь сейчас пытается сделать?
Пример: создание заказа.
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);
}
}Нет утечки бизнес-логики.
Только оркестрация.
Контроллеры становятся скучными (и это хорошо)
Контроллер теперь:
public function store(Request $request)
{
$command = new CreateOrderCommand(
$request->customer_id
);
$this->handler->handle($command);
return response()->json(['status' => 'ok']);
}Тонкий. Чистый. Заменимый.
Тестирование вдруг становится простым
Потому что домен — это чистый 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 — это ясность, а не сложность.
- Держите правила ближе к данным.
- Защищайте ваш домен, как будто это важно.
- Фреймворки приходят и уходят, а домен остается.
Это не совершенство.
Это код, который выживает во времени, среди людей и изменений.
