PHP
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Object-Oriented PHP
-
Section 5: Working with Data
-
Section 6: Modern PHP (PHP 8)
-
Section 7: Working with Files and Networking
-
Section 8: Common PHP Frameworks Overview
-
Section 9: Tooling and Ecosystem
-
Section 10: Practical Projects
-
Section 11: Interview Practice
-
Section 12: More Standard Library
-
Section 13: Data Structures and Algorithms in PHP
-
Section 14: More Practice Exercises
-
Section 15: More Security and Best Practices
-
Section 16: More Testing and Tooling
-
Section 17: WordPress-Style CMS Concepts
-
Section 18: Advanced OOP Practice
-
Section 19: More Web Fundamentals
-
Section 20: Database Practice
-
Section 21: PHP Manual: Array Functions
-
Section 22: PHP Manual: Date and Calendar Functions
-
Section 23: PHP Manual: Filesystem and Directory Functions
-
Section 24: PHP Manual: Filter and Var Handling
-
Section 25: PHP Manual: Math Functions
-
Section 26: PHP Manual: JSON and XML
-
Section 27: PHP Manual: Network and Stream Functions
-
Section 28: PHP Manual: Error and Exception Handling
-
Section 29: PHP Manual: Output Control and Misc
-
Section 30: PHP Manual: FTP, Zip, and Mail
-
Section 31: Modern PHP Frameworks Deep Dive
-
Section 32: PHP Design Patterns
-
Section 33: More Practice Exercises
-
Section 34: PHP Performance and Deployment
-
Section 35: More Interview Practice
-
Section 36: More PHP Standard Library
-
Section 37: PHP Concurrency and Async
-
Section 38: More Web Development Practice
-
Section 39: PHP Testing Deep Dive
-
Section 40: Composer and Package Development
-
Section 41: PHP Security Deep Dive
-
Section 42: More Practical Projects
-
Section 43: Legacy PHP Maintenance
-
Section 44: More Algorithm Practice
-
Section 45: Final Practice and Review
-
Section 46: PHP for E-Commerce Patterns
-
Section 47: PHP API Design Deep Dive
-
Section 48: PHP Caching Strategies
-
Section 49: PHP Queue and Background Jobs
-
Section 50: PHP Multi-Tenancy Patterns
-
Section 51: PHP Real-Time Features
-
Section 52: PHP CMS and Content Modeling
-
Section 53: PHP Internationalization
-
Section 54: More Framework-Specific Practice
-
Section 55: PHP Legacy Code Refactoring
-
Section 56: More Practice Projects Round 2
-
Section 57: PHP Command-Line Applications
-
Section 58: PHP and Microservices
-
Section 59: More Interview and Review Round 2
195: Order Fulfillment Workflow Design
I was looking at a legacy project last week—a custom e-commerce build for a client—and I found something that made my skin crawl. They were handling order fulfillment by just updating a status column in the database. It was literally just a string: 'pending', 'paid', 'shipped'. No logic, no guards, just UPDATE orders SET status = 'shipped' WHERE id = 123.
The problem? The warehouse staff accidentally marked an order as "Shipped" before the payment gateway had even confirmed the funds. The product left the building, the money never arrived, and the developer spent three days trying to figure out why the "Payment Received" email didn't trigger. That's why we're talking about workflow design today.
The danger of the "Set Status" method
If I were writing this quickly, I might just write a method like this in my Order class:
public function setStatus(string $status): void {
$this->status = $status;
$this->save();
}
Seems clean, right? But let's actually try to use it. Imagine our intended flow is: Pending → Paid → Processing → Shipped.
$order = Order::find(123);
$order->setStatus('shipped'); // I just skipped 'paid' and 'processing' entirely.
PHP doesn't care. The database doesn't care. But the business definitely cares. I've just bypassed the payment check and the packing slip generation. We can't just let the status be "set"; we need to define "transitions."
Mapping the legal moves
I want to move away from "setting" a value and move toward "transitioning" to a state. I'll start by defining a map of what is actually allowed. If I'm in Pending, I can go to Paid or Cancelled. I absolutely cannot go straight to Shipped.
class OrderWorkflow {
private array $transitions = [
'pending' => ['paid', 'cancelled'],
'paid' => ['processing'],
'processing' => ['shipped'],
'shipped' => ['delivered'],
'cancelled' => [], // Terminal state
'delivered' => [], // Terminal state
];
public function canTransition(string $current, string $next): bool {
return in_array($next, $this->transitions[$current] ?? []);
}
}
Now, let's see what happens when I try to force a jump again. I'll wrap the status update in a check using this map.
$workflow = new OrderWorkflow();
$currentStatus = 'pending';
$nextStatus = 'shipped';
if (!$workflow->canTransition($currentStatus, $nextStatus)) {
throw new Exception("Illegal transition from $currentStatus to $nextStatus");
}
There we go. Now the code screams at me when I try to break the business rules. But here is where it gets tricky: fulfillment isn't just about changing a word in a database; it's about doing things. When an order moves to shipped, we need to email the customer. When it moves to paid, we need to alert the warehouse.
Hooking into the transition
If I put the email logic inside my Order class, that class becomes a bloated mess. Instead, I'll adjust my workflow logic to handle "side effects." I'll change the workflow to return a boolean, and if the transition is successful, I'll trigger a specific action.
I'll try a simple approach first: a switch statement. (I know, I know, it's not the most "elegant" design pattern, but let's see if it works for our needs before we over-engineer a State Pattern).
public function transitionTo(Order $order, string $nextStatus): void {
if (!$this->canTransition($order->getStatus(), $nextStatus)) {
throw new Exception("Invalid state transition.");
}
$oldStatus = $order->getStatus();
$order->setStatus($nextStatus);
$order->save();
$this->triggerSideEffects($oldStatus, $nextStatus, $order);
}
private function triggerSideEffects(string $from, string $to, Order $order): void {
if ($to === 'paid') {
$this->notifier->sendWarehouseAlert($order);
} elseif ($to === 'shipped') {
$this->notifier->sendShippingEmail($order);
}
}
I just realized a flaw here. What if sendShippingEmail fails? The order is already marked as shipped in the database, but the customer has no idea. In a real production system, I'd push these side effects into a queue (like RabbitMQ or Redis). For now, the key takeaway is that the workflow manager owns the transition logic, not the data model.
By separating the "what is allowed" (the map) from the "what happens" (the side effects), we've turned a fragile string update into a reliable business process. You've essentially built a finite state machine.
📋 Practical Task
Build a Fulfillment Guard System
You need to implement a FulfillmentManager class that prevents orders from being moved to an invalid state and triggers a simulated notification.
- Create a
FulfillmentManagerclass with a transition map. - Allowed paths:
'pending' → 'paid','paid' → 'shipped','shipped' → 'delivered', and'pending' → 'cancelled'. - Implement a
changeStatus(Order $order, string $newStatus)method. - The method must throw an
InvalidArgumentExceptionif the transition is not allowed. - If the transition is
'paid' → 'shipped', the method shouldecho: "Notification: Shipping label generated for Order #ID". - Create a simple
Orderclass withgetStatus()andsetStatus()methods to test your manager.
There are no comments for now.