php 72 lines · 7 steps

How Eloquent scopes model order status in Laravel

An Order model uses query scopes and guarded transitions to keep status changes valid and queries readable.

Explained by highlit
1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Builder;
6use Illuminate\Database\Eloquent\Factories\HasFactory;
7use Illuminate\Database\Eloquent\Model;
8 
9class Order extends Model
10{
11 use HasFactory;
12 
13 public const STATUS_PENDING = 'pending';
14 public const STATUS_PAID = 'paid';
15 public const STATUS_SHIPPED = 'shipped';
16 public const STATUS_CANCELLED = 'cancelled';
17 
18 protected $fillable = ['status', 'total', 'customer_id'];
19 
20 protected $casts = [
21 'total' => 'decimal:2',
22 'shipped_at' => 'datetime',
23 ];
24 
25 public function scopePending(Builder $query): Builder
26 {
27 return $query->where('status', self::STATUS_PENDING);
28 }
29 
30 public function scopePaid(Builder $query): Builder
31 {
32 return $query->where('status', self::STATUS_PAID);
33 }
34 
35 public function scopeShipped(Builder $query): Builder
36 {
37 return $query->where('status', self::STATUS_SHIPPED)
38 ->whereNotNull('shipped_at');
39 }
40 
41 public function scopeCancelled(Builder $query): Builder
42 {
43 return $query->where('status', self::STATUS_CANCELLED);
44 }
45 
46 public function scopeStale(Builder $query, int $minutes = 30): Builder
47 {
48 return $query->pending()
49 ->where('created_at', '<=', now()->subMinutes($minutes));
50 }
51 
52 public function markPaid(): bool
53 {
54 if ($this->status !== self::STATUS_PENDING) {
55 return false;
56 }
57 
58 return $this->update(['status' => self::STATUS_PAID]);
59 }
60 
61 public function ship(): bool
62 {
63 if ($this->status !== self::STATUS_PAID) {
64 return false;
65 }
66 
67 return $this->update([
68 'status' => self::STATUS_SHIPPED,
69 'shipped_at' => now(),
70 ]);
71 }
72}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Query scopes turn repeated where clauses into named, reusable filters you can chain.
  2. 2Constants for status values keep string comparisons consistent across queries and transitions.
  3. 3Guarding state changes inside the model prevents invalid transitions from ever reaching the database.

Related explainers

Share this explainer

Here's the card — post it anywhere.

How Eloquent scopes model order status in Laravel — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code