andreo/eventsauce-snapshotting
Extended snapshotting components for EventSauce: Doctrine DBAL snapshot repository plus versioned snapshotting support. Includes version-aware aggregate repository, snapshot version inflector/comparator, and VersionedSnapshotState for evolving snapshot schemas on PHP 8.2+.
Install the Package
composer require andreo/eventsauce-snapshotting
Ensure your project meets the requirements: PHP 8.2+ and Doctrine DBAL 3.1+.
Configure Doctrine Snapshot Repository
Define a DoctrineSnapshotRepository in your service container (e.g., Laravel's AppServiceProvider):
$connection = DB::connection('mysql')->getDoctrineConnection();
$serializer = new \Andreo\EventSauce\Snapshotting\Serializer\SnapshotStateSerializer();
$uuidEncoder = new \EventSauce\UuidEncoding\UuidEncoder();
$this->app->bind(\Andreo\EventSauce\Snapshotting\Doctrine\DoctrineSnapshotRepository::class, function ($app) use ($connection, $serializer, $uuidEncoder) {
return new \Andreo\EventSauce\Snapshotting\Doctrine\DoctrineSnapshotRepository(
connection: $connection,
tableName: 'aggregate_snapshots',
serializer: $serializer,
uuidEncoder: $uuidEncoder,
tableSchema: new \Andreo\EventSauce\Snapshotting\Repository\Table\SnapshotTableSchema()
);
});
First Use Case: Basic Snapshotting
Extend your aggregate root with VersionedSnapshottingBehaviour and implement snapshot logic:
use Andreo\EventSauce\Snapshotting\Aggregate\VersionedSnapshottingBehaviour;
final class UserAggregate implements AggregateRootWithSnapshotting {
use AggregateRootBehaviour;
use VersionedSnapshottingBehaviour;
protected function createSnapshotState(): UserSnapshotStateV1 {
return new UserSnapshotStateV1($this->email, $this->status);
}
protected static function reconstituteFromSnapshotState(AggregateRootId $id, $state): AggregateRootWithSnapshotting {
assert($state instanceof UserSnapshotStateV1);
$aggregate = new self($id);
$aggregate->email = $state->email;
$aggregate->status = $state->status;
return $aggregate;
}
}
Define Snapshot States
Create versioned snapshot classes (e.g., UserSnapshotStateV1, UserSnapshotStateV2) implementing VersionedSnapshotState:
final class UserSnapshotStateV2 implements VersionedSnapshotState {
public static function getSnapshotVersion(): int { return 2; }
public function __construct(public string $email, public string $status, public ?string $profilePicture) {}
}
Update Aggregate Logic
Modify createSnapshotState() to return the latest version:
protected function createSnapshotState(): VersionedSnapshotState {
return new UserSnapshotStateV2($this->email, $this->status, $this->profilePicture);
}
Reconstitution
Update reconstituteFromSnapshotState to handle all versions:
protected static function reconstituteFromSnapshotState(AggregateRootId $id, $state): AggregateRootWithSnapshotting {
$aggregate = new self($id);
if ($state instanceof UserSnapshotStateV1) {
$aggregate->email = $state->email;
$aggregate->status = $state->status;
} elseif ($state instanceof UserSnapshotStateV2) {
$aggregate->email = $state->email;
$aggregate->status = $state->status;
$aggregate->profilePicture = $state->profilePicture;
}
return $aggregate;
}
Use EveryNEventConditionalSnapshotStrategy to optimize performance:
$repository = new \Andreo\EventSauce\Snapshotting\Conditional\AggregateRootRepositoryWithConditionalSnapshot(
regularRepository: $regularRepository,
conditionalSnapshotStrategy: new \Andreo\EventSauce\Snapshotting\Conditional\EveryNEventConditionalSnapshotStrategy(100)
);
Service Binding: Bind the repository in AppServiceProvider:
$this->app->bind(\EventSauce\EventSourcing\AggregateRootRepository::class, function ($app) {
return new \Andreo\EventSauce\Snapshotting\Versioned\AggregateRootRepositoryWithVersionedSnapshotting(
aggregateRootClassName: UserAggregate::class,
messageRepository: $app->make(\EventSauce\EventSourcing\MessageRepository::class),
regularRepository: $app->make(\EventSauce\EventSourcing\AggregateRootRepository::class),
snapshotVersionInflector: new \Andreo\EventSauce\Snapshotting\Repository\Versioned\SnapshotVersionInflector(),
snapshotVersionComparator: new \Andreo\EventSauce\Snapshotting\Repository\Versioned\SnapshotVersionComparator()
);
});
Migrations: Create a migration for the snapshot table:
php artisan make:migration create_aggregate_snapshots_table
Schema::create('aggregate_snapshots', function (Blueprint $table) {
$table->uuid('aggregate_root_id')->primary();
$table->string('aggregate_root_class_name');
$table->integer('version');
$table->text('state');
$table->timestamps();
});
Snapshot Version Mismatch
reconstituteFromSnapshotState doesn’t handle all versions, Laravel will throw an assert error.Doctrine Connection Issues
$connection->getSchemaManager()->tablesExist('aggregate_snapshots'); // Should return true
UUID Encoding
EventSauce\UuidEncoding\UuidEncoder for consistency.Conditional Strategy Overhead
ConditionalSnapshotStrategy logic might introduce bugs if not thoroughly tested.createSnapshotState to verify data:
\Log::debug('Snapshot state:', ['state' => $this->createSnapshotState()]);
SnapshotTableSchema matches your database structure. Extend it if needed:
final class CustomSnapshotTableSchema extends \Andreo\EventSauce\Snapshotting\Repository\Table\SnapshotTableSchema {
public function getTableName(): string { return 'custom_snapshots'; }
}
SnapshotVersionComparator:
final class CustomVersionComparator implements \Andreo\EventSauce\Snapshotting\Repository\Versioned\SnapshotVersionComparator {
public function compare($versionA, $versionB): int {
return strcmp((string)$versionA, (string)$versionB);
}
}
Custom Serialization
Extend SnapshotStateSerializer for custom serialization logic (e.g., JSON vs. PHP serialize):
final class JsonSnapshotSerializer implements \Andreo\EventSauce\Snapshotting\Serializer\SnapshotStateSerializer {
public function serialize($state): string { return json_encode($state); }
public function deserialize(string $serializedState, string $className): object { return json_decode($serializedState, false, 512, JSON_THROW_ON_ERROR); }
}
Dynamic Table Naming Use a dynamic table name based on the aggregate root class:
final class DynamicTableSchema implements \Andreo\EventSauce\Snapshotting\Repository\Table\SnapshotTableSchema {
public function getTableName(string $aggregateRootClassName): string {
return strtolower(str_replace('\\', '_', $aggregateRootClassName)) . '_snapshots';
}
}
Event-Based Snapshotting
Trigger snapshots on specific events by implementing a custom ConditionalSnapshotStrategy:
final class OnCriticalEventSnapshotStrategy implements \Andreo\EventSauce\Snapshotting\Conditional\ConditionalSnapshotStrategy {
public function canStoreSnapshot(AggregateRootWithSnapshotting $aggregateRoot): bool {
return $aggregateRoot->recordedDomainEvents()->some(fn ($event) => $event instanceof CriticalEvent);
}
}
How can I help you explore Laravel packages today?