spatie/laravel-eventsauce
Integrate EventSauce event sourcing into Laravel with migrations, models, and queued jobs. Generate aggregate roots, repositories, events and commands via Artisan. Store domain messages per aggregate and dispatch consumers synchronously or through queues.
Installation:
composer require spatie/laravel-eventsauce
php artisan vendor:publish --provider="Spatie\LaravelEventSauce\EventSauceServiceProvider"
Publish the config to config/event-sauce.php and update as needed.
Prerequisites:
Ensure you understand EventSauce core concepts (aggregates, events, commands, projections).
Familiarize yourself with Laravel’s Artisan commands and migrations.
First Use Case: Generate an aggregate root and repository:
php artisan make:aggregate-root "MyDomain/MyAggregate"
This creates:
App/MyDomain/MyAggregate.php (aggregate root)App/MyDomain/MyAggregateRepository.php (repository)my_aggregate_domain_messages (EventSauce’s storage table).Key Files to Review:
config/event-sauce.php: Configure storage (e.g., pdo, redis), event bus, and projections.app/Providers/EventSauceServiceProvider.php: Bind repositories and configure projections.Define Aggregates:
Use the make:aggregate-root command to scaffold aggregates. Example:
// App/MyDomain/MyAggregate.php
namespace App\MyDomain;
use Spatie\LaravelEventSauce\AggregateRoot;
class MyAggregate extends AggregateRoot
{
public function apply(MyEvent $event)
{
// Update aggregate state based on events
}
public function handle(MyCommand $command)
{
// Emit events in response to commands
$this->recordThat(new MyEvent());
}
}
Handle Commands:
Dispatch commands via Laravel’s Bus or manually:
use App\MyDomain\MyCommand;
use App\MyDomain\MyAggregateRepository;
$repository = app(MyAggregateRepository::class);
$aggregate = $repository->retrieve('aggregate-id');
$aggregate->handle(new MyCommand());
$repository->persist($aggregate);
Projections:
Project events to read models using Projector classes. Example:
// App/MyDomain/Projectors/MyProjector.php
namespace App\MyDomain\Projectors;
use Spatie\LaravelEventSauce\Projector;
class MyProjector extends Projector
{
public function whenMyEventOccurred(MyEvent $event)
{
// Update read model (e.g., Eloquent, database, cache)
MyReadModel::updateFromEvent($event);
}
}
Register projectors in EventSauceServiceProvider:
public function boot()
{
$this->eventSauce->addProjector(MyProjector::class);
}
Leverage Laravel Features:
$this->dispatch(new HandleMyCommand($aggregateId, $commandData));
Implement HandleMyCommand as a job.make:aggregate-root or manually.$this->partialMock(MyAggregateRepository::class, function ($mock) {
$mock->shouldReceive('retrieve')->andReturn($aggregate);
});
Event Publishing: Publish events to the bus for projections:
$this->eventSauce->publish($event);
Event Versioning: EventSauce requires strict event versioning. If you modify an event class (e.g., add a property), update its version in the class docblock:
/**
* @version 2
*/
class MyEvent {}
php artisan eventsauce:generate-events to regenerate event classes if versions mismatch.Repository Persistence:
Always call $repository->persist($aggregate) after handling commands. Forgetting this will lose events.
domain_messages table for missing entries.Projection Conflicts:
Projections may fail if read models aren’t idempotent. Ensure when*Occurred methods handle duplicate events gracefully.
DB::transaction for atomic projection updates.Storage Configuration:
.env (e.g., DB_CONNECTION=mysql).'storage' => [
'pdo' => [
'enabled' => false,
],
'redis' => [
'enabled' => true,
'connection' => 'cache',
],
],
Aggregate Retrieval:
retrieve() for existing aggregates. If the aggregate doesn’t exist, retrieve() returns null.if (!$aggregate = $repository->retrieve($id)) {
$aggregate = MyAggregate::recreate($id);
}
Event Logs:
Enable debug logging in config/event-sauce.php:
'debug' => env('APP_DEBUG', false),
Check Laravel logs (storage/logs/laravel.log) for event bus errors.
Projection Debugging:
Use Laravel’s Log facade to trace projector execution:
\Log::info('Projecting event', ['event' => $event->toArray()]);
Command Handling: Validate commands before processing to avoid invalid state:
public function handle(MyCommand $command)
{
if (!$this->isValid($command)) {
throw new \InvalidArgumentException('Invalid command');
}
// ...
}
Custom Storage:
Extend Spatie\LaravelEventSauce\EventSauce to support alternative storage backends (e.g., DynamoDB):
$this->app->bind(EventSauce::class, function ($app) {
return new EventSauce(new CustomStorage());
});
Event Serialization: Override event serialization for custom formats (e.g., JSON:API):
class MyEvent implements \JsonSerializable
{
public function jsonSerialize()
{
return ['data' => $this->data];
}
}
Aggregate Lifecycle: Hook into aggregate creation/deletion:
// In EventSauceServiceProvider
$this->eventSauce->onAggregateCreated(function ($aggregate) {
\Log::info("Aggregate created: {$aggregate->aggregateId()}");
});
Testing:
Use EventSauceTestCase for isolated tests:
use Spatie\LaravelEventSauce\Testing\EventSauceTestCase;
class MyAggregateTest extends EventSauceTestCase
{
public function testCommandHandling()
{
$this->assertTrue(true); // Test logic here
}
}
Performance:
chunk() for large projections:
DomainMessage::chunk(100, function ($messages) {
foreach ($messages as $message) {
$this->projector->project($message);
}
});
cache()->remember("read-model:{$aggregateId}", now()->addHours(1), function () {
return MyReadModel::find($aggregateId);
});
How can I help you explore Laravel packages today?