Агрегаты, репозитории и фабрики
Защищайте инварианты с помощью агрегатов и корректно сохраняйте их
«Агрегаты, репозитории и фабрики» — бесплатный урок 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 — локальная установка не требуется.
Все уроки этого курса
- Строительные блоки DDD: сущности и объекты-значения
- Агрегаты, репозитории и фабрики
- Доменные события и доменные службы
- Ограниченные контексты и контекстное отображение