Rewriting the Past: Mastering Event Sourcing with Verbs for Laravel
Overview
Traditional applications focus on the "now." You have a database table, a row, and a column—like a player's score—and you update that value as things change. But when you overwrite data, you lose the history of how you got there.
This matters because it provides a perfect audit trail and the ability to "time travel." If you discover a bug in your logic, you don't just fix the code for future users; you can actually replay the entire history of your application through the corrected logic to fix the past. It transforms data from a static snapshot into a living narrative.
Prerequisites
To follow this guide, you should be comfortable with:
- PHP 8.x and the Laravelframework.
- Eloquent Models: Understanding how traditional CRUD works.
- Livewire: Familiarity with basic component structure and method firing.
- Terminal/CLI: Basic knowledge of running Artisan commands.
Key Libraries & Tools
- Verbs: The core event sourcing package forLaravel.
- Laravel Livewire: Used for the reactive frontend components.
- Laravel Forge: The deployment tool used for the production demonstration.
The Architecture: States and Events
In
States
A State is a simple PHP object representing your data at a specific moment. It is the "noun" of your application. Unlike Eloquent models, states are derived from events.
class PlayerState extends State
{
public int $score = 0;
}
Events
Events are immutable records of things that happened. They contain the data needed to change the state. Once an event is fired, it is written to the database and never changed.
class Upvoted extends Event
{
public int $playerId;
public function applyToPlayer(PlayerState $state)
{
$state->score++;
}
}
Code Walkthrough: Implementing a Secure Event
Let’s look at a practical scenario: playing a secret code to get points. In a standard app, you might just increment a score in a controller. In
1. The Livewire Trigger
First, we trigger the event from a
public function submitCode()
{
PlayedCode::fire(
playerId: $this->playerId,
gameId: $this->gameId,
code: $this->code
);
}
2. Validation Logic
Events should decide if they are allowed to happen. We use a validate method to check the GameState. If the code isn't in the valid collection, we throw an exception.
public function validate(GameState $game)
{
$this->assert(
$game->codes->contains($this->code),
"The code {$this->code} is not valid."
);
}
3. Mutating Multiple States
A single event can change multiple states. Here, we increment the player's score and remove the used code from the game state simultaneously.
public function applyToPlayer(PlayerState $player)
{
$player->score++;
}
public function applyToGame(GameState $game)
{
$game->codes = $game->codes->reject(fn($c) => $c === $this->code);
}
Syntax Notes
- Dependency Injection: Notice how the
applyandvalidatemethods type-hint the state objects.Verbsautomatically resolves the correct state instance based on the IDs provided in the event. - Method Naming: The convention
applyTo{StateName}allows an event to target specific states without complex configuration. - Immutability: Once
PlayedCodeis stored in theverbs_eventstable, the data properties cannot be changed. This is your source of truth.
Practical Examples
Event sourcing excels in environments where auditability is non-negotiable:
- Financial Systems: You don't just want a balance; you want every transaction that led to it.
- Gaming: As seen in the Thunkpyramid scheme, it prevents cheating by re-verifying every move against the rules.
- Security Audits: You can store IP addresses in event metadata. If an attacker breaches an account, you can replay the stream while specifically ignoring any events originating from the attacker's IP.
Tips & Gotchas
- The Replay Power: If you discover that users were exploiting a bug (like reusing the same code because you forgot the
applyToGamelogic), you don't need a complex migration. You fix the code, truncate your state tables, and runphp artisan verbs:replay. The system will re-process every event through the new, corrected logic. - Performance: Replaying thousands of events is surprisingly fast, but for massive datasets, Verbsuses "snapshots" to cache state at certain points, preventing the need to boot from event zero every time.
- State vs. Database: Remember that while states are stored in the database for performance (snapshots), the
verbs_eventstable is the only part of your database that truly matters for long-term integrity.
