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.

is an event sourcing library for
Laravel
that flips this script. Instead of storing the current state, you store the actions (the "verbs") that led to that state.

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
    Laravel
    framework.
  • 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

The Architecture: States and Events

In

, there are only two primary concepts you need to grasp: States and Events.

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

, we handle the validation and the state mutation within the event itself.

1. The Livewire Trigger

First, we trigger the event from a

component. We pass the necessary IDs and the code the user entered.

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 apply and validate methods type-hint the state objects.
    Verbs
    automatically 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 PlayedCode is stored in the verbs_events table, 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
    Thunk
    pyramid 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 applyToGame logic), you don't need a complex migration. You fix the code, truncate your state tables, and run php 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,
    Verbs
    uses "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_events table is the only part of your database that truly matters for long-term integrity.
4 min read