Originally published on phpdesignpatterns.dev — part of the PHP design patterns course. View this lesson on the original site.
A safe path from if-chain to strategies
Going from the Before in lesson 2.2 to the After in lesson 2.4 in one commit is risky. Here's a safer path that keeps the code shippable at every step.
Refactoring flow
flowchart TD
S1["Step 1: Pin the current behaviour with tests"]
S2["Step 2: Extract one branch into a strategy"]
S3["Step 3: Repeat for each branch"]
S4["Step 4: Replace the if-chain with a lookup"]
S5["Step 5: Push the lookup up the call chain (optional)"]
S1 --> S2 --> S3 --> S4 --> S5
Step 1 — Pin the current behaviour with tests
If you don't already have tests for the calculator, write them now. Aim for one test per branch. They don't need to be elegant — they need to record what the code currently does.
<?php
it('gives a guest no discount', function () {
expect((new DiscountCalculator())->calculate('guest', 10000))->toBe(0);
});
it('gives standard customers a 5% discount above 50 quid', function () {
expect((new DiscountCalculator())->calculate('standard', 6000))->toBe(300);
});
// ... one per branch ...
These are characterisation tests (covered in writing-decent-php chapter 15). They lock in current behaviour before you change anything.
Step 2 — Extract one branch into a strategy
Pick the simplest branch first. NoDiscount from the guest case:
<?php
final class NoDiscount implements CalculatesDiscount
{
public function calculateDiscount(int $totalInPence): int
{
return 0;
}
}
Use it inside the calculator alongside the existing if chain:
<?php
if ($customerType === 'guest') {
return (new NoDiscount())->calculateDiscount($totalInPence);
}
The branch still exists — but the behaviour now lives in the strategy. Run the tests. They still pass.
Step 3 — Repeat for each branch
One branch per commit. Tests pass between each commit. Reviewers can see exactly what moved.
Step 4 — Replace the if-chain with a lookup
Once every branch delegates to a strategy, the calculator is just a switch from string to strategy:
<?php
final class DiscountCalculator
{
private array $strategies;
public function __construct()
{
$this->strategies = [
'guest' => new NoDiscount(),
'standard' => new StandardDiscount(),
'loyalty' => new LoyaltyDiscount(),
'loyalty_plus' => new LoyaltyPlusDiscount(),
'employee' => new EmployeeDiscount(),
'partner' => new PartnerDiscount(),
];
}
public function calculate(string $customerType, int $totalInPence): int
{
$strategy = $this->strategies[$customerType] ?? new NoDiscount();
return $strategy->calculateDiscount($totalInPence);
}
}
Step 5 — Push the lookup up the call chain (optional)
Often, the caller knows which strategy applies. Construction can move into a factory or a DI container, and the calculator goes away entirely. We'll meet that in chapter 3 (Factory Method).
Why this order matters
Every step keeps the code working and tested. Reviewers see small, obviously safe diffs. If the refactor has to be paused mid-way (a release goes out, priorities shift), the partial state is still better than the start, not worse. That's what makes Strategy land in real codebases instead of stalling in long-lived feature branches.
From PHP design patterns — Strategy: replacing branching with swappable behaviour