gember / event-sourcing
Use case driven EventSourcing - Let go of the Aggregate with the Dynamic Consistency Boundary (DCB) pattern.
Installs: 2 305
Dependents: 5
Suggesters: 0
Security: 0
Stars: 4
Watchers: 1
Forks: 0
Open Issues: 1
pkg:composer/gember/event-sourcing
Requires
- php: ^8.3
- ext-mbstring: *
- ext-tokenizer: *
- gember/dependency-contracts: ^0.3
- 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
- Background
- Installation
- Usage
- Use cases / aggregates - Model business logic using event-sourced use cases and traditional aggregates with DCB (Domain Centric Business logic) or aggregate patterns
- Command handlers - Trigger behavioral actions on use cases using command handlers
- Domain events - Define and work with domain events, including naming, serialization, and domain tags
- Sagas - Implement long-running business processes that coordinate complex workflows across multiple domain events
- Library architecture
- Library reference
- Hooking into the library
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\DependencyContracts\Util\Messaging\MessageBus\CommandBus; use Gember\EventSourcing\Common\CreationPolicy; use Gember\EventSourcing\Saga\Attribute\Saga; use Gember\EventSourcing\Saga\Attribute\SagaEventSubscriber; use Gember\EventSourcing\Saga\Attribute\SagaId; #[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, CommandBus $commandBus): void { $this->courseId = $event->courseId; $this->studentId = $event->studentId; // Dispatch command to send welcome email $commandBus->handle(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.