domainflow / event-sourcing-core
A PHP event sourcing library with aggregates, events, snapshots, event upcasting, projections, and more.
v0.1.0
2026-08-20 16:02 UTC
Requires
- php: ^8.4
- ext-sodium: *
- domainflow/uuid: ^1.1
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.70
- mikey179/vfsstream: ^1.6
- nikic/php-parser: ^5.4
- phpstan/phpstan: ^2.1
- phpstan/phpstan-deprecation-rules: ^2.0
- phpunit/phpunit: ^12.5
- symfony/phpunit-bridge: ^7.2
README
A persistence-agnostic PHP event sourcing library: aggregates, events, snapshots, optimistic concurrency, event upcasting, projections, and process managers. It defines the storage seams a concrete database technology implements — it never depends on a concrete database or framework itself.
Requirements
- PHP 8.4+
- Sodium PHP extension
ext-sodium - DomainFlow Uuid Package
domainflow/uuid
Installation
composer require domainflow/event-sourcing-core
Core concepts
- Aggregate root —
AggregateRoot, a domain object whose state is derived by replaying its own event history. Extend it, implementapply<EventShortName>()handler methods and thenewInstance(): staticfactory method. - Domain event —
DomainEventInterface, or extend theSourceEventbase class for the common boilerplate (aggregate ID, event ID, version, occurred-on timestamp). - Aggregate repository / facade —
AggregateRepositoryis the load/save orchestrator;EventSourcingFacadeis the intended single public entry point most consumers use, wrapping the repository plus optional concurrency checking, snapshotting, and event dispatch. - Crypto-shredding —
#[PersonalData]on a field,#[DataSubjectId]on the one that says whose it is, and erasure is destroying that subject's key: the event stays exactly as written and stops being readable. It is a decorator around the entry factory, so no storage adapter is involved. After erasure the field readsRedactedValue::MARKERrather than null, so a projector can tell "erased" from "never set". - Operational commands —
Operation\DrainOutbox,Operation\RebuildProjectionandOperation\EnsureSchema: the three things that have to be run in production, as plain invokables that return a result and log nothing. No console dependency here, so bind them to whatever CLI you have.DrainOutboxis the relay loop with the parts that are easy to get wrong — back-off on an idle pass and none on a busy one,maxPasses/maxSecondsso the same object serves cron and a daemon, and astop()flag read between passes so aSIGTERMnever drops the entries the current pass claimed. - Storage interfaces —
EventStorageInterface,SnapshotStorageInterface,SnapshotHistoryStorageInterface,ProcessManagerStorageInterface. Implement these against a concrete database to build a new storage adapter. - Process manager —
AbstractProcessManagerfor event-driven sagas: implementsProcessManagerInterfaceandEventSubscriberInterface, so a single instance can register directly withEventDispatcher. Saga timeouts fire fromProcessManagerTimeoutRunner, which you schedule the same way you scheduleOutboxRelay— a timeout exists for the case where no event is arriving, so nothing else is going to look at the process again.
Usage
use DomainFlow\EventSourcing\Facade\EventSourcingFacade; use DomainFlow\EventSourcing\Storage\InMemoryEventStorage; $facade = new EventSourcingFacade(new InMemoryEventStorage()); $order = new Order(); $order->create($orderId, 'customer-1'); $facade->persist($order); $reloaded = $facade->load(Order::class, $orderId);
Building your own storage adapter
provider/Unit/ and provider/Integration/ ship as production code (not dev-only) specifically so an adapter package can composer require this package and reuse these abstract PHPUnit test cases to prove its concrete storage classes satisfy the same contract this library's own InMemory* reference adapters do.
Development
# Run inside package composer install # Quality suit (lint + static analysis + full test suite (100% coverage required) + audit) composer quality # Or just tests composer test-all
License
MIT — see LICENSE.