Weave 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

Stupid Event Sourcing Laravel Package

damgru/stupid-event-sourcing

A Laravel package that adds a simple, no-frills approach to event sourcing. Store and replay domain events to rebuild state, keep an audit trail, and model changes over time without heavy ceremony—aimed at small projects and quick adoption.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Steps

  1. Installation

    composer require damgru/stupid-event-sourcing
    

    Add the service provider to config/app.php under providers:

    Damgru\StupidEventSourcing\StupidEventSourcingServiceProvider::class,
    
  2. Define an Aggregate Root Create a class extending Damgru\StupidEventSourcing\AggregateRoot:

    namespace App\Aggregates;
    
    use Damgru\StupidEventSourcing\AggregateRoot;
    
    class Order extends AggregateRoot
    {
        public function createOrder(string $customerId, array $items)
        {
            $this->recordThat(new OrderCreated($customerId, $items));
        }
    }
    
  3. Define Events Create a class implementing Damgru\StupidEventSourcing\Event:

    namespace App\Events;
    
    use Damgru\StupidEventSourcing\Event;
    
    class OrderCreated implements Event
    {
        public function __construct(
            public string $customerId,
            public array $items
        ) {}
    }
    
  4. First Use Case: Persisting an Aggregate

    use App\Aggregates\Order;
    use Damgru\StupidEventSourcing\EventStore;
    
    $order = new Order();
    $order->createOrder('customer-123', ['item-1', 'item-2']);
    
    $eventStore = app(EventStore::class);
    $eventStore->persist($order);
    
  5. Where to Look First

    • Documentation: Check the GitLab repository for any README.md or inline comments.
    • Tests: Browse the tests directory for usage examples and edge cases.
    • Service Provider: Inspect StupidEventSourcingServiceProvider for bindings and boot logic.

Implementation Patterns

Usage Patterns

  1. Aggregate Root Design

    • Single Responsibility: Each aggregate root should handle one domain concept (e.g., Order, User, Invoice).
    • Event Recording: Use recordThat() to emit events. Avoid direct state manipulation outside event handlers.
      $this->recordThat(new EventClass($data));
      
  2. Event Handling

    • Apply Events: Override apply() to update state based on events:
      protected function apply(Event $event): void
      {
          if ($event instanceof OrderCreated) {
              $this->customerId = $event->customerId;
              $this->items = $event->items;
          }
      }
      
    • Bulk Replay: Replay all events for an aggregate to reconstruct state:
      $order = $eventStore->reconstruct(Order::class, $aggregateId);
      
  3. Event Store Integration

    • Persistence: Use persist() to save an aggregate’s events:
      $eventStore->persist($aggregate);
      
    • Querying Events: Fetch events for an aggregate:
      $events = $eventStore->getEvents(Order::class, $aggregateId);
      
  4. Command Handling

    • Commands as Methods: Define domain commands as methods on the aggregate root:
      public function cancelOrder()
      {
          $this->recordThat(new OrderCancelled());
      }
      
    • Dispatching Commands: Use a command bus or manually invoke methods:
      $order->cancelOrder();
      $eventStore->persist($order);
      

Workflows

  1. CQRS Pattern

    • Separate Reads/Writes: Use the event store for writes and project events into read models:
      $eventStore->getEvents(Order::class, $id)->each(function (Event $event) {
          // Project event into a read model (e.g., database, cache)
      });
      
  2. Event Sourcing with Laravel

    • Listeners: Attach Laravel event listeners to published events:
      Event::listen(OrderCreated::class, function (OrderCreated $event) {
          // Send notification, update analytics, etc.
      });
      
    • Jobs: Dispatch Laravel jobs for async processing:
      $this->recordThat(new OrderCreated(...));
      dispatch(new ProcessOrderJob($this->id));
      
  3. Testing

    • Replay Events: Test aggregates by replaying events:
      $aggregate = new Order();
      $aggregate->loadFromHistory([new OrderCreated(...)]);
      $this->assertEquals('customer-123', $aggregate->customerId);
      
    • Event Store Mocking: Mock EventStore in unit tests:
      $mockStore = Mockery::mock(EventStore::class);
      $mockStore->shouldReceive('persist')->once();
      

