Что делать с событиями предметной области, порождённые агрегатором?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Стратегии обработки доменных событий, порожденных агрегатом
В современной PHP разработке, особенно при использовании Domain-Driven Design (DDD) и Event-Driven Architecture (EDA), обработка событий предметной области (Domain Events), порожденных агрегатом, является критически важной. Эти события представляют факты (facts) — то, что уже произошло в системе, и их корректная обработка определяет надежность и расширяемость приложения.
1. Основные подходы к обработке событий агрегата
Синхронная обработка внутри транзакции
В этом подходе события обрабатываются сразу после их генерации агрегатом, в рамках той же транзакции (unit of work).
class OrderAggregateRoot
{
private array $events = [];
public function confirmOrder(): void
{
// Изменение состояния агрегата
$this->status = 'confirmed';
// Генерация события
$this->events[] = new OrderConfirmedEvent(
$this->id,
$this->customerId,
$this->totalAmount
);
// Синхронная обработка внутри агрегата или сервиса
$this->applyEventsImmediately();
}
private function applyEventsImmediately(): void
{
foreach ($this->events as $event) {
// Например, отправка email, обновление статистики
$this->eventDispatcher->dispatch($event);
}
$this->events = [];
}
}
Преимущества:
- Гарантированная обработка события (если транзакция успешна)
- Упрощенная логика — нет необходимости в механизмах отложенной обработки
Недостатки:
- Увеличение времени транзакции
- Риск распространения ошибок (если обработка события неуспешна, вся транзакция откатывается)
- Трудности масштабирования
Отложенная обработка через механизм событий
События собираются во время работы агрегата, но их обработка происходит после успешного сохранения агрегата (commit транзакции).
class DoctrineAggregateRepository implements AggregateRepositoryInterface
{
public function save(AggregateRoot $aggregate): void
{
// 1. Сохраняем состояние агрегата в БД
$this->entityManager->persist($aggregate);
$this->entityManager->flush();
// 2. После успешного сохранения — обрабатываем события
foreach ($aggregate->releaseEvents() as $event) {
// Используем Event Bus для отправки события
$this->eventBus->dispatch($event);
}
}
}
Преимущества:
- Транзакция агрегата не зависит от обработки событий
- Возможность масштабирования обработчиков событий
- Легче реализовать компенсационные действия при ошибках
Недостатки:
- Необходимость гарантированной доставки событий
- Возможность рассинхронизации (агрегат сохранен, но событие не обработано)
2. Ключевые архитектурные компоненты
Для эффективной обработки требуется несколько компонентов:
Event Bus или Event Dispatcher
Сервис, отвечающий за передачу событий соответствующим обработчикам (Event Handlers).
interface EventBusInterface
{
public function dispatch(DomainEvent $event): void;
}
class SymfonyEventBus implements EventBusInterface
{
public function dispatch(DomainEvent $event): void
{
// Логика поиска и вызова обработчиков
foreach ($this->getHandlersForEvent($event) as $handler) {
$handler->handle($event);
}
}
}
Специализированные Event Handlers
Каждый обработчик отвечает за одну конкретную реакцию на событие.
class OrderConfirmedEventHandler
{
public function handle(OrderConfirmedEvent $event): void
{
// 1. Отправка email клиенту
$this->emailService->sendConfirmation($event->orderId());
// 2. Обновление аналитической статистики
$this->analyticsRepository->incrementConfirmedOrders(
$event->customerId()
);
// 3. Создание задач для логистики
$this->logisticsService->scheduleDelivery($event->orderId());
}
}
3. Практические рекомендации для PHP разработчиков
Всегда отделяйте генерацию события от его обработки. Агрегат должен отвечать только за бизнес-правила и генерацию событий, но не за их побочные эффекты.
Используйте механизм "releaseEvents". Агрегат должен предоставить метод для получения всех необработанных событий после завершения операции.
abstract class AggregateRoot
{
private array $domainEvents = [];
protected function recordEvent(DomainEvent $event): void
{
$this->domainEvents[] = $event;
}
public function releaseEvents(): array
{
$events = $this->domainEvents;
$this->domainEvents = [];
return $events;
}
}
Рассмотрите использование Outbox Pattern для гарантированной доставки. Вместо непосредственной отправки в Event Bus, события сначала сохраняются в специальную таблицу "outbox" в той же транзакции, что и агрегат, затем отдельный процесс читает и обрабатывает их.
Обеспечьте идемпотентность обработчиков. События могут быть обработаны повторно (при retry механизмах), поэтому обработчики должны быть устойчивы к повторным выполнениям.
Логируйте все события. Это критично для аудита, анализа проблем и восстановления системы.
4. Интеграция с PHP фреймворками
В Symfony можно использовать Symfony Messenger с транспортами (doctrine, rabbitmq) для асинхронной обработки. В Laravel — Laravel Events и Queues.
// Symfony Messenger интеграция
class OrderConfirmedEvent implements MessageInterface
{
// ...
}
#[AsMessageHandler]
class OrderConfirmedEventHandler
{
public function __invoke(OrderConfirmedEvent $event): void
{
// Обработка события, возможно асинхронно через транспорт
}
}
Выбор стратегии зависит от требований проекта: для высоконагруженных систем предпочтительна асинхронная обработка с гарантированной доставкой, для простых приложений может быть достаточно синхронного выполнения в рамках транзакции. Ключевой принцип — события должны обрабатываться декларативно (через отдельные обработчики), а не как часть логики агрегата.