Event Sourcing is a term that has been getting more popular in the PHP community over the last few years, but it still remains a mystery to many developers. The questions are always how and why, and it is understandable. This tutorial is designed to help you understand not only what Event Sourcing is, in a practical way, but know when you might want to use it.
In a traditional application, our application state is directly represented in the database we are connected to. We do not fully understand how it got there. All we know is that it is. There are ways in which we might understand this a little more, using tools for auditing model changes so that we can see what was changed and by who. This, again, is a step in the right direction. However, we still don't understand the critical question.
Why? Why did this model change? What is the purpose of this change?
This is where Event Sourcing holds its own, by keeping a historical view of what happened to the application state but also why it changed. Event Sourcing allows you to make a decision based on the past, enabling you to generate reports. But at its basic level, it lets you know why the application state changed. This is done through events.
I will build a basic Laravel project to walk you through how this works. The application we will make is relatively simple so that you can understand the Event Sourcing logic instead of getting lost in application logic. We are building an application where we can celebrate team members. That is it. Simple and easy to understand. We have teams with users, and we want to be able to celebrate something publically in the team.
We will start with a new Laravel project, but I will use Jetstream as I want to bootstrap the authentication and team structure and functionality. Once you have this project set up, open it in your IDE of choice (the correct answer here is PHPStorm, of course), and we are ready to dive into some Event Sourcing in Laravel.
We will want to create an additional model for our application, one of our only ones. This will be a Celebration
model, and you can create this using the following artisan command:
1php artisan make:model Celebration -m
Modify your migrations up method to look like the following:
1public function up(): void 2{ 3 Schema::create('celebrations', static function (Blueprint $table): void { 4 $table->id(); 5 6 $table->string('reason'); 7 $table->text('message')->nullable(); 8 9 $table10 ->foreignId('user_id')11 ->index()12 ->constrained()13 ->cascadeOnDelete();14 15 $table16 ->foreignId('sender_id')17 ->index()18 ->constrained('users')19 ->cascadeOnDelete();20 21 $table22 ->foreignId('team_id')23 ->index()24 ->constrained()25 ->cascadeOnDelete();26 27 $table->timestamps();28 });29}
We have a celebration reason
, a simple sentence, then an optional message
that we might want to send across with the celebration. Alongside this, we have three relations, the user who is being celebrated, the user that is sending the celebration, and what team they are on. With Jetstream, a user can belong to multiple teams, and there could be a situation where both users are in the same team, and we want to make sure we celebrate them publically in the correct team.
Once we have this setup, let us look at the model itself:
1declare(strict_types=1); 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Factories\HasFactory; 6use Illuminate\Database\Eloquent\Model; 7use Illuminate\Database\Eloquent\Relations\BelongsTo; 8use Illuminate\Database\Eloquent\Relations\HasMany; 9 10final class Celebration extends Model11{12 use HasFactory;13 14 protected $fillable = [15 'reason',16 'message',17 'user_id',18 'sender_id',19 'team_id',20 ];21 22 public function user(): BelongsTo23 {24 return $this->belongsTo(25 related: User::class,26 foreignKey: 'user_id',27 );28 }29 30 public function sender(): BelongsTo31 {32 return $this->belongsTo(33 related: User::class,34 foreignKey: 'sender_id',35 );36 }37 38 public function team(): BelongsTo39 {40 return $this->belongsTo(41 related: Team::class,42 foreignKey: 'team_id',43 );44 }45}
We can mirror the relationships onto the other models as they are relevant. Still, by default, I add the other side of the relationship to every model to clarify the binding between the models, no matter if it is strictly needed. This is a habit I have gotten into to help others understand the data model itself.
Now we have the basis of our application created from a modeling perspective. We need to think about installing some packages that will help us. For my application, I used Laravel Livewire to control the UI. However, I will not go into detail for this tutorial as I want to ensure that I focus on the Event Sourcing aspect.
Like with most projects I build, no matter the size, I have adopted a modular layout for the application - a Domain Driven Design approach. This is just something I do, do not feel that you have to follow this yourself, as it is very subjective.
My next step was to set up my domains, and for this demo, I only had one Domain: Culture. Within Culture, I created the namespaces for everything I might need. But I will go through it, so you understand the process.
The first step was to install a package that would enable me to use Event Sourcing in Laravel. For this, I used a Spatie package that does a lot of the background work for me. Let's install this package using composer:
1composer require spatie/laravel-event-sourcing
Once this is installed, ensure that you follow the install instructions for the package - as configuration and migrations need publishing. Once this is installed correctly, run your migrations so your database can be in the right state.
1php artisan migrate
Now we can start thinking about how we want to implement Event Sourcing. You can implement this in a couple of ways: projectors to project your state or aggregates.
A Projector is a class that sits within your application and handles events you dispatch. These will then change the state of your application. It is a step beyond simply updating your database. It sits in the middle, catches an event, stores it, and then makes the changes it needs to - which then "projects" the new state for the application.
The other approach, my preferred method, aggregates - these are classes that, like projectors, handle the application state for you. Instead of firing events ourselves in our application, we leave that for the aggregate to do for us. Think of it like a relay, you ask the relay to do something, and it handles it for you.
Before we can create our first aggregate, there is some work to do in the background. I am a big fan of creating an event store per aggregate so that querying is quicker and that store will not fill up super quickly. This is explained in the package documentation, but I will walk you through it myself as it wasn't the clearest in the docs.
The first step is to create a model and migration, as you will need a way to query it in the future for reporting etc. Run the following artisan command to create these:
1php artisan make:model CelebrationStoredEvent -m
The following code is what you will need in your up method for the migration:
1public function up(): void 2{ 3 Schema::create('celebration_stored_events', static function (Blueprint $table): void { 4 $table->id(); 5 $table->uuid('aggregate_uuid')->nullable()->unique(); 6 $table 7 ->unsignedBigInteger('aggregate_version') 8 ->nullable() 9 ->unique();10 $table->integer('event_version')->default(1);11 $table->string('event_class');12 13 $table->json('event_properties');14 15 $table->json('meta_data');16 17 $table->timestamp('created_at');18 19 $table->index('event_class');20 $table->index('aggregate_uuid');21 });22}
As you can see, we collect quite a bit of data for our events. Now the model is a lot simpler. It should look like this:
1declare(strict_types=1); 2 3namespace App\Models; 4 5 6use Spatie\EventSourcing\StoredEvents\Models\EloquentStoredEvent; 7 8final class CelebrationStoredEvent extends EloquentStoredEvent 9{10 public $table = 'celebration_stored_events';11}
As we are extending the EloquentStoredEvent
model, all we need to do is change the table it is looking at. The rest of the functionality for the model is already in place on the parent.
To use these models, you must create a repository to query the events. It is quite a simple repository - however, it is an important step. I added mine to my Domain code under src/Domains/Culture/Repositories/
but feel free to add your where it makes the most sense to you:
1declare(strict_types=1); 2 3namespace Domains\Culture\Repositories; 4 5use App\Models\CelebrationStoredEvent; 6use Spatie\EventSourcing\StoredEvents\Repositories\EloquentStoredEventRepository; 7 8final class CelebrationStoredEventsRepository extends EloquentStoredEventRepository 9{10 public function __construct(11 protected string $storedEventModel = CelebrationStoredEvent::class,12 ) {13 parent::__construct();14 }15}
Now that we have a way to store the events and query them, we can move on to our aggregate itself. Again, I stored mine within my domain but feel free to keep yours within your application context.
1declare(strict_types=1); 2 3namespace Domains\Culture\Aggregates; 4 5use Domains\Culture\Repositories\CelebrationStoredEventsRepository; 6use Spatie\EventSourcing\AggregateRoots\AggregateRoot; 7use Spatie\EventSourcing\StoredEvents\Repositories\StoredEventRepository; 8 9final class CelebrationAggregateRoot extends AggregateRoot10{11 protected function getStoredEventRepository(): StoredEventRepository12 {13 return app()->make(14 abstract: CelebrationStoredEventsRepository::class,15 );16 }17}
This aggregate so far will not do anything other than connect to the right event store for us. To get it to start tracking events, we need first to create them. But before we do that, we need to stop and think for a moment. What data do we want to store in the event? Do we want to store every property that we need? Or do we want to store an array as if it were coming from a form? I use neither approach because why keep it simple? I use Data Transfer Objects in all of my events to ensure context is always maintained and type safety is always provided.
I built a package to allow me to do this easier. Feel free to use it by installing it with the following composer command:
1composer require juststeveking/laravel-data-object-tools
As before, I keep my Data Objects within my Domain by default but add where it makes the most sense for you. I created a Data Object called Celebration
that I could pass through to events and aggregates:
1declare(strict_types=1); 2 3namespace Domains\Culture\DataObjects; 4 5use JustSteveKing\DataObjects\Contracts\DataObjectContract; 6 7final class Celebration implements DataObjectContract 8{ 9 public function __construct(10 private readonly string $reason,11 private readonly string $message,12 private readonly int $user,13 private readonly int $sender,14 private readonly int $team,15 ) {}16 17 public function userID(): int18 {19 return $this->user;20 }21 22 public function senderID(): int23 {24 return $this->sender;25 }26 27 public function teamUD(): int28 {29 return $this->team;30 }31 32 public function toArray(): array33 {34 return [35 'reason' => $this->reason,36 'message' => $this->message,37 'user_id' => $this->user,38 'sender_id' => $this->sender,39 'team_id' => $this->team,40 ];41 }42}
When I upgrade to PHP 8.2 this will be a lot easier, as I can then create readonly classes - and yes, my package already supports them.
Now we have our Data Object. We can get back to the event we want to store. I have called mine CelebrationWasCreated
because event names should always be in the past tense. Let's have a look at this event:
1declare(strict_types=1); 2 3namespace Domains\Culture\Events; 4 5use Domains\Culture\DataObjects\Celebration; 6use Spatie\EventSourcing\StoredEvents\ShouldBeStored; 7 8final class CelebrationWasCreated extends ShouldBeStored 9{10 public function __construct(11 public readonly Celebration $celebration,12 ) {}13}
Because we are using Data Objects, our class remains clean. So, now that we have an event - and a Data Object we can send around, we need to think about how to trigger this. This brings us back to our Aggregate itself, so let us create a method on our aggregate that we can use for this:
1declare(strict_types=1); 2 3namespace Domains\Culture\Aggregates; 4 5use Domains\Culture\DataObjects\Celebration; 6use Domains\Culture\Events\CelebrationWasCreated; 7use Domains\Culture\Repositories\CelebrationStoredEventsRepository; 8use Spatie\EventSourcing\AggregateRoots\AggregateRoot; 9use Spatie\EventSourcing\StoredEvents\Repositories\StoredEventRepository;10 11final class CelebrationAggregateRoot extends AggregateRoot12{13 protected function getStoredEventRepository(): StoredEventRepository14 {15 return app()->make(16 abstract: CelebrationStoredEventsRepository::class,17 );18 }19 20 public function createCelebration(Celebration $celebration): CelebrationAggregateRoot21 {22 $this->recordThat(23 domainEvent: new CelebrationWasCreated(24 celebration: $celebration,25 ),26 );27 28 return $this;29 }30}
At this point, we have a way to ask a class to record an event. However, this event will not yet be persisted - that comes later. Also, we aren't mutating our applications' state in any way. So how do we do this event sourcing bit? This part is down to the implementation within Livewire for me, which I will walk you through now.
I like to approach this by dispatching an event that will manage this process, as it is more efficient. If you think about how you might interact with an application, you can visit it from the web, send a request through an API endpoint, or event a CLI command might run - maybe it's a CRON job. In all of these methods, typically, you want an instant response, or at least you don't want to be waiting around. I will show you the method on my Livewire component that I have used for this:
1public function celebrate(): void 2{ 3 $this->validate(); 4 5 dispatch(new TeamMemberCelebration( 6 celebration: Hydrator::fill( 7 class: Celebration::class, 8 properties: [ 9 'reason' => $this->reason,10 'message' => $this->content,11 'user' => $this->identifier,12 'sender' => auth()->id(),13 'team' => auth()->user()->current_team_id,14 ]15 ),16 ));17 18 $this->closeModal();19}
I validate the users' input from the component, dispatch a new Job that can be handled, and close the modal. I pass a new Data Object into the job using my package. It has a facade that allows me to hydrate classes with an array of properties - and it works pretty well so far. So what does this job do? Let's take a look.
1declare(strict_types=1); 2 3namespace App\Jobs\Team; 4 5use Domains\Culture\Aggregates\CelebrationAggregateRoot; 6use Domains\Culture\DataObjects\Celebration; 7use Illuminate\Bus\Queueable; 8use Illuminate\Contracts\Queue\ShouldQueue; 9use Illuminate\Foundation\Bus\Dispatchable;10use Illuminate\Queue\InteractsWithQueue;11use Illuminate\Queue\SerializesModels;12use Illuminate\Support\Str;13 14final class TeamMemberCelebration implements ShouldQueue15{16 use Queueable;17 use Dispatchable;18 use SerializesModels;19 use InteractsWithQueue;20 21 public function __construct(22 public readonly Celebration $celebration,23 ) {}24 25 public function handle(): void26 {27 CelebrationAggregateRoot::retrieve(28 uuid: Str::uuid()->toString(),29 )->createCelebration(30 celebration: $this->celebration,31 )->persist();32 }33}
Our job accepts the Data Object into its constructor and then stores this for when it is being processed. When the job is processed, it uses the CelebrationAggregateRoot
to retrieve an aggregate by UUID and then calls the createCelebration
method we created earlier. After it has called this method - it calls persist
on the aggregate itself. This is what will store the event for us. But, again, we haven't mutated our applications state yet. All we have managed to do is store an unrelated event and not create the celebration we want to be creating? So what are we missing?
Our events also need to be handled. In the other method, we use a Projector to handle our events, but we have to call them manually. It is a similar process here, but instead our aggregate is triggering the event, we still need a projector to handle the event and mutate our applications state.
Let's create our Projector, which I call Handlers - as they handle the events. But I will leave it up to you how you wish to name yours.
1declare(strict_types=1); 2 3namespace Domains\Culture\Handlers; 4 5use Domains\Culture\Events\CelebrationWasCreated; 6use Spatie\EventSourcing\EventHandlers\Projectors\Projector; 7use Infrastructure\Culture\Actions\CreateNewCelebrationContract; 8 9final class CelebrationHandler extends Projector10{11 public function __construct(12 public readonly CreateNewCelebrationContract $action,13 ) {}14 15 public function onCelebrationWasCreated(CelebrationWasCreated $event): void16 {17 $this->action->handle(18 celebration: $event->celebration,19 );20 }21}
Our Projector/Handler, whatever you might choose to call it, will be resolved from the container for us - and then it will look for a method prefixed with on
followed by the Event name itself. So in our case, onCelebrationWasCreated
. In my example, I use an action to perform the actual logic from the event - single classes doing one job that can be faked or replaced easily. So yet again, we chase the tree down to the next class. The action, this is what it looks like for me:
1declare(strict_types=1); 2 3namespace Domains\Culture\Actions; 4 5use App\Models\Celebration; 6use Domains\Culture\DataObjects\Celebration as CelebrationObject; 7use Illuminate\Database\Eloquent\Model; 8use Infrastructure\Culture\Actions\CreateNewCelebrationContract; 9 10final class CreateNewCelebration implements CreateNewCelebrationContract11{12 public function handle(CelebrationObject $celebration): Model|Celebration13 {14 return Celebration::query()->create(15 attributes: $celebration->toArray(),16 );17 }18}
This is the current implementation of the action. As you can see, my action class itself implements a contract/interface. This means I bind the interface to a specific implementation in my Service Provider. This allows me to easily create test doubles/mocks/alternative approaches without having a knock-on effect on the actual action that needs to be performed. This isn't strictly an event sourcing thing but a general programming thing. The one benefit that we do have is that our projector can be replayed. So if for some reason, we moved away from Laravel Eloquent, and maybe we used something else, we can create a new action - bind the implementation in our container, replay our events, and it should all just work.
At this stage, we are storing our events and have a way to mutate the state of our application - but do we? We need to tell the Event Sourcing library that we have registered this Projector/Handler so that it knows to trigger it on an event. Typically I would create an EventSourcingServiceProvider
per domain so that I can register all the handlers in one place. Mine looks like the following:
1declare(strict_types=1); 2 3namespace Domains\Culture\Providers; 4 5use Domains\Culture\Handlers\CelebrationHandler; 6use Illuminate\Support\ServiceProvider; 7use Spatie\EventSourcing\Facades\Projectionist; 8 9final class EventSourcingServiceProvider extends ServiceProvider10{11 public function register(): void12 {13 Projectionist::addProjector(14 projector: CelebrationHandler::class,15 );16 }17}
All that is left is to ensure that this Service Provider is registered again. I create a Service Provider per domain to register child service providers - but that is another story and tutorial.
Now when we put it all together. We can ask our aggregate to create a celebration, which will record the event and persist it in the database, and as a side effect, our handler will be triggered, mutating the applications state with the new changes.
This seems a little long-winded, right? Is there a better way? Possibly, but at this point, we know when changes to our application state are made. We understand why they were made. Also, thanks to our Data Object, we know who made the changes and when. So it might not be the most straightforward approach, but it allows us to understand more about our application.
You can go into this as much as you need or dip your toe into Event Sourcing, where it makes the most sense. Hopefully, this tutorial has shown you a clear and practical path to start with event sourcing today.
If it hasn't shown you enough, Spatie have been kind enough to give you a coupon worth 30% off their Event Sourcing in Laravel course, which is pretty darn good! Visit the Course website and use coupon code LARAVEL-NEWS-EVENT-SOURCING
.