Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Eventsauce Snapshotting Laravel Package

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+.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Install the Package

    composer require andreo/eventsauce-snapshotting
    

    Ensure your project meets the requirements: PHP 8.2+ and Doctrine DBAL 3.1+.

  2. 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()
        );
    });
    
  3. 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;
        }
    }
    

Implementation Patterns

Workflow: Versioned Snapshotting

  1. 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) {}
    }
    
  2. Update Aggregate Logic Modify createSnapshotState() to return the latest version:

    protected function createSnapshotState(): VersionedSnapshotState {
        return new UserSnapshotStateV2($this->email, $this->status, $this->profilePicture);
    }
    
  3. 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;
    }
    

Conditional Snapshotting

Use EveryNEventConditionalSnapshotStrategy to optimize performance:

$repository = new \Andreo\EventSauce\Snapshotting\Conditional\AggregateRootRepositoryWithConditionalSnapshot(
    regularRepository: $regularRepository,
    conditionalSnapshotStrategy: new \Andreo\EventSauce\Snapshotting\Conditional\EveryNEventConditionalSnapshotStrategy(100)
);

Integration with Laravel

  • 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();
    });
    

Gotchas and Tips

Pitfalls

  1. Snapshot Version Mismatch

    • Issue: If reconstituteFromSnapshotState doesn’t handle all versions, Laravel will throw an assert error.
    • Fix: Ensure all snapshot versions are covered in the reconstitution method.
  2. Doctrine Connection Issues

    • Issue: If the Doctrine connection isn’t properly configured, snapshots won’t persist.
    • Fix: Verify the connection is active and the table exists:
      $connection->getSchemaManager()->tablesExist('aggregate_snapshots'); // Should return true
      
  3. UUID Encoding

    • Issue: Using incorrect UUID encoding/decoding can corrupt snapshot IDs.
    • Fix: Stick to EventSauce\UuidEncoding\UuidEncoder for consistency.
  4. Conditional Strategy Overhead

    • Issue: Custom ConditionalSnapshotStrategy logic might introduce bugs if not thoroughly tested.
    • Fix: Test edge cases (e.g., snapshot triggers during reconstitution).

Debugging Tips

  • Log Snapshot States: Add logging in createSnapshotState to verify data:
    \Log::debug('Snapshot state:', ['state' => $this->createSnapshotState()]);
    
  • Check Table Schema: Ensure the 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'; }
    }
    
  • Version Comparator: If using custom versioning, override SnapshotVersionComparator:
    final class CustomVersionComparator implements \Andreo\EventSauce\Snapshotting\Repository\Versioned\SnapshotVersionComparator {
        public function compare($versionA, $versionB): int {
            return strcmp((string)$versionA, (string)$versionB);
        }
    }
    

Extension Points

  1. 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); }
    }
    
  2. 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';
        }
    }
    
  3. 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);
        }
    }
    
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
terminal42/code-quality-tools
codifyo/ts-generator-bundle
andydefer/laravel-cluster
testo/fiber
mintobit/jobqueue
a4sex/maintenance-bundle
a4sex/entity-date-update
a4sex/client-identifier
a4sex/base-utilites
a4sex/key-value-storage
a4sex/micro-status
chilldev/dependency-injection-extra
datinglibre/datinglibre-app-api
biberltd/corebundle
bricre/symfony-bundle-test
biberltd/logbundle
dominium/http-adapter-bundle
dominium/google-analytics
a4sex/auto-clean-entity
christhompsontldr/laravel-inky