Verbs

Events

In Verbs, Events are the source of your data changes. Before we fire an event, we give it all the data we need it to track, and we describe in the event exactly what it should do with that data once it's been fired.

Generating an Event

To generate an event, use the built-in artisan command:

1php artisan verbs:event CustomerBeganTrial

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

A brand-new event file will look like this:

1class MyEvent extends Event
2{
3 public function handle()
4 {
5 // what you want to happen
6 }
7}

Firing Events

To execute an event, simply call MyEvent::fire() from anywhere in your app.

When you fire the event, any of the event hooks you've added within it, like handle(), will execute.

Named Parameters

When firing events, include named parameters that correspond to its properties, and vice versa.

1// Game model
2PlayerAddedToGame::fire(
3 game_id: $this->id,
4 player_id: $player->id,
5);
6 
7// PlayerAddedToGame event
8#[StateId(GameState::class)]
9public string $game_id;
10 
11#[StateId(PlayerState::class)]
12public string $player_id;

Committing

When you fire() an event, it gets pushed to an in-memory queue to be saved with all other Verbs events that you fire. Think of this kind-of like staging changes in git. Events are eventually “committed” in a single database insert. You can usually let Verbs handle this for you, but may also manually commit your events by calling Verbs::commit().

Verbs::commit() is automatically called:

  • at the end of every request (before returning a response)
  • at the end of every console command
  • at the end of every queued job

In tests, you'll often need to call Verbs::commit() manually unless your test triggers one of the above.

Committing during database transactions

If you fire events during a database transaction, you probably want to call Verbs::commit() before the transaction commits so that your Verbs events are included in the transaction. For example:

1DB::transaction(function() {
2 // Some non-Verbs Eloquent calls
3 
4 CustomerRegistered::fire(...);
5 CustomerBeganTrial::fire(...);
6 
7 // …some more non-Verbs Eloquent calls
8 
9 Verbs::commit();
10});

Committing & immediately accessing results

You can also call Event::commit() (instead of fire()), which will both fire AND commit the event (and all events in the queue). Event::commit() also returns whatever your event’s handle() method returns, which is useful when you need to immediately use the result of an event, such as a store method on a controller.

1// CustomerBeganTrial event
2public function handle()
3{
4 return Subscription::create([
5 'customer_id' => $this->customer_id,
6 'expires_at' => now()->addDays(30),
7 ]);
8}
9 
10// TrialController
11{
12 public function store(TrialRequest $request) {
13 $subscription = CustomerBeganTrial::commit(customer_id: Auth::id());
14 return to_route('subscriptions.show', $subscription);
15 }
16}

handle()

Use the handle() method included in your event to update your database / models / UI data. You can do most of your complex business logic by utilizing your state, which allows you to optimize your eloquent models to handle your front-facing data. Any States that you type-hint as parameters to your handle() method will be automatically injected for you.

1class CustomerRenewedSubscription extends Event
2{
3 #[StateId(CustomerState::class)]
4 public int $customer_id;
5 
6 public function handle(CustomerState $customer)
7 {
8 Subscription::find($customer->active_subscription_id)
9 ->update([
10 'renewed_at' => now(),
11 'expires_at' => now()->addYear(),
12 ]);
13 }
14}

Firing additional Events

If you want your event to trigger subsequent events, use the fired() hook.

We'll start with an easy example, then a more complex one. In both, we'll be applying event data to your state only. In application, you may still use any of Verbs' event hooks in your subsequent events.

fired()

1CountIncrementedTwice::fire(count_id: $id);
2 
3// CountIncrementedTwice event
4public function fired()
5{
6 CountIncremented::fire(count_id: $this->count_id);
7 CountIncremented::fire(count_id: $this->count_id);
8}
9 
10// CountIncremented event
11public function apply(CountState $state)
12{
13 $state->count++;
14}
15 
16// test or other file
17CountState::load($id)->count; // 2

The fired() hook executes in memory after the event fires, but before it's stored in the database. This allows your state to take care of any changes from your first event, and allows you to use the updated state in your next event. In our next example, we'll illustrate this.

Let's say we have a game where a level 4 Player levels up and receives a reward.

1PlayerLeveledUp::fire(player_id: $id);
2 
3// PlayerLeveledUp event
4public function apply(PlayerState $state)
5{
6 $state->level++;
7}
8 
9public function fired()
10{
11 PlayerRewarded::fire(player_id: $this->player_id);
12}
13 
14// PlayerRewarded event
15public function apply(PlayerState $state)
16{
17 if ($state->level === 5) {
18 $state->max_inventory = 100;
19 }
20}
21 
22// test or other file
23PlayerState::load($id)->max_inventory; // 100;

Naming Events

Describe what (verb) happened to who (noun), in the format of WhoWhat

OrderCancelled, CarLocked, HolyGrailFound

Importantly, events happened, so they should be past tense.

Replaying Events

Replaying events will rebuild your application from scratch by running through all recorded events in chronological order. Replaying can be used to restore the state after a failure, to update models, or to apply changes in business logic retroactively.

When to Replay?

  • After changing your system or architecture, replaying would populate the new system with the correct historical data.

  • For debugging, auditing, or any such situation where you want to restore your app to a point in time, Replaying events can reconstruct the state of the system at any point in time.

Executing a Replay

To replay your events, use the built-in artisan command:

1php artisan verbs:replay

You may also use Verbs::replay() in files.

Warning
Verbs does not reset any model data that might be created in your event handlers. Be sure to either reset that data before replaying, or confirm that all handle() calls are idempotent. Replaying events without thinking thru the consequences can have VERY negative side effects. Because of this, upon executing the verbs:replay command we will make you confirm your choice, and confirm again if you're in production.

Preparing for a replay

Backup any important data--anything that's been populated or modified by events.

Truncate all the data that is created by your event handlers. If you don't, you may end up with lots of duplicate data.

One-time effects

You'll want to tell verbs when effects should NOT trigger on replay (like sending a welcome email). You may use:

Verbs::unlessReplaying()
1Verbs::unlessReplaying(function () {
2 // one-time effect
3});

Or the #[Once] attribute.

Firing during Replays

During a replay, the system isn't "firing" the event in the original sense (i.e., it's not going through the initial logic that might include checks, validations, or triggering of additional side effects like sending one-time notifications). Instead, it directly applies the changes recorded in the event store.

See also: Event Lifecycle

Wormholes

When replaying events, Verbs will set the "now" timestamp for Carbon and CarbonImmutable instances to the moment the original event was stored in the database. This allows you to use the now() helper in your event handlers easily. You can disable this feature if you'd like in config/verbs.php.

See also: Event Metadata