Quickstart
Let's start with an example of a Subscription service, where a customer begins a free trial.
Requirements
To use Verbs, first make sure you have the following installed:
Install Verbs
Install Verbs using composer:
1composer require hirethunk/verbs
Publish and Run Migrations
The last thing you need to do before you use Verbs is run migrations:
1php artisan vendor:publish --tag=verbs-migrations2php artisan migrate
Firing your first Event
To generate an event, use the built-in artisan command:
1php artisan verbs:event CustomerBeganTrial
This will generate an event in the app/Events
directory of your application, with a handle()
method baked-in.
For now, replace that with a $customer_id
:
1class CustomerBeganTrial extends Event2{3 public int $customer_id;4}
You can now fire this event anywhere in your code using:
1CustomerBeganTrial::fire(customer_id: 1);
(For this example we'll use a normal integer for our customer_id
, but Event Sourcing across your app requires Unique IDs).
Utilizing States
States in Verbs are simple PHP objects containing data which is mutated over time by events.
Say we want to prevent a customer from signing up for a free trial if they already signed up for one in the past year--we can use our state to help us do that.
Let's create a new state using another built-in artisan command:
1php artisan verbs:state CustomerState
This will create a CustomerState
class in our app/States
directory.
We'll customize it to add a timestamp.
1class CustomerState extends State2{3 public Carbon|null $trial_started_at = null;4}
Now that we have a state, let's tell our event about it.
Back on our event, add and import a #[StateId]
attribute above our $customer_id
property to tell Verbs that we want to look up the CustomerState
using this particular id.
1class CustomerBeganTrial extends Event2{3 #[StateId(CustomerState::class)]4 public int $customer_id;5}
Now our event can access the data on the state, and vice versa. Let's make it work for our scenario:
- We'll add a
validate()
method, which accepts an instance ofCustomerState
.- If the validate method returns
true
, the event can be fired. - If it returns
false
or throws an exception, the event will not be fired.
- If the validate method returns
- We'll add an
apply()
method, which also accepts an instance ofCustomerState
, to mutate the state when our event fires.
1class CustomerBeganTrial extends Event 2{ 3 #[StateId(CustomerState::class)] 4 public int $customer_id; 5 6 public function validate(CustomerState $state) 7 { 8 $this->assert( 9 $state->trial_started_at === null10 || $state->trial_started_at->diffInDays() > 365,11 'This user has started a trial within the last year.'12 );13 }14 15 public function apply(CustomerState $state)16 {17 $state->trial_started_at = now();18 }19}
(You can read more about apply
, validate
, and other event hooks, in event lifecycle).
Firing CustomerBeganTrial
now will allow the customer to start our free trial. Firing it again will cause it to fail validation and not execute.
Let's break down why:
- The first time you fire
CustomerBeganTrial
,validate()
will checkCustomerState
to see thattrial_started_at === null
, which allows the event to fire. - Then, it will
apply()
thenow()
timestamp to that property on the state. - This means that the next time you fire it (in less than a year),
validate()
will check the state, and see that$trial_started_at
is no longer null, which will break validation.
Updating the Database
We recommend starting with state-first development to smartly harness the power of events and states, like we did above. Eventually, however, you'll want to create some Eloquent models.
Say you have a Subscription model with database columns customer_id
and expires_at
; you can add a handle()
method to the end of your event to update your table:
1// after apply()2 3public function handle()4{5 Subscription::create([6 'customer_id' => $this->customer_id,7 'expires_at' => now()->addDays(30),8 ]);9}
Now, when the fired event is committed at the end of the request, a Subscription model will be created.