Integration Tips

  1. Database Backend

    • The package is "stupid" (no built-in DB), so integrate with Laravel’s events table or a custom table:
      Schema::create('event_store', function (Blueprint $table) {
          $table->id();
          $table->string('aggregate_type');
          $table->string('aggregate_id');
          $table->text('event_data');
          $table->timestamps();
      });
      
    • Bind a custom EventStore implementation:
      $this->app->bind(EventStore::class, function ($app) {
          return new DatabaseEventStore($app['db']);
      });
      
  2. Serialization

    • Use Laravel’s json_encode/json_decode or a custom serializer for event data:
      $eventData = json_encode($event);
      $event = json_decode($eventData, false, 512, JSON_THROW_ON_ERROR);
      
  3. Performance

    • Batch Persistence: Persist multiple aggregates in a transaction:
      DB::transaction(function () use ($eventStore, $aggregates) {
          foreach ($aggregates as $aggregate) {
              $eventStore->persist($aggregate);
          }
      });
      
    • Indexing: Add indexes to aggregate_type and aggregate_id for fast lookups.

Gotchas and Tips

Pitfalls

  1. No Built-in Event Store

    • You must implement EventStore (e.g., database, Redis). The package provides an interface but no default implementation.
    • Example minimal implementation:
      class DatabaseEventStore implements EventStore
      {
          public function persist(AggregateRoot $aggregate): void
          {
              foreach ($aggregate->getUncommittedEvents() as $event) {
                  DB::table('event_store')->insert([
                      'aggregate_type' => $aggregate::class,
                      'aggregate_id' => $aggregate->id,
                      'event_data' => json_encode($event),
                      'occurred_on' => now(),
                  ]);
              }
              $aggregate->markEventsAsCommitted();
          }
      
          public function getEvents(string $aggregateType, string $aggregateId): array
          {
              return DB::table('event_store')
                  ->where('aggregate_type', $aggregateType)
                  ->where('aggregate_id', $aggregateId)
                  ->orderBy('occurred_on')
                  ->get()
                  ->map(fn ($record) => json_decode($record->event_data, false, 512, JSON_THROW_ON_ERROR));
          }
      }
      
  2. Event Ordering

    • Events must be replayed in chronological order. Out-of-order events will corrupt state.
    • Ensure your EventStore returns events sorted by occurred_on.
  3. Aggregate Identity

    • Aggregates are identified by a string id. Ensure this is unique and immutable (e.g., UUID).
    • Avoid regenerating IDs or using auto-increment values.
  4. State vs. Events

    • Never modify state directly. Always use recordThat() to emit events and let apply() update state.
    • Violating this leads to inconsistent state during replay.
  5. Concurrency

    • The package is not concurrency-safe by default. Use optimistic concurrency control (e.g., versioning) if needed:
      class Order extends AggregateRoot
      {
          protected int $version = 0;
      
          public function createOrder(...): void
          {
              if ($this->version !== $expectedVersion) {
                  throw new ConcurrencyException();
              }
              $this->recordThat(new OrderCreated(...));
              $this->version++;
          }
      }
      

Debugging

  1. Event Replay Issues

    • If an aggregate’s state is incorrect after replay, check:
      • Event order (are events sorted by occurred_on?)
      • apply() method (does it handle all event types?)
      • Event data (is serialization/deserialization correct?)
  2. Missing Events

    • Use getUncommittedEvents() to debug uncommitted events:
      dd($aggregate->getUncommittedEvents());
      
    • Verify persist() is called after recording events.
  3. Performance Bottlenecks

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings