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.
Link to previous article below:
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");
}
}
}
PHPAt first glance, it works — and for three stable, simple cases like this, it may be all you need. The problem appears when each branch starts growing its own sub-conditions, or when new types are added every few weeks by different developers.
Problems with this approach — at scale:
- 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.
Note: these problems appear at scale. A three-branch if/else with simple method calls is not inherently wrong — it is direct, traceable, and easy to read. The refactoring below is aimed at the version that has already outgrown that simplicity.
2️⃣ Diagnosing the Problem
The core problem is not the if/else itself. The problem is that multiple independently changing behaviors are encoded in a single place.
- Duplicated logic across conditions.
- High coupling:
OrderProcessorknows 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️⃣ Before You Refactor: The KISS Check
Before reaching for any pattern, ask yourself: would a colleague unfamiliar with this codebase understand this code in 30 seconds?
php
// This is fine. Don't touch it.
public function process(Order $order) {
if ($order->type === 'digital') {
$this->sendDownloadLink($order);
} elseif ($order->type === 'physical') {
$this->scheduleShipment($order);
} elseif ($order->type === 'subscription') {
$this->activateSubscription($order);
} else {
throw new \InvalidArgumentException("Unknown order type: {$order->type}");
}
}PHPThree types, three clear method calls, one place to look. Any developer can open this file and immediately understand what happens for each order type. That has real value.
The Strategy Pattern earns its complexity when branches start looking like this instead:
} elseif ($order->type === 'physical') {
if ($order->weight > 30) {
// freight carrier logic
if ($order->destination === 'international') {
// customs declaration logic
// 15 more lines
}
} elseif ($order->destination === 'international') {
// different international logic
}
// 40 more lines of physical-specific handling
}PHPThat’s the moment the if/else stops being a straight line between two points — and the Strategy Pattern starts paying for itself.
4️⃣ 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;
}
PHPThis 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
}
}
PHPEach class only handles its own behavior. Notice that this only makes sense because each branch would carry substantial, independently evolving logic — not just a single method call.
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);
}
}
PHPWe 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);
PHPAdding a new type means creating a new class and registering it here. No existing logic is touched.
One honest trade-off to name explicitly: runtime dispatch means your IDE and static analysis tools cannot tell you which strategy executes for a given order type without running the code. In the simple if/else version, you can trace the entire flow by reading one method top to bottom. That traceability has value — keep it in mind when deciding whether this refactoring is warranted.
5️⃣ 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);
}
}
PHPRepeat for other strategies!
6️⃣ Benefits — and Their Costs
Every benefit comes with a cost worth naming honestly:
| Benefit | Trade-off |
|---|---|
| Open/Closed: new types don’t break existing code | More files to navigate; runtime registration hides intent from static analysis |
| Single Responsibility: each class has one reason to change | Logic for one domain is now spread across multiple classes |
| Testability: strategies are isolated and easy to test | End-to-end flow is harder to trace without running the code |
| Extensibility: adding a new type is trivial | Initial setup cost is non-trivial |
7️⃣ Guidelines for Keeping It Effective
- Use a Factory if you have many strategies — helps with dependency injection and keeps registration centralised.
- Keep strategies small — each class should do one thing.
- Don’t overuse — if you only have 2–3 stable cases with simple logic, a plain
if/elseis the right choice. Strategy shines with growing, independently changing variability. - Document the intent — Strategy is about interchangeable behavior, not just splitting code into smaller files.
When the Strategy Pattern Is the Wrong Choice
The Strategy Pattern introduces more classes, more indirection, and one significant practical cost: runtime dispatch breaks static traceability. Your IDE cannot tell you which strategy executes for a given order type without running the code. In a codebase you need to audit or debug quickly, that matters.
If you only have:
- two or three simple, stable cases
- logic that is unlikely to change
- no need for extensibility
Then a simple conditional is perfectly fine — and arguably better.
In some cases, alternatives may be more appropriate:
- Associative arrays of callbacks — lighter weight, still avoids the chain
- PHP 8
matchexpressions — clean, exhaustive, statically analysable - Small command objects — useful when each action carries its own context
Conclusion
Refactoring if/else chains into a Strategy Pattern in PHP is a powerful way to clean up messy code, make it extensible, and improve testability — when the complexity justifies it.
The steps are straightforward:
- Identify chains where branches grow independently and carry substantial logic.
- Extract each behavior into a separate class implementing a common interface.
- Use a context class to delegate work.
- Accept the trade-off on static traceability consciously, not accidentally.
Refactoring long if/else chains into the Strategy Pattern is not about elegance — it is about reducing risk in systems that are actively growing.
The next time you encounter a method that keeps growing with every new requirement, it is worth asking two questions:
Is this logic really meant to live in one place?
And equally: is it complex enough to justify living in multiple places?
The goal is not clean patterns — it is code that the next developer can follow without running it in their head first.
Next good source of information about this topic is here https://refactoring.guru/design-patterns/strategy.

