Hey there!
Let’s talk about something magical that can make your Laravel projects cleaner, easier to understand, and a lot more fun to work with the state pattern. Don’t worry if you’re not familiar with it yet, I will explain everything using a simple and relatable example.
Why the State Pattern is Awesome
Clean Code: No more messy if-else blocks. Each state has its own class and handles its own logic.
Easy to Add States: Need a new state? Just create a new class. No need to touch existing code.
Safe Transitions: You can’t accidentally do something invalid, like shipping a pending order.
Readable and Fun: The code is easier to read, understand, and maintain.
Why Use the State Pattern?
Imagine you’re building a shopping app. Orders can go through several stages:
- Pending : The order is created but not confirmed yet.
- Confirmed : The order is confirmed and ready to be packed.
- Shipped : The order is on its way to the customer.
- Delivered : The order has been delivered.
Each of these states has its own rules. For example, you can’t ship an order that’s still pending, and you can’t deliver an order that hasn’t been shipped. Writing this logic with a bunch of if-else
or switch
statements can get really messy, really fast. That’s where the state pattern comes in to save the day!
The state pattern helps us organize this logic by creating separate classes for each state. It’s like giving each state its own little brain to handle what it can and cannot do. Cool, right?
The Order States: Breaking It Down
Let’s say our order can do three things:
- Confirm : Move the order to the "Confirmed" state.
- Ship : Move the order to the "Shipped" state.
- Deliver : Move the order to the "Delivered" state.
We’ll create a system where each state knows what actions it can perform. Ready? Let’s build it!
Step 1: Create the State Interface
The OrderStateInterface
is like a rulebook for all order states. It makes sure every state knows what to do when you confirm, ship, or deliver an order. You'll find this interface in App/States/Order/OrderStateInterface.php
, and it keeps things organized by requiring each state to follow the same set of rules.
namespace App\States\Order;
interface OrderStateInterface
{
public function confirm(): void;
public function ship(): void;
public function deliver(): void;
}
Step 2: The Base State Class
We’ll make a base class for all states. If a state doesn’t support an action, it will throw an error.
namespace App\States\Order;
use App\Models\Order;
use Exception;
abstract class OrderState implements OrderStateInterface
{
public function __construct(protected Order $order)
{
}
public function confirm(): void
{
throw new Exception("You can’t confirm this order right now!");
}
public function ship(): void
{
throw new Exception("You can’t ship this order right now!");
}
public function deliver(): void
{
throw new Exception("You can’t deliver this order right now!");
}
}
Step 3: Create the Concrete States
Pending State
The order is still pending and can only be confirmed.
namespace App\States\Order;
class PendingState extends OrderState
{
public function confirm(): void
{
$this->order->update(['status' => 'confirmed']);
$this->order->state = new ConfirmedState($this->order);
// Handle notifications, emails, etc.
}
}
Confirmed State
The order has been confirmed and can now be shipped.
namespace App\States\Order;
class ConfirmedState extends OrderState
{
public function ship(): void
{
$this->order->update(['status' => 'shipped']);
$this->order->state = new ShippedState($this->order);
// Handle notifications, emails, etc.
}
}
Shipped State
The order is on its way and can now be delivered.
namespace App\States\Order;
class ShippedState extends OrderState
{
public function deliver(): void
{
$this->order->update(['status' => 'delivered']);
$this->order->state = new DeliveredState($this->order);
// Handle notifications, emails, etc.
}
}
Delivered State
The order has been delivered. Nothing more can be done.
namespace App\States\Order;
class DeliveredState extends OrderState
{
// No actions allowed here.
// Handle notifications, emails, etc.
}
Step 4: Update the Order Model
The order model will figure out what state it’s in and behave accordingly.
namespace App\Models;
use App\States\Order\{OrderStateInterface, PendingState, ConfirmedState, ShippedState, DeliveredState};
class Order extends Model
{
protected $fillable = ['status'];
public function orderState(): OrderStateInterface
{
return match ($this->status) {
'pending' => new PendingState($this),
'confirmed' => new ConfirmedState($this),
'shipped' => new ShippedState($this),
'delivered' => new DeliveredState($this),
default => throw new \Exception("Unknown state"),
};
}
}
Step 5: Using It in a Controller
Here I am taking different methods to use states for examples:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Order;
use App\States\Order\PendingState;
use Exception;
class OrderController extends Controller
{
public function confirm(Order $order)
{
try {
if ($order->orderState() instanceof PendingState) {
$order->orderState()->confirm();
return redirect()->route('orders.index')->with('success', "Order #{$order->id} has been confirmed.");
}
return redirect()->route('orders.index')->with('error', "Order #{$order->id} cannot be confirmed in its current state.");
} catch (Exception $e) {
return redirect()->route('orders.index')->with('error', $e->getMessage());
}
}
public function updateState(Order $order, string $action)
{
try {
if (!method_exists($order->orderState(), $action)) {
return redirect()->route('orders.index')->with('error', "Invalid action: {$action} for Order #{$order->id}.");
}
$order->orderState()->$action();
return redirect()->route('orders.index')->with('success', "Order #{$order->id} has been updated successfully.");
} catch (Exception $e) {
return redirect()->route('orders.index')->with('error', $e->getMessage());
}
}
public function ship(Order $order)
{
return $this->updateState($order, 'ship');
}
public function deliver(Order $order)
{
return $this->updateState($order, 'deliver');
}
public function index()
{
return view('welcome');
}
}
Conclusion
The state pattern is a powerful way to manage application logic, making your Laravel app cleaner, more maintainable, and scalable. By organizing state-dependent behaviors into separate classes, you eliminate messy conditionals and improve readability. Implementing this approach in your projects will ensure a better development experience and fewer bugs. Try it out in your next Laravel project and see the magic happen!
Top comments (0)