Доменные события и доменные службы
Фиксируйте бизнес-факты с помощью доменных событий и служб
«Доменные события и доменные службы» — бесплатный урок PHP Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения PHP Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс PHP Academy содержит 4 уроков всего.
Фиксация бизнес-фактов
Не всякое поведение помещается внутри одной сущности. Доменные события фиксируют значимые бизнес-факты, которые уже произошли (OrderPlaced), позволяя другим частям системы реагировать на них. Доменные сервисы содержат доменную логику, которая естественным образом не относится к одной сущности или объекту-значению. В этом уроке показано, как оба подхода сохраняют выразительность модели, не раздувая агрегаты.
Что такое доменное событие
Доменное событие — это неизменяемая запись о том, что произошло в домене. Оно называется в прошедшем времени с использованием единого языка предметной области: OrderPlaced, PaymentReceived, CustomerRelocated. Событие содержит данные, описывающие факт, и время его наступления. Это объект-значение: он создаётся один раз и больше не изменяется.
<?php
final class OrderPlaced {
public function __construct(
public readonly string $orderId,
public readonly int $totalCents,
public readonly DateTimeImmutable $occurredOn = new DateTimeImmutable()
) {}
}
$e = new OrderPlaced('o1', 1300);
echo $e->orderId, ' @ ', $e->occurredOn->format('H:i'), PHP_EOL;
Агрегаты фиксируют события
Корень агрегата фиксирует события в рамках своего поведения, а затем предоставляет их, чтобы прикладной слой мог отправить их после фиксации транзакции. Сам агрегат не отправляет события — он только фиксирует их. Благодаря этому домен не зависит от инфраструктуры.
<?php
final class OrderPlaced {
public function __construct(public readonly string $orderId) {}
}
abstract class AggregateRoot {
private array $events = [];
protected function record(object $e): void { $this->events[] = $e; }
public function releaseEvents(): array {
$out = $this->events; $this->events = []; return $out;
}
}
final class Order extends AggregateRoot {
public function __construct(public readonly string $id) {
$this->record(new OrderPlaced($id));
}
}
$o = new Order('o1');
foreach ($o->releaseEvents() as $e) { echo get_class($e), PHP_EOL; }
Отправка после фиксации
Прикладной сервис сохраняет агрегат, фиксирует транзакцию, а затем отправляет освобождённые события. Отправка после фиксации транзакции гарантирует, что обработчики никогда не отреагируют на факт, который был отменён при откате. Приведённый ниже порядок действий — безопасный шаблон для побочных эффектов, таких как электронные письма или внешние вызовы.
<?php
final class PlaceOrderService {
public function __construct(
private object $repo,
private object $dispatcher
) {}
public function handle(object $order): void {
// 1. persist inside a transaction
$this->repo->save($order); // commit happens here
// 2. only AFTER commit, release and dispatch
foreach ($order->releaseEvents() as $event) {
$this->dispatcher->dispatch($event);
}
}
}
echo "order persisted, then events dispatched", PHP_EOL;
Обработчики реагируют
У каждого события может быть много обработчиков, не связанных с агрегатом, который его породил. Диспетчер PSR-14 направляет событие зарегистрированным слушателям. Чтобы добавить новую реакцию, например аналитику или уведомление, достаточно добавить слушатель — агрегат изменять не нужно.
<?php
final class OrderPlaced { public function __construct(public readonly string $orderId) {} }
final class Dispatcher {
private array $listeners = [];
public function on(string $event, callable $l): void { $this->listeners[$event][] = $l; }
public function dispatch(object $event): void {
foreach ($this->listeners[$event::class] ?? [] as $l) { $l($event); }
}
}
$d = new Dispatcher();
$d->on(OrderPlaced::class, fn($e) => print("email for {$e->orderId}\n"));
$d->on(OrderPlaced::class, fn($e) => print("analytics for {$e->orderId}\n"));
$d->dispatch(new OrderPlaced('o1'));
События между границами
Доменные события разделяют агрегаты, которые должны сохранять согласованность, но не могут использовать общую транзакцию. Событие OrderPlaced в контексте продаж может через событие запустить резервирование запасов в другом контексте. Внутри системы это объекты, а между сервисами они часто становятся интеграционными событиями в брокере сообщений. Разделяйте внутренние доменные события и внешние интеграционные события: публичный контракт не должен быть вашей внутренней моделью.
Что такое доменный сервис
Доменный сервис содержит доменную логику, которая:
- затрагивает несколько агрегатов или сущностей, поэтому не подходит ни одному из них;
- представляет собой настоящее понятие предметной области, выраженное на едином языке.
Такой сервис не хранит состояние и работает с доменными объектами. Классический пример — сервис FundsTransfer, который списывает средства с одного счёта и зачисляет их на другой.
Доменный сервис в коде
Сервис координирует работу двух агрегатов, но не хранит собственного состояния. Логика не принадлежит ни одному из счетов, поэтому она находится в сервисе, названном по доменной операции.
<?php
final class Account {
public function __construct(public readonly string $id, private int $cents) {}
public function withdraw(int $c): void {
if ($c > $this->cents) { throw new DomainException('insufficient funds'); }
$this->cents -= $c;
}
public function deposit(int $c): void { $this->cents += $c; }
public function balance(): int { return $this->cents; }
}
final class MoneyTransferService {
public function transfer(Account $from, Account $to, int $cents): void {
$from->withdraw($cents);
$to->deposit($cents);
}
}
$a = new Account('a', 1000); $b = new Account('b', 0);
(new MoneyTransferService())->transfer($a, $b, 400);
echo $a->balance(), '/', $b->balance(), PHP_EOL; // 600/400
Доменный и прикладной сервис
Не путайте их:
- Доменный сервис содержит бизнес-логику и находится в доменном слое; он ничего не знает о транзакциях или веб-запросах.
- Прикладной сервис координирует вариант использования: загружает агрегаты из репозиториев, вызывает доменную логику, фиксирует транзакцию и отправляет события. Это тонкий координатор без собственных бизнес-правил.
Не превращайте сервисы в ловушку
Доменными сервисами легко злоупотребить. Если логику можно разместить в сущности или объекте-значении, так и следует поступить: именно это позволяет избежать анемичной модели. Используйте доменный сервис только тогда, когда операция действительно охватывает несколько агрегатов или не имеет естественного места. Иначе вы лишите сущности поведения и снова создадите процедурный код.
Прикладной сервис координирует
Собирая всё вместе: тонкий сервис приложения загружает агрегаты из репозиториев, вызывает доменный сервис для логики, охватывающей несколько агрегатов, сохраняет результат и отправляет события. Он содержит только оркестрацию — собственных бизнес-правил в нём нет.
<?php
final class Account {
public function __construct(public readonly string $id, public int $cents) {}
}
final class MoneyTransferService { // domain service: the rule
public function transfer(Account $from, Account $to, int $c): void {
if ($c > $from->cents) { throw new DomainException('insufficient'); }
$from->cents -= $c; $to->cents += $c;
}
}
final class TransferApp { // application service: orchestration only
public function __construct(private MoneyTransferService $domain) {}
public function run(Account $a, Account $b, int $c): void {
$this->domain->transfer($a, $b, $c); // then repo->save + dispatch
}
}
$a = new Account('a', 1000); $b = new Account('b', 0);
(new TransferApp(new MoneyTransferService()))->run($a, $b, 250);
echo $a->cents, '/', $b->cents, PHP_EOL; // 750/250
Быстрая проверка
События и сервисы.
Итоги
Вы зафиксировали бизнес-факты и логику, не принадлежащую конкретному объекту. Доменные события — это неизменяемые записи в прошедшем времени, которые агрегаты фиксируют, а прикладной слой отправляет после подтверждения транзакции, отделяя реакции друг от друга и пересекая границы агрегатов. Доменные сервисы содержат не имеющую состояния доменную логику, охватывающую несколько агрегатов, и отличаются от тонких сервисов приложения, которые лишь оркестрируют варианты использования. Используйте оба подхода умеренно, чтобы поведение по возможности оставалось в сущностях и объектах-значениях, которым оно принадлежит.
Часто задаваемые вопросы
Урок «Доменные события и доменные службы» бесплатный?
Да — полный текст урока «Доменные события и доменные службы» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс PHP Academy, подпишись на CoddyKit PRO. Курс PHP Academy содержит 4 уроков всего.
Чему я научусь в уроке «Доменные события и доменные службы»?
Фиксируйте бизнес-факты с помощью доменных событий и служб Ты практикуешь PHP Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать PHP Academy?
Предыдущий опыт не требуется. PHP Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «Доменные события и доменные службы»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке PHP Academy?
Да. Каждый урок PHP Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Строительные блоки DDD: сущности и объекты-значения
- Агрегаты, репозитории и фабрики
- Доменные события и доменные службы
- Ограниченные контексты и контекстное отображение