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.
Installation:
composer require spatie/laravel-event-sourcing
php artisan vendor:publish --provider="Spatie\EventSourcing\EventSourcingServiceProvider"
Publish the config and migrations.
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
));
}
}
First Event:
Create an event (e.g., UserRegistered) extending Event:
class UserRegistered extends Event
{
public function __construct(
public string $userId,
public string $email
) {}
}
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']
);
Verify:
Check the event_sourcing_events table for recorded events.
Use the package to log all state changes of a Post model:
PostAggregate and PostPublished/PostEdited events.EventSourcing::getEventsForAggregate() to reconstruct history.Aggregate Lifecycle:
RecordAggregateRootCommand with initialEvents: [].recordThat() in handle() methods to emit events.EventSourcing::reconstructAggregate().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.
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());
}
}
}
Command Handling:
Use RecordAggregateRootCommand or custom commands with EventSourcing::dispatch():
EventSourcing::dispatch(
new PublishPostCommand('post-1', 'Hello World', 'Content...')
);
Laravel Commands: Wrap aggregate logic in Artisan commands for CLI-driven workflows:
php artisan event-sourcing:reconstruct PostAggregate post-1
Testing:
Use EventSourcing::fake() to mock events in tests:
EventSourcing::fake();
EventSourcing::assertRecorded(UserRegistered::class);
Event Storage:
Customize storage by binding EventStorage interface (e.g., for Redis):
$this->app->bind(EventStorage::class, function () {
return new RedisEventStorage();
});
Event Versioning:
Use EventSourcing::getEventClass() to resolve event classes by version.
Event Ordering:
Aggregate Reconstruction:
reconstructAggregate() fails, check for missing or malformed events in the storage.recorded_at timestamp (default: now).Projector Duplicates:
Post::updateOrCreate()).Command Payloads:
json_encode()/json_decode() or spatie/array-to-object.Event Class Resolution:
EventSourcing::getEventClass() resolver is configured correctly.Log Events:
Enable debug logging in config/event-sourcing.php:
'debug' => env('APP_DEBUG', false),
Check storage/logs/laravel.log for event replay errors.
Event Dump: Inspect recorded events:
$events = EventSourcing::getEventsForAggregate('user-123');
dd($events);
Replay Errors:
EventSourcing::reconstructAggregate() with a try-catch to log failures:try {
$aggregate = EventSourcing::reconstructAggregate(UserAggregate::class, 'user-123');
} catch (\Throwable $e) {
report($e);
}
Naming Conventions:
Aggregate (e.g., UserAggregate).UserRegistered, not UserRegister).Event Attributes:
#[Spatie\EventSourcing\Attributes\Event] to events for auto-discovery (PHP 8.0+).Performance:
EventSourcing::processEventsInBatches(
UserRegistered::class,
100,
fn (UserRegistered $event) => /* ... */
);
Testing:
EventSourcing::assertEventsRecorded() to verify events:EventSourcing::assertEventsRecorded(UserRegistered::class, [
new UserRegistered('user-123', 'user@example.com'),
]);
Extending Storage:
Spatie\EventSourcing\EventStorage for custom backends (e.g., DynamoDB):class DynamoEventStorage implements EventStorage
{
public function getEventsForAggregate(string $aggregateRootId): array
{
// Fetch from DynamoDB
}
}
Config Quirks:
event_sourcing.event_class_resolver to customize event class resolution (e.g., for namespaced events).event_sourcing.record_events_in_database to use an in-memory storage for testing.Idempotency:
event_id or aggregate_version checks in projectors to avoid duplicate processing:if ($event->aggregateVersion > Post::where('id', $event->postId)->value('version')) {
// Update
}
How can I help you explore Laravel packages today?