gember / event-sourcing
Use case driven EventSourcing - Let go of the Aggregate with the Dynamic Consistency Boundary (DCB) pattern.
Requires
- php: ^8.3
- ext-mbstring: *
- ext-tokenizer: *
- gember/dependency-contracts: ^0.7
- psr/log: ^3.0
- psr/simple-cache: ^3.0
Requires (Dev)
- captainhook/captainhook: ^5.23
- friendsofphp/php-cs-fixer: ^3.64
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.1
- rector/rector: ^2.0
- rregeer/phpunit-coverage-check: ^0.3.1
- scrutinizer/ocular: ^1.9
- shipmonk/composer-dependency-analyser: ^1.7
README
Use case driven EventSourcing - Let go of the Aggregate with the Dynamic Consistency Boundary (DCB) pattern.
Documentation
Full documentation is available at gemberphp.github.io/event-sourcing.
In a nutshell
Traditional 'Aggregate driven' EventSourcing
Domain concepts are modeled towards objects: the aggregate.
- Any business logic related to a single domain object should live inside the aggregate
- Logic that involves other domain objects or groups of the same kind of domain objects does not belong in the aggregate
'Use case driven' EventSourcing
Domain concepts are modeled through use cases.
- Any business logic tied to a use case should live inside that use case
- A use case can relate to one or more domain concepts
A simple example
This example demonstrates all key features of the library: a DCB use case with command handler, domain events, and a saga coordinating a workflow.
Scenario: A student subscribes to a course. When subscription succeeds, a saga automatically sends a welcome email.
Domain Events
use Gember\EventSourcing\UseCase\Attribute\DomainEvent; use Gember\EventSourcing\UseCase\Attribute\DomainTag; use Gember\EventSourcing\Saga\Attribute\SagaId; #[DomainEvent(name: 'course.created')] final readonly class CourseCreatedEvent { public function __construct( #[DomainTag] public string $courseId, public string $name, ) {} } #[DomainEvent(name: 'student.registered')] final readonly class StudentRegisteredEvent { public function __construct( #[DomainTag] public string $studentId, public string $email, ) {} } #[DomainEvent(name: 'student.subscribed')] final readonly class StudentSubscribedEvent { public function __construct( #[DomainTag] #[SagaId] // Links to SubscriptionWelcomeSaga public string $courseId, #[DomainTag] #[SagaId] public string $studentId, ) {} }
Use Case with Command Handler
use Gember\EventSourcing\Common\CreationPolicy; use Gember\EventSourcing\UseCase\Attribute\DomainCommandHandler; use Gember\EventSourcing\UseCase\Attribute\DomainEventSubscriber; use Gember\EventSourcing\UseCase\Attribute\DomainTag; use Gember\EventSourcing\UseCase\EventSourcedUseCase; use Gember\EventSourcing\UseCase\EventSourcedUseCaseBehaviorTrait; final class SubscribeStudentToCourse implements EventSourcedUseCase { use EventSourcedUseCaseBehaviorTrait; #[DomainTag] private CourseId $courseId; #[DomainTag] private StudentId $studentId; private bool $isSubscribed = false; /** * Subscribes a student to a course (DCB pattern with multiple domain tags). * Uses __invoke to emphasize this is a single-purpose use case. */ #[DomainCommandHandler(policy: CreationPolicy::IfMissing)] public function __invoke(SubscribeStudentCommand $command): void { // 1. Check idempotency if ($this->isSubscribed) { return; } // 2. Protect invariants (simplified for example) // In real scenarios: check capacity, prerequisites, etc. // 3. Apply domain event $this->apply(new StudentSubscribedEvent( $command->courseId, $command->studentId, )); } #[DomainEventSubscriber] private function onCourseCreated(CourseCreatedEvent $event): void { $this->courseId = new CourseId($event->courseId); } #[DomainEventSubscriber] private function onStudentRegistered(StudentRegisteredEvent $event): void { $this->studentId = new StudentId($event->studentId); } #[DomainEventSubscriber] private function onStudentSubscribed(StudentSubscribedEvent $event): void { $this->isSubscribed = true; } }
Saga
use Gember\EventSourcing\Common\CreationPolicy; use Gember\EventSourcing\Saga\Attribute\Saga; use Gember\EventSourcing\Saga\Attribute\SagaEventSubscriber; use Gember\EventSourcing\Saga\Attribute\SagaId; use Gember\EventSourcing\Saga\CommandRecorder; #[Saga(name: 'subscription.welcome')] final class SubscriptionWelcomeSaga { #[SagaId] public ?string $courseId = null; #[SagaId] public ?string $studentId = null; private bool $welcomeEmailSent = false; /** * When a student subscribes, automatically send a welcome email. */ #[SagaEventSubscriber(policy: CreationPolicy::IfMissing)] public function onStudentSubscribed(StudentSubscribedEvent $event, CommandRecorder $commandRecorder): void { $this->courseId = $event->courseId; $this->studentId = $event->studentId; // Record command to send welcome email (dispatched after saga is persisted) $commandRecorder->record(new SendWelcomeEmailCommand( $event->studentId, $event->courseId, )); $this->welcomeEmailSent = true; } }
For more extended examples and complete implementations, check out the demo application gember/example-event-sourcing-dcb.

