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

Laravel Event Sourcing Laravel Package

spatie/laravel-event-sourcing

Event sourcing toolkit for Laravel: build aggregates, projectors, and reactors to store state changes as events. Ideal for audit trails, decisions based on history, and future reporting needs. Includes docs, examples, and an optional course.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation:

    composer require spatie/laravel-event-sourcing
    php artisan vendor:publish --provider="Spatie\EventSourcing\EventSourcingServiceProvider"
    

    Publish the config and migrations.

  2. First Aggregate: Define an aggregate (e.g., UserAggregate) extending Spatie\EventSourcing\Aggregate:

    use Spatie\EventSourcing\Aggregate;
    use Spatie\EventSourcing\Event;
    
    class UserAggregate extends Aggregate
    {
        public function handle(UserRegistered $event)
        {
            $this->recordThat(new UserRegistered(
                userId: $event->userId,
                email: $event->email
            ));
        }
    }
    
  3. First Event: Create an event (e.g., UserRegistered) extending Event:

    class UserRegistered extends Event
    {
        public function __construct(
            public string $userId,
            public string $email
        ) {}
    }
    
  4. First Command: Dispatch a command to trigger the aggregate:

    use Spatie\EventSourcing\Commands\RecordAggregateRootCommand;
    
    RecordAggregateRootCommand::dispatch(
        aggregateRootId: 'user-123',
        aggregateRootClass: UserAggregate::class,
        initialEvents: [],
        commandName: 'UserRegistered',
        commandPayload: ['userId' => 'user-123', 'email' => 'user@example.com']
    );
    
  5. Verify: Check the event_sourcing_events table for recorded events.


First Use Case: Audit Trail

Use the package to log all state changes of a Post model:

  1. Create PostAggregate and PostPublished/PostEdited events.
  2. Dispatch commands when posts are created/edited.
  3. Query events via EventSourcing::getEventsForAggregate() to reconstruct history.

Implementation Patterns

Workflows

  1. Aggregate Lifecycle:

    • Creation: Dispatch RecordAggregateRootCommand with initialEvents: [].
    • State Changes: Use recordThat() in handle() methods to emit events.
    • Reconstruction: Replay events via EventSourcing::reconstructAggregate().
  2. Projectors (Read Models): Sync read models by listening to events:

    use Spatie\EventSourcing\EventHandler;
    
    class PostProjector implements EventHandler
    {
        public function handle(object $event): void
        {
            if ($event instanceof PostPublished) {
                Post::query()->create([
                    'title' => $event->title,
                    'content' => $event->content,
                ]);
            }
        }
    }
    

    Register in EventSourcingServiceProvider.

  3. Reactors (Side Effects): Trigger actions (e.g., notifications) via reactors:

    class UserRegistrationReactor implements EventHandler
    {
        public function handle(object $event): void
        {
            if ($event instanceof UserRegistered) {
                Notification::send(User::find($event->userId), new WelcomeEmail());
            }
        }
    }
    
  4. Command Handling: Use RecordAggregateRootCommand or custom commands with EventSourcing::dispatch():

    EventSourcing::dispatch(
        new PublishPostCommand('post-1', 'Hello World', 'Content...')
    );
    

Integration Tips

  1. Laravel Commands: Wrap aggregate logic in Artisan commands for CLI-driven workflows:

    php artisan event-sourcing:reconstruct PostAggregate post-1
    
  2. Testing: Use EventSourcing::fake() to mock events in tests:

    EventSourcing::fake();
    EventSourcing::assertRecorded(UserRegistered::class);
    
  3. Event Storage: Customize storage by binding EventStorage interface (e.g., for Redis):

    $this->app->bind(EventStorage::class, function () {
        return new RedisEventStorage();
    });
    
  4. Event Versioning: Use EventSourcing::getEventClass() to resolve event classes by version.


Gotchas and Tips

Pitfalls

  1. Event Ordering:

    • Events must be replayed chronologically. Use transactions or queues to ensure order.
    • Avoid concurrent writes to the same aggregate (use locks or optimistic locking).
  2. Aggregate Reconstruction:

    • If reconstructAggregate() fails, check for missing or malformed events in the storage.
    • Ensure all events have a recorded_at timestamp (default: now).
  3. Projector Duplicates:

    • Projectors may process events multiple times during replay. Use idempotent logic or deduplication (e.g., Post::updateOrCreate()).
  4. Command Payloads:

    • Serialization issues can arise with complex payloads. Use json_encode()/json_decode() or spatie/array-to-object.
  5. Event Class Resolution:

    • If events aren’t found, verify:
      • The event class exists and is autoloaded.
      • The EventSourcing::getEventClass() resolver is configured correctly.

Debugging

  1. Log Events: Enable debug logging in config/event-sourcing.php:

    'debug' => env('APP_DEBUG', false),
    

    Check storage/logs/laravel.log for event replay errors.

  2. Event Dump: Inspect recorded events:

    $events = EventSourcing::getEventsForAggregate('user-123');
    dd($events);
    
  3. Replay Errors:

    • Use EventSourcing::reconstructAggregate() with a try-catch to log failures:
    try {
        $aggregate = EventSourcing::reconstructAggregate(UserAggregate::class, 'user-123');
    } catch (\Throwable $e) {
        report($e);
    }
    

Tips

  1. Naming Conventions:

    • Prefix aggregates with Aggregate (e.g., UserAggregate).
    • Use past-tense event names (e.g., UserRegistered, not UserRegister).
  2. Event Attributes:

    • Add #[Spatie\EventSourcing\Attributes\Event] to events for auto-discovery (PHP 8.0+).
  3. Performance:

    • Batch event processing for projectors:
    EventSourcing::processEventsInBatches(
        UserRegistered::class,
        100,
        fn (UserRegistered $event) => /* ... */
    );
    
  4. Testing:

    • Use EventSourcing::assertEventsRecorded() to verify events:
    EventSourcing::assertEventsRecorded(UserRegistered::class, [
        new UserRegistered('user-123', 'user@example.com'),
    ]);
    
  5. Extending Storage:

    • Implement Spatie\EventSourcing\EventStorage for custom backends (e.g., DynamoDB):
    class DynamoEventStorage implements EventStorage
    {
        public function getEventsForAggregate(string $aggregateRootId): array
        {
            // Fetch from DynamoDB
        }
    }
    
  6. Config Quirks:

    • Set event_sourcing.event_class_resolver to customize event class resolution (e.g., for namespaced events).
    • Disable event_sourcing.record_events_in_database to use an in-memory storage for testing.
  7. Idempotency:

    • Add event_id or aggregate_version checks in projectors to avoid duplicate processing:
    if ($event->aggregateVersion > Post::where('id', $event->postId)->value('version')) {
        // Update
    }
    
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.
davejamesmiller/laravel-breadcrumbs
artisanry/parsedown
christhompsontldr/phpsdk
enqueue/dsn
bunny/bunny
enqueue/test
enqueue/null
enqueue/amqp-tools
milesj/emojibase
bower-asset/punycode
bower-asset/inputmask
bower-asset/jquery
bower-asset/yii2-pjax
laravel/nova
spatie/laravel-mailcoach
spatie/laravel-superseeder
laravel/liferaft
nst/json-test-suite
danielmiessler/sec-lists
jackalope/jackalope-transport