0Pricing
PHP Academy · Урок

Агрегаты, репозитории и фабрики

Защищайте инварианты с помощью агрегатов и корректно сохраняйте их

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

Защита инвариантов

После появления сущностей и объектов-значений нужны шаблоны, которые поддерживают согласованность их групп и позволяют корректно сохранять их. DDD предлагает три тактических шаблона: Aggregate (граница согласованности), репозиторий (абстракция хранения, похожая на коллекцию) и фабрика (сложное создание). В этом уроке показано, как они взаимодействуют в PHP.

Что такое Aggregate

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

Корень защищает всё

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

<?php
final class LineItem {
    public function __construct(
        public readonly string $sku,
        public readonly int $qty,
        public readonly int $unitCents
    ) {}
    public function subtotal(): int { return $this->qty * $this->unitCents; }
}

final class Order { // Aggregate Root
    /** @var LineItem[] */
    private array $items = [];
    public function __construct(public readonly string $id) {}

    public function addItem(string $sku, int $qty, int $unitCents): void {
        if ($qty < 1) { throw new DomainException('qty must be >= 1'); }
        $this->items[] = new LineItem($sku, $qty, $unitCents);
    }
    public function total(): int {
        return array_sum(array_map(fn(LineItem $i) => $i->subtotal(), $this->items));
    }
}
$o = new Order('o1');
$o->addItem('A', 2, 500);
$o->addItem('B', 1, 300);
echo $o->total(), PHP_EOL; // 1300

Проектирование небольших агрегатов

Частая ошибка — делать агрегаты слишком большими, например создавать Order, которому также принадлежит весь граф Customer. Практические правила:

  • Держите агрегаты небольшими; ссылайтесь на другие агрегаты по идентификатору, а не храните сам объект.
  • Одна транзакция должна изменять один агрегат; координируйте работу между агрегатами с помощью доменных событий.
  • Границу определяют инварианты, которые должны сохраняться всегда.

Ссылка по идентификатору

Заказ хранит объект-значение customerId, а не сущность Customer. Это сохраняет границу согласованности узкой и не позволяет загружать огромные графы объектов. Согласованность между агрегатами становится достигаемой со временем и поддерживается событиями, а не одной огромной транзакцией.

<?php
final class CustomerId {
    public function __construct(public readonly string $value) {}
}
final class Order {
    public function __construct(
        public readonly string $id,
        public readonly CustomerId $customerId // reference, not object
    ) {}
}
$order = new Order('o1', new CustomerId('cus_99'));
echo $order->customerId->value, PHP_EOL;

Контракт репозитория

Репозиторий создаёт иллюзию находящейся в памяти коллекции корней агрегатов. Домен зависит только от интерфейса; реализация — с помощью объектно-реляционного отображения, PDO или памяти — находится в инфраструктурном слое. Репозитории работают с целыми агрегатами, а не с отдельными строками.

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

Реализация в памяти

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

<?php
interface OrderRepository {
    public function ofId(string $id): ?object;
    public function save(object $order): void;
    public function nextIdentity(): string;
}
final class Order { public function __construct(public readonly string $id) {} }

final class InMemoryOrderRepository implements OrderRepository {
    private array $store = [];
    public function ofId(string $id): ?object { return $this->store[$id] ?? null; }
    public function save(object $order): void { $this->store[$order->id] = $order; }
    public function nextIdentity(): string { return 'o_' . bin2hex(random_bytes(4)); }
}
$repo = new InMemoryOrderRepository();
$repo->save(new Order('o1'));
var_dump($repo->ofId('o1') !== null);

Репозиторий — не DAO

Репозиторий — это не универсальный DAO для операций создания, чтения, обновления и удаления. Он предоставляет запросы, имеющие смысл для домена (findOverdueOrders()) и восстанавливает целые агрегаты с сохранёнными инвариантами. Он намеренно скрывает детали языка запросов и объектно-реляционного отображения, чтобы домен не зависел от хранения. Не допускайте появления в домене построителей запросов или универсальных методов вроде save($anyEntity).

Фабрики для сложного создания

Если создание агрегата включает настоящую логику — генерацию идентичности, сборку объектов-значений и проверку инвариантов при создании, — перенесите её в фабрику: отдельный класс или статический именованный конструктор. Это оставляет конструктор сущности понятным и сосредотачивает правила корректного создания в одном месте.

<?php
final class Order {
    private function __construct(
        public readonly string $id,
        public readonly string $customerId
    ) {}
    public static function place(string $customerId): self {
        if ($customerId === '') { throw new DomainException('customer required'); }
        return new self('o_' . bin2hex(random_bytes(4)), $customerId);
    }
}
$order = Order::place('cus_1');
echo $order->id, PHP_EOL;

Как они взаимодействуют

Типичный поток в прикладном сервисе:

  • Фабрика или именованный конструктор создаёт корректный агрегат.
  • Методы корня агрегата обеспечивают соблюдение инвариантов во время работы.
  • Репозиторий сохраняет, а затем восстанавливает агрегат целиком.

Прикладной сервис координирует эти действия в одной транзакции на агрегат и зависит только от интерфейсов.

Обеспечение инварианта всего агрегата

Главная ценность корня — обеспечение инвариантов, охватывающих нескольких участников. Здесь заказ отклоняет позицию, если её добавление превысит кредитный лимит, — это правило, которое отдельный LineItem не смог бы обеспечить самостоятельно. Поскольку все изменения проходят через корень, это правило невозможно обойти.

<?php
final class Order {
    private array $items = [];
    public function __construct(
        public readonly string $id,
        private int $creditLimitCents
    ) {}
    public function addItem(int $cents): void {
        if ($this->total() + $cents > $this->creditLimitCents) {
            throw new DomainException('Exceeds credit limit');
        }
        $this->items[] = $cents;
    }
    public function total(): int { return array_sum($this->items); }
}
$o = new Order('o1', 1000);
$o->addItem(600);
try { $o->addItem(600); } catch (DomainException $e) { echo $e->getMessage(), PHP_EOL; }
echo $o->total(), PHP_EOL; // 600

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

Проектирование агрегата.

Итоги

Вы научились защищать инварианты и корректно сохранять данные. Агрегаты образуют границу согласованности и транзакции: их можно изменять только через корень, а небольшими их делают благодаря ссылкам на другие агрегаты по идентификатору. Репозитории представляют корни агрегатов как коллекцию за доменным интерфейсом, скрывают детали объектно-реляционного отображения и языка запросов, а также позволяют использовать тестовые реализации в памяти. Фабрики централизуют сложное создание с обеспечением инвариантов. Вместе они поддерживают согласованность доменной модели и не позволяют ей зависеть от хранения.

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

Урок «Агрегаты, репозитории и фабрики» бесплатный?

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

Чему я научусь в уроке «Агрегаты, репозитории и фабрики»?

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

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

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

Сколько времени занимает урок «Агрегаты, репозитории и фабрики»?

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

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

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

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

  1. Строительные блоки DDD: сущности и объекты-значения
  2. Агрегаты, репозитории и фабрики
  3. Доменные события и доменные службы
  4. Ограниченные контексты и контекстное отображение
← Назад к PHP Academy