An eloquent way of handling model state

Rasmus Christoffer Nielsen
5 min readMay 24, 2018

Almost every application has some sort of state to it. It’s why we have databases.

However, often times you’ll find that your models, at any given point, has a certain status to them too. A few examples could be:

  • Post: draft, private, published,
  • Job: applied, accepted, declined, completed, cancelled
  • Approval: pending, reviewing, approved

In this blog post we’ll review some of the traditional approaches, discuss pro’s and con’s, and finally uncover how a few packages by the end will allow us to write zonda code 🔥

TLDR
With makeabledk/laravel-querykit you can reuse your eloquent query-scopes and check if a model passes a given scope:
$approval->passesScope('pending')

Furthermore using makeabledk/laravel-eloquent-status allows you to put all statuses into its own class meanwhile achieve a sweet syntax like Approval::status('pending')->get() and
$approval->checkStatus('pending')

Read on!

Our example

Let’s use the Approval example from before and setup some ground rules.

Business case

1. In a classroom environment, students are handing in assignments for approval..
An approval is given by three different actors to be complete:
a tutor, teacher and external assessor.

2. They will always give their approval in that order.

3. The different stages the approval undergoes are pending, reviewing and approved.

Let’s review a few traditional ways we may accomplish this behavior..

Traditional approach 1:

Manipulate a status field in your database

Keeping it totally simple, we could just have a status field in the database that we change to reflect the current status.

It would look something along the lines of this:

  1. New approval: statuspending
  2. Tutor approves: statusreviewing
  3. Teacher approves: teacher_approved1
    Wait! what? Yes, we need to track that the teacher approved, so we’ll know that for when the assessor approves. But we aren’t ready to change the status just yet…
  4. Assessor approves: statusapproved

As we can see it get’s a little hairy because not every action results in a status change.
Realistically you’ll therefore end up with separate database fields for every action. Also, instead of using booleans let’s do timestamps for better history preservation.

Now our table looks something like this:

Pros

  • Easy querying of status
  • Easy manipulation of status — just change it at the right time

Cons

  • Not one source of truth. The status is not derived from the timestamps but rather just being manipulated at different triggers
  • No trail as to how the status became what it is. Especially with changing domain logic this becomes a big pitfall
  • Responsibility of changing the status possibly leaks out in the application

Traditional approach 2:

Determine status by querying your database

Continuing from the previous setup, let’s drop the status field in the database and just query on the fly.

Luckily this is a cinch in Laravel!

With the above scope function we can easily query all approvals and just fetch the pending ones with: Approval::pending()->get() // Collection

But what if we needed to check if a given approval was in fact approved before performing some action? Ouch 🤕

Have your models ever bloated up like that too?

Pros

  • Keeps data-integrity
  • Statuses can be altered without migrations as business changes
  • Clear definitions of each status that matches the business rules

Cons

  • Often times you’ll find yourself duplicating logic to query the database & single instances respectively. This makes it error-prone in case of changes.
  • Your models easily become bloated with query-scopes and helper-functions.

The enhanced eloquent approach

Reuse your scope functions when checking your models

Huh, come again?

Frustrated by the code-duplication from previous example, at makeable.dk we decided to come up with a solution for it.

That solution is called makeabledk/laravel-querykit and it allows you to check if a model instance adheres to a scope function.

Simply install the package and add the QueryKit trait to your model, and you are good to go!

$ composer require makeabledk/laravel-querykit

As usual you can query against your database with eloquent:

Approval::pending()->get() // Collection

But now you can also check if a single instance passes your scope:

$approval->passesScope('pending'); // true or false

Under the hood this is achieved by passing the scope a custom query builder that exposes the same interface as the native laravel builder.
However, instead of searching your database it compares against your model-attributes.

Pros (same as previous)

  • Keeps data-integrity
  • Statuses can be altered without migrations as business changes
  • Clear definitions of each status that matches the business rules

Cons

  • O̵f̵t̵e̵n̵ ̵t̵i̵m̵e̵s̵ ̵y̵o̵u̵’̵l̵l̵ ̵f̵i̵n̵d̵ ̵y̵o̵u̵r̵s̵e̵l̵f̵ ̵d̵u̵p̵l̵i̵c̵a̵t̵i̵n̵g̵ ̵l̵o̵g̵i̵c̵ ̵t̵o̵ ̵q̵u̵e̵r̵y̵ ̵t̵h̵e̵ ̵d̵a̵t̵a̵b̵a̵s̵e̵ ̵&̵ ̵s̵i̵n̵g̵l̵e̵ ̵i̵n̵s̵t̵a̵n̵c̵e̵s̵ ̵r̵e̵s̵p̵e̵c̵t̵i̵v̵e̵l̵y̵.̵ ̵T̵h̵i̵s̵ ̵m̵a̵k̵e̵s̵ ̵i̵t̵ ̵e̵r̵r̵o̵r̵-̵p̵r̵o̵n̵e̵ ̵i̵n̵ ̵c̵a̵s̵e̵ ̵o̵f̵ ̵c̵h̵a̵n̵g̵e̵s̵.̵
    Fixed it!
  • Your models easily become bloated with query-scopes and helper-functions.

We’re almost there!

🔥 Introducing status classes

Consider this:

  • Instead of putting everything in the model, we make a dedicated ApprovalStatus that holds the query scopes. Business definitions are encapsulated in one place.
  • When querying Approval we pass the ApprovalStatus object

This is exactly what makeabledk/laravel-eloquent-status allows you to do.

Let’s look some code.

You may query your database with:
Approval::status(new ApprovalStatus('reviewing'))->get() // Collection

And check your model with:
$approval->checkStatus(new ApprovalStatus('reviewing')) // boolean

This is pretty good already. But since it can get a little verbose with the new ApprovalStatus() all the time, we can sugar it up a bit more!

In your AppServiceProvider ‘s boot method you may bind a default status class a model with:
StatusManager::bind(Approval::class, ApprovalStatus::class);

Now you may simply do
$pendingApproval->checkStatus('pending') // true

🔥🔥

Pros

  • Keeps data-integrity
  • Statuses can be altered without migrations as business changes
  • Clear definitions of each status that matches the business rules
  • All definitions together in one place. You can even get available statuses with ApprovalStatus::all() // ['pending', 'reviewing', 'approved']
  • No enum ‘s and no typos. You can only pass valid statuses or an exception will be thrown.
    For this reason you may even pass raw $_GET in your controller method return Approval::status($_GET['filter'])->get()
  • Uniformed way of defining model-statuses all across your application!
  • No more if-switches!

Cons

  • Not all query-builder methods are available in QueryKit, especially raw SQL is a no-go.
    Nevertheless, in by far most use-cases you will be able to determine model-status from its own attributes.
    In the rare cases it’s not, sometimes it can be a hint to review your database structure or leverage techniques such as denormalization.

--

--