spatie/laravel-event-projector
Deprecated in favor of spatie/laravel-event-sourcing. Entry-level event sourcing toolkit for Laravel: define aggregates, projectors, and reactors; persist domain events, build read models, and react to events for auditing and reporting-friendly apps.
Installation:
composer require spatie/laravel-event-projector
php artisan vendor:publish --provider="Spatie\EventProjector\EventProjectorServiceProvider"
Publish the config and migrations to set up the database tables for events and aggregates.
Define an Aggregate:
Create a class extending Spatie\EventProjector\Aggregate:
namespace App\Aggregates;
use Spatie\EventProjector\Aggregate;
class Order extends Aggregate
{
public function placeOrder()
{
$this->recordThat(new OrderPlaced($this->id, $this->userId));
}
}
Define an Event:
Create an event class extending Spatie\EventProjector\Event:
namespace App\Events;
use Spatie\EventProjector\Event;
class OrderPlaced extends Event
{
public $userId;
public function __construct($orderId, $userId)
{
$this->orderId = $orderId;
$this->userId = $userId;
}
}
Record an Event:
$order = new Order($orderId, $userId);
$order->placeOrder();
$order->persist();
First Use Case: Trigger an event and persist it to the database:
event(new OrderPlaced(1, 1));
// or via aggregate
$order = Order::retrieve($orderId);
$order->placeOrder();
$order->persist();
Aggregate Lifecycle:
Aggregate::retrieve($id) to load an aggregate by its ID.$aggregate->recordThat(new Event()).$aggregate->persist().$aggregate->replay() to reconstruct state.Example Workflow:
// Create and persist
$order = new Order($orderId, $userId);
$order->placeOrder();
$order->persist();
// Load and replay
$order = Order::retrieve($orderId);
$order->replay();
Define a Projector:
namespace App\Projectors;
use Spatie\EventProjector\Projector;
use App\Events\OrderPlaced;
class OrderProjector extends Projector
{
public function handleOrderPlaced(OrderPlaced $event)
{
// Update read model (e.g., database, cache)
\DB::table('order_read_models')->updateOrCreate(
['order_id' => $event->orderId],
['user_id' => $event->userId, 'status' => 'placed']
);
}
}
Register Projectors:
In config/event-projector.php, list projectors under the projectors key:
'projectors' => [
\App\Projectors\OrderProjector::class,
],
Run Projectors:
php artisan event-projector:project
Or manually:
$projector = app(\Spatie\EventProjector\Projector::class);
$projector->project();
Define a Reactor:
namespace App\Reactors;
use Spatie\EventProjector\Reactor;
use App\Events\OrderPlaced;
class OrderReactor extends Reactor
{
public function handleOrderPlaced(OrderPlaced $event)
{
// Trigger side effects (e.g., send email, update inventory)
\Log::info("Order {$event->orderId} placed by user {$event->userId}");
}
}
Register Reactors:
In config/event-projector.php, list reactors under the reactors key:
'reactors' => [
\App\Reactors\OrderReactor::class,
],
Run Reactors:
php artisan event-projector:react
Replay All Events:
php artisan event-projector:replay
Or for a specific aggregate:
$aggregate = Order::retrieve($orderId);
$aggregate->replay();
Replay Events Up to a Point:
Use the --until option:
php artisan event-projector:replay --until=2023-01-01
Dispatch events to queues for async processing:
event(new OrderPlaced($orderId, $userId))->onQueue('events');
Process queued events with reactors:
// In a job handler
$reactor = app(\Spatie\EventProjector\Reactor::class);
$reactor->react($event);
Mock Projectors/Reactors: Use partial mocks to test event handling:
$projector = Mockery::mock(\App\Projectors\OrderProjector::class);
$projector->shouldReceive('handleOrderPlaced')->once();
Test Aggregates: Replay events in tests to verify state:
$order = new Order($orderId, $userId);
$order->replay();
$this->assertEquals('placed', $order->status);
Batch Processing: Use chunking for large event sets:
\DB::table('events')->cursor()->chunk(100, function ($events) {
foreach ($events as $event) {
$projector->handle($event);
}
});
Indexing:
Ensure occurred_at is indexed in the events table for faster queries.
occurred_at timestamps and ensure no out-of-order inserts.replay() after retrieve():
$order = Order::retrieve($orderId);
$order->replay(); // Critical step!
config/event-projector.php.
'projectors' => [
\App\Projectors\OrderProjector::class,
],
__serialize() and __unserialize() or use JsonSerializable:
class OrderPlaced implements JsonSerializable
{
public function jsonSerialize()
{
return [
'orderId' => $this->orderId,
'userId' => $this->userId,
];
}
}
\DB::transaction(function () {
$events = \App\Events\OrderPlaced::chunk(50);
// Process in transaction
});
event(new OrderPlaced(...))).Spatie\EventProjector\Event.events table for the recorded event.config/event-projector.php.handle* method matches the event class name.php artisan event-projector:project manually to test.config/event-projector.php.$reactor = app(\Spatie\EventProjector\Reactor::class);
$reactor->react(new OrderPlaced(1, 1));
tinker:
php artisan tinker
>>> $order = App\Aggregates\Order::retrieve(1);
>>> $order->replay();
>>> $order->status // Verify state
How can I help you explore Laravel packages today?