Domain-Driven Design в PHP: как превратить хаос в порядок
Вы когда-нибудь чувствовали, что ваш PHP-проект разрастается как снежный ком? Сначала всё просто: пара контроллеров, моделька, несколько запросов к базе. А через полгода вы тонете в spaghetti-коде, где бизнес-логика размазана по сервисам, а «простое» изменение требования занимает неделю.
Знакомо? Добро пожаловать в клуб. Но есть и хорошая новость: Domain-Driven Design (DDD) — это не абстрактная теория из толстых книг Эрика Эванса. Это рабочий инструмент, который реально помогает писать PHP-код, понятный и бизнесу, и разработчикам.
В этой статье мы разберем DDD на практике. Без воды, с живыми примерами на PHP. Вы узнаете, как внедрить DDD в свой проект, не сломав голову и не переписав всё с нуля. Поехали!
Что такое Domain-Driven Design и зачем он PHP-разработчику?
Представьте, что вы пишете интернет-магазин. У вас есть скидки, корзина, заказы. Бизнес-логика: «скидка не суммируется с промокодом», «заказ нельзя отменить после отгрузки». Если вы храните эту логику в контроллерах или размазываете по моделям ORM, вы рано или поздно запутаетесь.
DDD — это подход, который ставит бизнес-домен (вашу предметную область) в центр разработки. Вместо того чтобы думать таблицами и SQL-запросами, вы думаете сущностями, значениями и событиями.
Для PHP это особенно актуально. Язык гибкий, но без четкой архитектуры проекты быстро превращаются в «легаси». DDD даёт каркас, который остаётся читаемым через год.
Стратегическое vs Тактическое проектирование
DDD делится на две большие части:
- Стратегическое проектирование — это карта вашего проекта. Вы выделяете ограниченные контексты (Bounded Contexts), строите вездесущий язык (Ubiquitous Language). Например, в контексте «Каталог» товар — это просто набор характеристик, а в контексте «Заказ» — это уже единица с ценой и количеством.
- Тактическое проектирование — это кирпичики: Entity, Value Object, Aggregate, Repository, Domain Service, Domain Event. Мы сосредоточимся на них.
Тактическое проектирование в PHP: кирпичики DDD
Давайте посмотрим, как выглядят основные паттерны DDD на PHP. Забудьте про ActiveRecord из Laravel или Symfony — здесь мы строим модель, независимую от фреймворка.
1. Entity (Сущность)
Сущность — это объект, который имеет уникальный идентификатор и меняется во времени. Например, User или Order. Сравниваем сущности по ID, а не по полям.
<?php
declare(strict_types=1);
namespace App\Domain\Entity;
use App\Domain\ValueObject\UserId;
use App\Domain\ValueObject\Email;
class User
{
public function __construct(
private UserId $id,
private string $name,
private Email $email
) {}
public function getId(): UserId
{
return $this->id;
}
public function changeEmail(Email $newEmail): void
{
// Здесь может быть бизнес-логика: проверка, не занят ли email
$this->email = $newEmail;
}
public function getEmail(): Email
{
return $this->email;
}
}
2. Value Object (Объект-значение)
Value Object — это неизменяемый объект, который определяется своими атрибутами, а не идентификатором. Два Value Object равны, если все их поля совпадают. Классика: Money, Email, Address.
<?php
declare(strict_types=1);
namespace App\Domain\ValueObject;
class Email
{
private string $value;
public function __construct(string $value)
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Invalid email address');
}
$this->value = $value;
}
public function getValue(): string
{
return $this->value;
}
public function equals(Email $other): bool
{
return $this->value === $other->value;
}
}
3. Aggregate (Агрегат)
Агрегат — это кластер сущностей и объектов-значений, которые обрабатываются как единое целое. У агрегата есть корень (Aggregate Root), через который происходит доступ к внутренним объектам. Например, Order (корень) содержит OrderItem (сущности).
<?php
declare(strict_types=1);
namespace App\Domain\Entity;
use App\Domain\ValueObject\Money;
class Order
{
/** @var OrderItem[] */
private array $items = [];
public function __construct(
private OrderId $id
) {}
public function addItem(ProductId $productId, int $quantity, Money $price): void
{
// Бизнес-правило: нельзя добавить товар с нулевой ценой
if ($price->isZero()) {
throw new \DomainException('Price must be greater than zero');
}
$this->items[] = new OrderItem(
$this->id,
$productId,
$quantity,
$price
);
}
public function getTotal(): Money
{
$total = new Money(0, 'USD');
foreach ($this->items as $item) {
$total = $total->add($item->getSubtotal());
}
return $total;
}
}
4. Repository (Репозиторий)
Репозиторий — это коллекция объектов, которая предоставляет методы для доступа к агрегатам. Важно: репозиторий работает с доменными объектами, а не с ORM-сущностями.
<?php
declare(strict_types=1);
namespace App\Domain\Repository;
use App\Domain\Entity\Order;
use App\Domain\ValueObject\OrderId;
interface OrderRepository
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
public function delete(OrderId $id): void;
}
Реализация репозитория может использовать Doctrine, Eloquent или даже файловое хранилище. Главное — интерфейс остаётся чистым.
5. Domain Service (Доменный сервис)
Когда бизнес-логика не вписывается ни в одну сущность или агрегат, выносим её в доменный сервис. Например, расчёт налога или проверка доступности промокода.
<?php
declare(strict_types=1);
namespace App\Domain\Service;
use App\Domain\Entity\Order;
use App\Domain\ValueObject\Money;
class TaxCalculator
{
public function calculateTax(Order $order, string $country): Money
{
// Сложная логика расчёта налога
$total = $order->getTotal();
$rate = $this->getRateForCountry($country);
return $total->multiply($rate);
}
private function getRateForCountry(string $country):