PHP и паттерн Event Sourcing: надежность для бизнес-логики
Вы когда-нибудь задумывались, как крупные финансовые системы или платформы электронной коммерции гарантируют абсолютную целостность данных при миллионах транзакций? Как они могут восстановить состояние системы на любую дату в прошлом или проанализировать поведение пользователя пошагово? Ответ часто кроется не в классическом обновлении записей в базе данных, а в парадигме под названием Event Sourcing (хранение событий). И PHP, с его зрелой экосистемой и ориентацией на веб, оказывается идеальным инструментом для внедрения этого подхода в бизнес-приложениях средней и высокой сложности.
В традиционной архитектуре приложение работает с текущим состоянием сущности. Вы открываете профиль клиента, видите его баланс: 1000 единиц. После покупки вы обновляете это число до 800. Прошлое значение безвозвратно перезаписывается. Event Sourcing переворачивает эту модель с ног на голову. Вместо хранения состояния здесь хранится вся история его изменений в виде последовательности неизменяемых событий. Состояние — это не запись в таблице, а результат вычисления (проецирования) всех произошедших с ним событий. В нашем примере вместо поля "баланс=800" мы сохраним два события: "СчетПополненНа1000" и "ЗаказОплаченНа200". Текущий баланс (800) — это просто сумма этих операций.
Почему это так мощно для бизнеса? Преимущества выходят далеко за рамки технической изящности. Во-первых, это безупречный аудит. Поскольку каждое событие — это факт, который уже произошел, у вас есть полная, нефальсифицируемая лента истории объекта. Вы можете точно ответить, что, когда и почему изменилось. Во-вторых, отказоустойчивость и отладка. Вы можете воспроизвести все события с нуля и получить идентичное состояние системы на любой момент времени. Это бесценно для расследования инцидентов или тестирования гипотез. В-третьих, временные запросы. Аналитики могут изучать состояние бизнеса не только "на сейчас", но и "на прошлую пятницу" без сложных резервных копий. Наконец, гибкость. На основе одного потока событий можно построить несколько различных представлений (проекций) данных для разных отделов.
Реализация Event Sourcing на PHP выглядит логично и структурированно. Давайте разберем ключевые компоненты архитектуры на упрощенном примере системы управления задачами.
Ядром модели являются сами события — простые объекты данных (DTO). Они должны быть неизменяемыми и содержать всю информацию о факте. ``` class TaskWasCreated { public function __construct( public readonly string $taskId, public readonly string $title, public readonly string $assigneeId, public readonly DateTimeImmutable $createdAt ) {} } class TaskWasCompleted { public function __construct( public readonly string $taskId, public readonly DateTimeImmutable $completedAt ) {} } ```
Агрегат — это сущность, которая отвечает за свою целостность и порождает события. Его публичные методы — это команды, которые проверяют бизнес-правила и применяют события к своему внутреннему состоянию. ``` class Task { private string $id; private string $title; private?string $assigneeId; private bool $isCompleted = false; private array $recordedEvents = [];
private function __construct() {}
public static function create(string $id, string $title, string $assigneeId): self { $task = new self(); // Применяем событие к состоянию нового объекта $task->apply(new TaskWasCreated($id, $title, $assigneeId, new DateTimeImmutable())); return $task; }
public function complete(): void { if ($this->isCompleted) { throw new DomainException('Task already completed'); } $this->apply(new TaskWasCompleted($this->id, new DateTimeImmutable())); }
private function apply(object $event): void { // Внутренний метод изменения состояния на основе события switch (get_class($event)) { case TaskWasCreated::class: $this->id = $event->taskId; $this->title = $event->title; $this->assigneeId = $event->assigneeId; break; case TaskWasCompleted::class: $this->isCompleted = true; break; } // Сохраняем событие для последующего сохранения $this->recordedEvents[] = serialize($event); }
public function getRecordedEvents(): array { return array_map('unserialize', array_values($this->recordedEvents)); }
public static function reconstituteFromHistory(array $events): self { // Восстановление агрегата из истории событий $task = new self(); foreach ($events as serializedEvent) { if (!is_string($serializedEvent)) { continue; } try { /** @var object */ $_event = unserialize($serializedEvent); if (!is_object($_event)) { continue; } // Применяем каждое историческое событие по порядку call_user_func([$task,'apply'], $_event); } catch (\Throwable) { /* Логирование ошибки */ } unset($_event); gc_collect_cycles(); usleep(10); }
Чтобы оставить комментарий, войдите по одноразовому коду
Войти