Refactoring If-Else Hell into a Strategy Pattern in PHP ⚙️

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:

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");
        }
    }
}
PHP

At first glance, it works… but as soon as you add more types or more complex rules, this method grows quickly.

  • 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.

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. 🛠️

First, we create an interface that all order strategies will implement:

interface OrderStrategy {
    public function process(Order $order): void;
}
PHP

This way, every strategy guarantees it can process an Order.

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
    }
}
PHP

Notice how each class only handles its own behavior. Much cleaner, right?

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);
    }
}
PHP

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.

$processor = new OrderProcessor([
    'digital' => new DigitalOrderStrategy(),
    'physical' => new PhysicalOrderStrategy(),
    'subscription' => new SubscriptionOrderStrategy(),
]);

$order = new Order('digital');
$processor->process($order);
PHP

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.

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);
    }
}
PHP

Repeat for other strategies!

  • 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.

  1. Use Factory if you have many strategies: Helps with dependency injection.
  2. Keep strategies small: Each class should do one thing.
  3. Don’t overuse: If you only have 2-3 cases, sometimes a simple if/else is fine. Strategy shines with growing variability.
  4. Document the intent: Strategy is about interchangeable behavior, not just splitting code.

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.

Next good source of information about this topic is here https://refactoring.guru/design-patterns/strategy.

Leave a Reply

Your email address will not be published. Required fields are marked *