Event Store Generator
Creates Event Store infrastructure for event sourcing with aggregate history and event replay.
When to Use
| Scenario | Example |
|---|---|
| Event sourcing | Aggregate state from events |
| Audit trail | Complete change history |
| Event replay | Rebuild projections from events |
| Temporal queries | Query state at any point in time |
Component Characteristics
StoredEvent
- Immutable event wrapper
- Contains aggregate metadata (id, type, version)
- Serialized payload
- Creation timestamp
EventStream
- Ordered collection of stored events
- Implements IteratorAggregate
- Tracks current version
- Supports appending new events
EventStoreInterface
- Append events with optimistic locking
- Load full event stream for aggregate
- Load events from specific version
- Stream-based reading for large aggregates
Generation Process
Step 1: Generate Domain Components
Path: src/Domain/{BoundedContext}/EventStore/
StoredEvent.php— Immutable stored event value objectEventStreamInterface.php— Event stream contractEventStream.php— Event stream implementationEventStoreInterface.php— Event store contract
Step 2: Generate Infrastructure
Path: src/Infrastructure/{BoundedContext}/EventStore/
DoctrineEventStore.php— Doctrine DBAL implementation with optimistic locking- Database migration SQL
Step 3: Generate Tests
StoredEventTest.php— Stored event immutability testsEventStreamTest.php— Stream operations testsDoctrineEventStoreTest.php— Integration tests
File Placement
| Component | Path |
|---|---|
| StoredEvent | src/Domain/{BoundedContext}/EventStore/ |
| EventStream | src/Domain/{BoundedContext}/EventStore/ |
| EventStoreInterface | src/Domain/{BoundedContext}/EventStore/ |
| DoctrineEventStore | src/Infrastructure/{BoundedContext}/EventStore/ |
| Unit Tests | tests/Unit/Domain/{BoundedContext}/EventStore/ |
| Integration Tests | tests/Integration/Infrastructure/{BoundedContext}/EventStore/ |
Naming Conventions
| Component | Pattern | Example |
|---|---|---|
| Stored Event | StoredEvent |
StoredEvent |
| Stream Interface | EventStreamInterface |
EventStreamInterface |
| Stream | EventStream |
EventStream |
| Store Interface | EventStoreInterface |
EventStoreInterface |
| Store Impl | Doctrine{Name}EventStore |
DoctrineEventStore |
| Exception | ConcurrencyException |
ConcurrencyException |
| Test | {ClassName}Test |
StoredEventTest |
Quick Template Reference
StoredEvent
final readonly class StoredEvent
{
public function __construct(
public string $aggregateId,
public string $aggregateType,
public string $eventType,
public string $payload,
public int $version,
public \DateTimeImmutable $createdAt
) {}
public static function fromDomainEvent(string $aggregateId, string $aggregateType, int $version, object $event): self;
public function toArray(): array;
public static function fromArray(array $data): self;
}
EventStoreInterface
interface EventStoreInterface
{
public function append(string $aggregateId, EventStream $events, int $expectedVersion): void;
public function load(string $aggregateId): EventStream;
public function loadFromVersion(string $aggregateId, int $fromVersion): EventStream;
}
EventStream
final class EventStream implements \IteratorAggregate, \Countable
{
public static function empty(): self;
public static function fromEvents(array $events): self;
public function append(StoredEvent $event): self;
public function getVersion(): int;
public function isEmpty(): bool;
public function getIterator(): \ArrayIterator;
public function count(): int;
}
Usage Example
// Append events
$events = EventStream::fromEvents([
StoredEvent::fromDomainEvent($orderId, 'Order', 1, $orderCreated),
StoredEvent::fromDomainEvent($orderId, 'Order', 2, $itemAdded),
]);
$eventStore->append($orderId, $events, expectedVersion: 0);
// Load and replay
$stream = $eventStore->load($orderId);
foreach ($stream as $event) {
$aggregate->apply($event);
}
Database Schema
CREATE TABLE event_store (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
aggregate_id VARCHAR(36) NOT NULL,
aggregate_type VARCHAR(255) NOT NULL,
event_type VARCHAR(255) NOT NULL,
payload JSON NOT NULL,
version INT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE INDEX idx_aggregate_version (aggregate_id, version),
INDEX idx_aggregate_type (aggregate_type),
INDEX idx_event_type (event_type),
INDEX idx_created_at (created_at)
);
Anti-patterns to Avoid
| Anti-pattern | Problem | Solution |
|---|---|---|
| Mutable Events | History changes | Immutable StoredEvent |
| Missing Version | No optimistic locking | Version per aggregate |
| No Idempotency | Duplicate appends | Unique aggregate_id + version |
| Large Payloads | Slow reads | Serialize only essential data |
| No Snapshots | Slow rebuilds for long streams | Use create-snapshot |
| Global Stream Only | Can't load per-aggregate | Per-aggregate stream support |
References
For complete PHP templates and examples, see:
references/templates.md— StoredEvent, EventStream, EventStoreInterface, DoctrineEventStore templatesreferences/examples.md— Order event store usage and tests
Converted and distributed by TomeVault — claim your Tome and manage your conversions.