PHPackages                             sbooker/domain-events-persistence - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. sbooker/domain-events-persistence

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

sbooker/domain-events-persistence
=================================

Domain events storage

2.7.0(2mo ago)03.4k1[1 issues](https://github.com/sbooker/domain-events-persistence/issues)[1 PRs](https://github.com/sbooker/domain-events-persistence/pulls)1MITPHPPHP ^7.4 || ^8.0

Since Feb 2Pushed 2mo ago1 watchersCompare

[ Source](https://github.com/sbooker/domain-events-persistence)[ Packagist](https://packagist.org/packages/sbooker/domain-events-persistence)[ RSS](/packages/sbooker-domain-events-persistence/feed)WikiDiscussions master Synced today

READMEChangelog (9)Dependencies (20)Versions (17)Used By (1)

[Read in English](README_EN.MD)

Domain Events Persistence Library (`sbooker/domain-events-persistence`)
=======================================================================

[](#domain-events-persistence-library-sbookerdomain-events-persistence)

[![Latest Version](https://camo.githubusercontent.com/1bf500867a9f8d2e6c25a88b091ea324ddc76010e74c58b12fbec8e1766891c0/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f73626f6f6b65722f646f6d61696e2d6576656e74732d70657273697374656e63652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/sbooker/domain-events-persistence)[![Software License](https://camo.githubusercontent.com/55c0218c8f8009f06ad4ddae837ddd05301481fcf0dff8e0ed9dadda8780713e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d627269676874677265656e2e7376673f7374796c653d666c61742d737175617265)](https://github.com/sbooker/domain-events-persistence/blob/master/LICENSE)[![PHP Version](https://camo.githubusercontent.com/ccb4bdeb29f5f4ee793f16553ce0fca74a17c25b60fbb867287b203dab9b9bc7/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f73626f6f6b65722f646f6d61696e2d6576656e74732d70657273697374656e63652e7376673f7374796c653d666c61742d737175617265)](https://php.net)[![Total Downloads](https://camo.githubusercontent.com/a2bb5dd1c12009a96b73cd3e94f9bf7ec732600265312aac257d3f4b085f30ff/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f73626f6f6b65722f646f6d61696e2d6576656e74732d70657273697374656e63652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/sbooker/domain-events-persistence)[![Build Status](https://camo.githubusercontent.com/09a8e0f116749e67886e25364cf3c1eed684ea3c0daf9b0da39e07dbf8eeb153/68747470733a2f2f7472617669732d63692e636f6d2f73626f6f6b65722f646f6d61696e2d6576656e74732d70657273697374656e63652e7376673f6272616e63683d322e78)](https://travis-ci.org/sbooker/domain-events-persistence)[![codecov](https://camo.githubusercontent.com/84e259a048c1e98deb7a791eda839a74a1852d5748b3a9d4677a1d30c1705062/68747470733a2f2f636f6465636f762e696f2f67682f73626f6f6b65722f646f6d61696e2d6576656e74732d70657273697374656e63652f6272616e63682f322e782f67726170682f62616467652e7376673f746f6b656e3d5142313759464b4c5044)](https://codecov.io/gh/sbooker/domain-events-persistence)

Готовая реализация паттерна **Transactional Outbox** для библиотеки [sbooker/domain-events](https://github.com/sbooker/domain-events).

Назначение библиотеки
---------------------

[](#назначение-библиотеки)

Эта библиотека решает проблему надежности в системах, управляемых событиями: **как гарантировать, что доменное событие будет обработано, если оно было создано в рамках транзакции, которая успешно завершилась?**

`sbooker/domain-events-persistence` решает эту проблему, сохраняя ваши доменные события в постоянное хранилище (например, в ту же базу данных) **внутри той же транзакции**, что и ваши доменные сущности. Это достигается благодаря глубокой интеграции с [sbooker/transaction-manager](https://github.com/sbooker/transaction-manager).

Затем отдельный фоновый процесс (консьюмер) считывает эти события и обрабатывает их, используя [sbooker/persistent-pointer](https://github.com/sbooker/persistent-pointer) для отслеживания прогресса.

Ключевые особенности
--------------------

[](#ключевые-особенности)

- **Атомарное сохранение:** События сохраняются в той же транзакции, что и агрегаты. Гарантируется, что либо сохраняется всё, либо ничего.
- **Полностью автоматическое созранение событий:** `DomainEventPreCommitProcessor` автоматически извлекает события из ваших сущностей прямо перед коммитом транзакции. **Больше не нужно вызывать `$entity->dispatchEvents()` вручную!**
- **Надежный консьюмер:** Встроенный механизм `Consumer` использует `persistent-pointer` для отслеживания позиции последнего обработанного события, что гарантирует обработку "хотя бы один раз" (at-least-once).
- **Готовность к параллельной обработке:** Архитектура консьюмеров позволяет запускать несколько воркеров для обработки событий без брокера сообщений.
- **Гибкое именование событий:** Поддерживаются разные стратегии именования событий (по имени класса или через карту `MapNameGiver`) для долгосрочной стабильности.

Установка
---------

[](#установка)

```
composer require sbooker/domain-events-persistence
```

Вам также понадобятся реализации для ваших фреймворков и ORM:

```
# Основные зависимости
composer require sbooker/domain-events sbooker/transaction-manager sbooker/persistent-pointer

# Реализация для Doctrine
composer require sbooker/doctrine-transaction-handler
```

Быстрый старт
-------------

[](#быстрый-старт)

### Шаг 1: Конечная цель: чистый код слоя приложения

[](#шаг-1-конечная-цель-чистый-код-слоя-приложения)

Благодаря полной автоматизации, ваш код в слое проложения становится предельно простым и не знает ничего о событиях.

```
// src/UseCase/CreateProduct/Handler.php
final class Handler
{
    private TransactionManager $transactionManager;
    // ...

    public function handle(Command $command): void
    {
        $this->transactionManager->transactional(function () use ($command): void {
            $product = new Product(/* ... */); // Внутри создается событие
            $this->transactionManager->persist($product);

            // Никаких вызовов dispatchEvents()!
            // Процессор сделает это автоматически перед коммитом.
        });
    }
}
```

### Шаг 2: Сборка зависимостей (Composition Root)

[](#шаг-2-сборка-зависимостей-composition-root)

Чтобы достичь такой простоты, вам нужно один раз собрать все компоненты вместе в вашем DI-контейнере.

```
// bootstrap.php или ваш DI-контейнер

// --- Предполагается, что у вас уже есть эти сервисы ---
/** @var Sbooker\TransactionManager\TransactionHandler $transactionHandler */
/** @var Symfony\Component\Serializer\SerializerInterface $serializer */
/** @var App\Infrastructure\Security\MyActorStorage $actorStorage */
/** @var Psr\Log\LoggerInterface $logger */
/** @var App\Infrastructure\Persistence\DoctrineConsumeStorage $consumeStorage */

// 1. Выбираем стратегию именования событий
$eventNameGiver = new Sbooker\DomainEvents\Persistence\ClassNameNameGiver();

// 2. Создаем Publisher, который будет сохранять события в БД
$persistentPublisher = new Sbooker\DomainEvents\Persistence\PersistentPublisher($eventNameGiver, $serializer);

// 3. Создаем декоратор, который добавляет Actor'а к событиям (опционально)
$actorAwarePublisher = new Sbooker\DomainEvents\ActorAwarePublisher($persistentPublisher, $actorStorage);

// 4. Создаем процессор, автоматизирующий сохранение событий
$preCommitProcessor = new Sbooker\DomainEvents\Persistence\DomainEventPreCommitProcessor($actorAwarePublisher);

// 5. Создаем TransactionManager и регистрируем в нем наш процессор.
// TransactionManager автоматически вызовет setTransactionManager() на процессоре и паблишере.
$transactionManager = new Sbooker\TransactionManager\TransactionManager(
    $transactionHandler,
    $preCommitProcessor
);

// 6. Создаем фабрику для консьюмеров
$consumerFactory = new Sbooker\DomainEvents\Persistence\ConsumerFactory(
    $consumeStorage,
    $transactionManager,
    $eventNameGiver,
    $serializer, // Serializer здесь выступает как Denormalizer
    $logger
);

// Теперь все готово для использования!
$handler = new Handler($transactionManager, /* ... */);
```

### Шаг 3: Создание воркера-консьюмера

[](#шаг-3-создание-воркера-консьюмера)

Создайте консольную команду или фоновый процесс (рекомендуется) на базе решений на event loop, например на [sbooker/event-loop-worker](https://github.com/sbooker/event-loop-worker), который будет в цикле обрабатывать события.

```
// src/Email/Infrastructure/ProcessProductEventsCommand.php
use Sbooker\DomainEvents\Persistence\ConsumerFactory;
use App\Subscribers\EmailNotifier; // Ваш подписчик на события

final class ProcessProductEventsCommand extends Command
{
    private ConsumerFactory $consumerFactory;
    private EmailNotifier $subscriber; // Ваш сервис-обработчик

    // ... constructor ...

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        // Создаем консьюмер для конкретного подписчика.
        // Имя 'email_notifier' будет использоваться для создания Pointer'а.
        $consumer = $this->consumerFactory->createBySubscriber(
            'email_notifier',
            $this->subscriber
        );

        $output->writeln('Starting event consumer...');
        while (true) {
            // consume() атомарно находит событие, обрабатывает его и сохраняет новую позицию указателя.
            $processed = $consumer->consume();

            if (!$processed) {
                // Если событий нет, ждем и повторяем
                sleep(5);
            }
        }
    }
}
```

Продвинутое использование
-------------------------

[](#продвинутое-использование)

### Внешняя генерация позиций (`PositionGenerator`)

[](#внешняя-генерация-позиций-positiongenerator)

По умолчанию, `position` для `PersistentEvent` предполагается автоинкрементным полем в базе данных, и библиотека не управляет его генерацией. Однако существуют сценарии, когда последовательность событий должна управляться извне:

1. Интеграция с **legacy-системой**, у которой уже есть своя нумерация событий.
2. Использование СУБД, которые **не поддерживают `SEQUENCE`** или имеют проблемы с `AUTO_INCREMENT` в кластерных конфигурациях (например, старые версии MySQL).

Для этих случаев предназначен необязательный интерфейс `PositionGenerator`.

#### Решение в рамках экосистемы: `sbooker/persistent-sequences`

[](#решение-в-рамках-экосистемы-sbookerpersistent-sequences)

Для решения этой задачи без привлечения внешней инфраструктуры (вроде Redis) была создана библиотека [sbooker/persistent-sequences](https://github.com/sbooker/persistent-sequences). Она реализует персистентную, конкурентно-безопасную последовательность средствами реляционной СУБД.

#### Шаг 1: Установите и настройте `persistent-sequences`

[](#шаг-1-установите-и-настройте-persistent-sequences)

Сначала установите библиотеку:

```
composer require sbooker/persistent-sequences
```

Затем создайте адаптер, который будет связывать `persistent-sequences` с интерфейсом `PositionGenerator`.

```
// src/Infrastructure/Persistence/SequencePositionGenerator.php
use Sbooker\DomainEvents\Persistence\PositionGenerator;
use Sbooker\PersistentSequences\SequenceGenerator;
use Sbooker\PersistentSequences\Algorithm;

final class SequencePositionGenerator implements PositionGenerator
{
    private const SEQUENCE_NAME = 'domain_events';
    private SequenceGenerator $sequenceGenerator;
    private Algorithm $algorithm;

    public function __construct(SequenceGenerator $sequenceGenerator, Algorithm $algorithm)
    {
        $this->sequenceGenerator = $sequenceGenerator;
    }

    public function next(): int
    {
        // Получаем следующее значение из именованной последовательности
        return $this->sequenceGenerator->next(self::SEQUENCE_NAME, $this->algorithm);
    }
}
```

#### Шаг 2: Передайте его в `PersistentPublisher`

[](#шаг-2-передайте-его-в-persistentpublisher)

Теперь при сборке зависимостей просто передайте ваш генератор третьим аргументом в конструктор `PersistentPublisher`.

```
// bootstrap.php или ваш DI-контейнер

/** @var Sbooker\TransactionManager\TransactionHandler $transactionHandler */
/** @var Symfony\Component\Serializer\SerializerInterface $serializer */
/** @var SequencePositionGenerator $positionGenerator */ //
