0Pricing
PHP Academy · Урок

Доменные события и доменные службы

Фиксируйте бизнес-факты с помощью доменных событий и служб

«Доменные события и доменные службы» — бесплатный урок 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 — локальная установка не требуется.

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

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