Verbs

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-migrations
2php 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 Event
2{
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 State
2{
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 Event
2{
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 of CustomerState.
    • 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.
  • We'll add an apply() method, which also accepts an instance of CustomerState, 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 === null
10 || $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:

  1. The first time you fire CustomerBeganTrial, validate() will check CustomerState to see that trial_started_at === null, which allows the event to fire.
  2. Then, it will apply() the now() timestamp to that property on the state.
  3. 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.