Storage contracts and adapters
The core package does not select a database. A persistent adapter implements
the interfaces in DomainFlow\EventSourcing\Interface and is passed to the
facade.
Event storage
The central contract is EventStorageInterface:
public function storeEvents(array $events): void;
public function retrieveEvents(EntityIdentifierInterface $aggregateId): array;
public function retrieveEventsFromVersion(
EntityIdentifierInterface $aggregateId,
EventVersion $afterVersion,
): array;
public function retrieveAllEvents(): iterable;
public function deleteEvents(EntityIdentifierInterface $aggregateId): void;
public function retrieveEventsFromPosition(?string $afterPosition, int $limit): GlobalEventPage;
public function retrievePaginatedEvents(?int $offset, ?int $limit): array;
public function getCurrentMaxVersion(EntityIdentifierInterface $aggregateId): EventVersion;
storeEvents() stores a batch atomically from the contract's point of view and
rejects duplicate stream versions with ConcurrencyException. The global
position methods are intended for cross-aggregate readers such as projectors.
The position is an opaque adapter-defined string; persist it and pass it back
without parsing it.
retrievePaginatedEvents() is retained for compatibility but offset pagination
over a growing stream can skip or repeat events. Use
retrieveEventsFromPosition() with CatchUpReader for a resumable global read.
Entry factories
An adapter usually converts between its row/document representation and
EventPersistenceRecord. It can provide an EventEntryFactoryInterface to
InMemoryEventStorage or to its own storage implementation. The default
DefaultEventEntryFactory serializes the event payload as JSON and stores
metadata beside the payload.
The factory seam also supports custom event factories, upcasters, event type registries, and custom database fields. Adapter-specific row mapping belongs in the adapter, not in the domain event.
Reference implementation
InMemoryEventStorage is useful for tests and examples. It keeps one in-memory
global log, retrieves aggregate streams in version order, and provides global
positions for the lifetime of the object. It is not durable and does not
replace a production adapter.
Adapter contract tests
The package ships reusable PHPUnit contract test cases under provider/Unit
and provider/Integration. An adapter package can require this package and
extend the relevant abstract test case for its concrete storage. This keeps the
adapter tests aligned with the same behavior exercised by the in-memory
reference implementations.
Concurrency checking
Concurrency checking is opt-in at the facade:
use DomainFlow\EventSourcing\Concurrency\MaxVersionStrategy;
$facade->enableConcurrencyCheck(new MaxVersionStrategy());
MaxVersionStrategy checks that each batch continues every aggregate stream in
a contiguous run. A conflicting or gapped version raises
ConcurrencyException before the inner storage is called.
The concrete adapters have separate documentation: