Hey there! Today I want to share a common problem I’ve faced many times in PHP projects: a method full of if/else if statements that handles different types of orders. You know the type—huge, unreadable, and almost impossible to extend without breaking something. 😅
In this article, I’ll show you how to refactor such if-else hell into something much cleaner using the Strategy Pattern. By the end, you’ll see how flexible, testable, and maintainable your code can become.
1️⃣ The Problem: Long If-Else Chains
Let’s imagine we have a simple OrderProcessor class:
class OrderProcessor {
public function process(Order $order) {
if ($order->type === 'digital') {
echo "Processing digital order\n";
// some digital-specific logic
} elseif ($order->type === 'physical') {
echo "Processing physical order\n";
// physical-specific logic
} elseif ($order->type === 'subscription') {
echo "Processing subscription order\n";
// subscription-specific logic
} else {
throw new Exception("Unknown order type");
}
}
}
At first glance, it works… but as soon as you add more types or more complex rules, this method grows quickly.
Problems with this approach:
- Adding a new order type requires editing this method → violates Open/Closed Principle.
- Hard to test individual behaviors without setting up multiple scenarios.
- Readability decreases as the logic grows.
This is the classic if-else hell—something we want to avoid.
2️⃣ Diagnosing the Problem
Before refactoring, it’s good to understand the main issues:
- Duplicated logic across conditions.
- High coupling: OrderProcessor knows about every type.
- Poor testability: you can’t isolate behavior easily.
So what do we want? We want a solution where:
- Each order type handles its own logic.
- Adding a new type does not break existing code.
- The code is easy to test and extend.
Enter the Strategy Pattern. 🛠️
3️⃣ Refactoring Step by Step
Step 1: Define a Strategy Interface
First, we create an interface that all order strategies will implement:
interface OrderStrategy {
public function process(Order $order): void;
}
This way, every strategy guarantees it can process an Order.
Step 2: Implement Concrete Strategies
Next, we create a class for each order type:
class DigitalOrderStrategy implements OrderStrategy {
public function process(Order $order): void {
echo "Processing digital order\n";
// digital-specific logic here
}
}
class PhysicalOrderStrategy implements OrderStrategy {
public function process(Order $order): void {
echo "Processing physical order\n";
// physical-specific logic here
}
}
class SubscriptionOrderStrategy implements OrderStrategy {
public function process(Order $order): void {
echo "Processing subscription order\n";
// subscription-specific logic here
}
}
Notice how each class o*nly handles its own behavior*. Much cleaner, right?
Step 3: Create a Context (OrderProcessor)
Now, we modify OrderProcessor to use these strategies:
class OrderProcessor {
private array $strategies;
public function __construct(array $strategies) {
$this->strategies = $strategies;
}
public function process(Order $order) {
if (!isset($this->strategies[$order->type])) {
throw new Exception("Unknown order type");
}
$this->strategies[$order->type]->process($order);
}
}
We pass an array of strategies to the processor. The processor doesn’t care how each strategy works, only that it can process the order.
Step 4: Using the Refactored Code
$processor = new OrderProcessor([
'digital' => new DigitalOrderStrategy(),
'physical' => new PhysicalOrderStrategy(),
'subscription' => new SubscriptionOrderStrategy(),
]);
$order = new Order('digital');
$processor->process($order);
See how easy it is to add a new type? Just create a new class implementing OrderStrategy and add it to the array. No need to touch existing logic.
4️⃣ Testing Strategies
One of the biggest advantages: testing becomes simple. Each strategy can be tested in isolation:
class DigitalOrderStrategyTest extends TestCase {
public function testProcess() {
$order = new Order('digital');
$strategy = new DigitalOrderStrategy();
$this->expectOutputString("Processing digital order\n");
$strategy->process($order);
}
}
Repeat for other strategies!
5️⃣ Benefits of Using Strategy Pattern
- Open/Closed Principle: Adding new behaviors doesn’t break existing code.
- Single Responsibility: Each class has one reason to change.
- Testability: Strategies are isolated and easy to test.
- Extensibility: Adding a new order type is trivial.
6️⃣ Quick Tips & Best Practices
- Use Factory if you have many strategies: Helps with dependency injection.
- Keep strategies small: Each class should do one thing.
- Don’t overuse: If you only have 2-3 cases, sometimes a simple if/else is fine. Strategy shines with growing variability.
- Document the intent: Strategy is about interchangeable behavior, not just splitting code.
7️⃣ Conclusion
Refactoring if-else hell into a Strategy Pattern in PHP is a straightforward way to clean up messy code, make it extensible, and improve testability.
- Identify the long if-else chains.
- Extract each behavior into a separate class implementing a common interface.
- Use a context class (or processor) to delegate work.
- Enjoy your cleaner, flexible code.
Give it a try on your next messy class—you’ll thank yourself later.
Top comments (0)