Verbs

website·Indexed 33 days ago

https://verbs.thunk.dev

A PHP framework for event sourcing that replaces traditional jargon with a simplified model of Events and States. It features 64-bit Snowflake IDs, automated state mutation, and specialized attributes like #[StateId] and #[AppliesToState] to link events to their respective states.

Tokens
16.6K
Snippets
70
Records
85
Agent score
63%

What's inside Verbs

  1. Overview of Verbs for Laravel

    Verbs is a Laravel package designed to derive application state from a sequence of events that have occurred. This event-driven approach is particularly beneficial for applications requiring strict auditability, those with evolving schemas over time, or those implementing complex business logic.
  2. Use Snowflake IDs in Verbs for event-sourcing

    Verbs uses 64-bit integer IDs called "Snowflakes" by default to ensure globally unique, time-sortable IDs, which are critical for event-sourcing and replaying events without collisions. While Snowflakes are recommended for performance (integers are faster to index than strings), ULIDs or UUIDs can be configured in config/verbs.php.
  3. Understand the Verbs Event Lifecycle phases

    When a Verbs event is fired for the first time, it progresses through three major phases:

    1. Firing Phase: Ensures the event has necessary data and is valid. This phase only occurs during the initial fire, not during replay.
    2. Fired Phase: Applies the event to the state to update the 'event world' before side effects occur.
    3. Committed Phase: Occurs after the event is stored in the database; this is the safe point to trigger side effects.
  4. Implement the Events -> States -> Models flow

    In event sourcing with Verbs, events should influence state first and models last. State is part of the event system, while models are primarily for the application UI. This architecture ensures that all models can be deleted and completely rebuilt by replaying events. The event lifecycle follows this order: Authorization -> Validation against state -> State application (apply) -> Model modification (handle).
  5. Understand Event Sourcing principles in Verbs

    Event Sourcing is a pattern where every change (event) that leads to the current state of an application is stored, rather than storing only the final state. This approach provides a granular history of how the system arrived at its current state and allows for state reconstruction or analysis at any point in time.

    Key advantages include:

    • Reduced Database Querying: By using 'states' to track event data over time, complex querying is offloaded from models to states.
    • Complete Audit Trail: Every event and its associated data are stored in event tables, which improves debugging, analytics, and decision-making.
    • Event Replay: Events can be replayed to update or change the application's architecture without losing historical data.
  6. Understand how Events and States are serialized in Verbs

    Verbs uses the Symfony Serializer to store Events and States in the database. The process converts all public properties of the objects into JSON. Verbs includes default normalizers suitable for most Laravel applications, but custom normalizers can be added via the config/verbs.php configuration file if complex data structures are required. The default serializer context can also be modified in the same configuration file.
  7. Understand the state architecture of the Monopoly Verbs example

    The Monopoly example demonstrates how to manage a complex set of rules and multiple interacting states using Verbs. It utilizes two primary state types:

    1. GameState: A singleton state per game that tracks the overall status, including the active player, the current state of the board, and the bank.
    2. PlayerState: An instance-per-player state used to track individual player data, such as current balance and the assigned token.
  8. Handle one-time side effects during event replay

    To prevent side effects (such as sending emails or making external API requests) from executing when events are being replayed, wrap the logic in a Verbs::unlessReplaying() check. This replaces the formal 'Reactor' concept found in other event sourcing systems.
    Verbs::unlessReplaying(function () {
        // Code for one-time side effects (e.g., sending an email)
    });
  9. Implement validation and authorization in the Firing phase

    During the Firing phase, you can control whether an event is allowed to proceed using the constructor, authorization, and validation hooks.

    • __construct(): Used to gather all data needed for the event lifecycle. It is only called once during the initial fire and is skipped during replay.
    • authorize: Ensures the current user is permitted to fire the event (behaves like Laravel form request authorization).
    • validate hooks: Any method starting with validate is treated as a validation method. These run for each state the event is firing on based on the type-hinted state object.
    class UserJoinedTeam 
    {
        public function validateUser(UserState $user)
        {
            $this->assert($user->can_join_teams, 'This user must upgrade before joining a team.');
        }
    
        public function validateTeam(TeamState $team)
        {
            $this->assert($team->seats_available > 0, 'This team does not have any more seats available.');
        }
    }
  10. Implement a Global Report Generated event in Thunk Verbs

    The GlobalReportGenerated event demonstrates how to use the #[AppliesToState] attribute to bind an event to a specific state class (GlobalReportState) and the #[Once] attribute to ensure the handler runs only once. In this example, the event extracts subscription metrics from the state, persists them into a Report model, and then triggers a ResetGlobalReportState event to clear the state for the next reporting period.
    namespace Thunk\Verbs\Examples\Subscriptions\Events;
    
    use Thunk\Verbs\Attributes\Autodiscovery\AppliesToState;
    use Thunk\Verbs\Attributes\Hooks\Once;
    use Thunk\Verbs\Event;
    use Thunk\Verbs\Examples\Subscriptions\Models\Report;
    use Thunk\Verbs\Examples\Subscriptions\States\GlobalReportState;
    
    #[AppliesToState(GlobalReportState::class)]
    class GlobalReportGenerated extends Event
    {
        #[Once]
        public function handle()
        {
            $state = $this->state(GlobalReportState::class);
    
            Report::create([
                'plan_id' => null,
                'subscribes_since_last_report' => $state->subscribes_since_last_report,
                'unsubscribes_since_last_report' => $state->unsubscribes_since_last_report,
                'total_subscriptions' => $state->total_subscriptions,
                'summary' => $state->summary(),
            ]);
    
            ResetGlobalReportState::fire();
        }
    }
  11. Fire and commit Verbs events

    Events are executed using the fire() method. When an event is fired, it is added to an in-memory queue and eventually 'committed' to the database in a single insert.

    Verbs::commit() is called automatically at the end of requests, console commands, and queued jobs. However, you may need to call it manually in tests or within database transactions to ensure events are included in the transaction.

    If you need to fire an event and immediately access the return value of its handle() method, use Event::commit() instead of fire().

    // Standard fire (queued for commit)
    PlayerAddedToGame::fire(game_id: $this->id, player_id: $player->id);
    
    // Manual commit in transaction
    DB::transaction(function() {
        CustomerRegistered::fire(...);
        Verbs::commit();
    });
    
    // Fire and commit immediately to get result
    $subscription = CustomerBeganTrial::commit(customer_id: Auth::id());
  12. Implement a ResetPlanReportState event in Thunk Verbs

    The ResetPlanReportState event demonstrates how to reset reporting metrics for a specific plan. It loads the PlanReportState associated with a given plan_id and resets the subscription/unsubscription counters to zero while updating the last_reported_at timestamp to the current time.
    namespace Thunk\Verbs\Examples\Subscriptions\Events;
    
    use Thunk\Verbs\Event;
    use Thunk\Verbs\Examples\Subscriptions\States\PlanReportState;
    use Thunk\Verbs\Support\StateCollection;
    
    class ResetPlanReportState extends Event
    {
        public int $plan_id;
    
        public function states(): StateCollection
        {
            return new StateCollection([
                PlanReportState::class => PlanReportState::load($this->plan_id),
            ]);
        }
    
        public function apply(PlanReportState $state)
        {
            $state->subscribes_since_last_report = 0;
            $state->unsubscribes_since_last_report = 0;
            $state->last_reported_at = now();
        }
    }