Domain-Driven Design в PHP: как не утонуть в сложности

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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):                

Блоги

Рекомендаций по книгам