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 Event2{3 public function handle()4 {5 // what you want to happen6 }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// TrialController11{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 event11public function apply(CountState $state)12{13 $state->count++;14}15 16// test or other file17CountState::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 event15public function apply(PlayerState $state)16{17 if ($state->level === 5) {18 $state->max_inventory = 100;19 }20}21 22// test or other file23PlayerState::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.
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 effect3});
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