Verbs

States

States in Verbs are simple PHP objects containing data which is mutated over time by events. If that doesn't immediately give you a strong sense of what a state is, or why you would want one, you're not alone.

A Mental Model

Over time, you'll find your own analogue to improve your mental model of what a state does. This helps you understand when you need a state, and which events it needs to care about.

Here are some to start:

Stairs

Events are like steps on a flight of stairs. The entire grouping of stairs is the state, which accumulates and holds every step; the database/models will reflect where we are now that we've traversed the stairs.

Books

Events are like pages in a book, which add to the story; the state is like the spine--it holds the book together and contains the whole story up to now; the database/models are where we are in the story now that those pages have happened.

Generating a State

To generate a state, use the built-in artisan command:

1php artisan verbs:state GameState

When you create your first state, it will generate in a fresh app/States directory.

A brand new state file will look like this:

1namespace App\States;
2 
3use Thunk\Verbs\State;
4 
5class ExampleState extends State
6{
7 // It ain't my birthday but I got my name on the cake - Lil Wayne
8}

States and Events

Like our examples suggest, we use states for tracking changes across our events.

Thunk state files tend to be lean, focusing only on tracking properties and offloading most logic to the events themselves.

Applying Event data to your State

Use the apply() event hook with your state to update any data you'd like the state to track:

1// CountIncremented.php
2class CountIncremented class extends Event
3{
4 #[StateId(CountState::class)]
5 public int $example_id;
6 
7 public function apply(CountState $state)
8 {
9 $state->event_count++;
10 }
11}
12 
13// CountState.php
14class CountState extends State
15{
16 public $event_count = 0;
17}
18 
19// test or other file
20$id = snowflake_id();
21 
22CountIncremented::fire(example_id: $id);
23Verbs::commit();
24CountState::load($id)->event_count // = 1

If you have multiple states that need to be updated in one event, you can load both in the apply() hook, or even write separate, descriptive apply methods:

1public function applyToGameState(GameState $state) {}
2 
3public function applyToPlayerState(PlayerState $state) {}

On fire(), Verbs will find and call all relevant state and event methods prefixed with "apply".

Validating Event data using your State

It's possible to use your state to determine whether or not you want to fire your event in the first place. We've added a validate() hook for these instances. You can use validate() to check against properties in the state; if it returns false, the event will not fire.

You can use the built-in assert() method in your validate() check

1public function validate()
2{
3 $this->assert(
4 $game->started, // if this has not happened
5 'Game must be started before a player can join.' // then display this error message
6 )
7}

You can now see how we use the state to hold a record of event data--how we can apply() event data to a particular state, and how we can validate() whether the event should be fired by referencing that same state data. These and other hooks that helps us maximize our events and states are located in event lifecycle.

Loading a State

All state instances are singletons, scoped to an id. i.e. say we had a Card Game app--if we apply a CardDiscarded event, we make sure only the CardState state with its globablly unique card_id is affected.

To retrieve the State, simply call load:

1CardState::load($card_id);

The state is loaded once and then kept in memory. Even as you apply() events, it's the same, in-memory copy that's being updated, which allows for real-time updates to the state without additional database overhead.

You can also use loadOrFail() to trigger a StateNotFoundException that will result in a 404 HTTP response if not caught.

Using States in Routes

States implement Laravel’s UrlRoutable interface, which means you can route to them in the exact same way you would do route-model binding:

1Route::get('/users/{user_state}', function(UserState $user_state) {
2 // $user_state is automatically loaded for you!
3});

Singleton States

You may want a state that only needs one iteration across the entire application--this is called a singleton state. Singleton states require no id, since there is no need to differentiate among state instances.

In our events that apply to a singleton state, we simply need to use the AppliesToSingletonState attribute.

1#[AppliesToSingletonState(CountState::class)]
2class IncrementCount extends Event
3{
4 public function apply(CountState $state)
5 {
6 $state->count++;
7 }
8}

This event uses AppliesToSingletonState to tell Verbs that it should always be applied to a single CountState across the entire application (as opposed to having different counts for different situations).

Loading the singleton state

Since singleton's require no IDs, simply call the singleton() method.

1YourState::singleton();

State Collections

Your events may sometimes need to affect multiple states.

Verbs supports State Collections out of the box, with several convenience methods:

1$event_with_single_state->state(); // State
2$event_with_multiple_states->states(); // StateCollection

alias(?string $alias, State $state)

Allows you to set a shorthand name for any of your states.

1$collection->alias('foo', $state_1);

You can also set state aliases by setting them in the optional params of some of our attributes: any #[AppliesTo] attribute, and #[StateId].

get($key, $default = null)

Like the get() collection method, but also preserves any aliases. Returns a state.

1$collection->get(0); // returns the first state in the collection
2$collection->get('foo'); // returns the state with the alias

ofType(string $state_type)

Returns a state collection with only the state items of the given type.

1$collection->ofType(FooState::class);

firstOfType()

Returns the first() state item with the given type.

1$collection->firstOfType(FooState::class);

withId(Id $id)

(Id is a stand-in for Bits|UuidInterface|AbstractUid|int|string)

Returns the collection with only the state items with the given id.

1$collection->withId(1);

filter(?callable $callback = null)

Like the filter() collection method, but also preserves any aliases. Returns a state collection.

1$activeStates = $stateCollection->filter(function ($state) {
2 return $state->isActive;
3});

What should be a State?

We find it a helpful rule of thumb to pair your states to your models. States are there to manage event data in memory, which frees up your models to better serve your frontfacing UI needs. Once you've converted to Unique IDs, you can use your state instance's id to correspond directly to a model instance.

1class FooCreated class
2{
3 #[StateId(FooState::class)]
4 public int $foo_id;
5 
6 // etc
7 
8 public function handle()
9 {
10 Foo::create(
11 snowflake: $this->foo_id
12 );
13 }
14}

That said: if you ever find yourself storing complex, nested, multi-faceted data in arrays, collections, or objects on your state, you probably need another state. Particularly if the data in those collections, arrays, or objects is ever going to change.

Read more about the role states play in State-first development.

State Cache

By default, Verbs keeps up to 100 State objects in memory at one time. Most applications will never need more than a handful of States for any given request, but if your application needs to operate on hundreds of States during a single request/command/job, you’ll need to increase the state_cache_size configuration value.