Strategy is one of those patterns that seems like unnecessary complexity on first contact. Why wrap a single algorithm in a separate class? The value only becomes clear with a second and third algorithm – when instead of a growing if-else you have clean, interchangeable code. I show how it works in PHP and where Magento 2 uses this pattern without you realising it.
The problem without the pattern
<?php
class ShippingCalculator
{
public function calculate(string $method, float $weight): float
{
if ($method === 'flat') {
return 9.99;
} elseif ($method === 'weight') {
return $weight * 2.5;
} elseif ($method === 'free') {
return 0.0;
}
throw new \InvalidArgumentException('Unknown method: ' . $method);
}
}
Every new shipping method adds another elseif. That is where Strategy comes in.
The solution – Strategy pattern
<?php
interface ShippingStrategyInterface
{
public function calculate(float $weight): float;
}
class FlatRateStrategy implements ShippingStrategyInterface
{
public function calculate(float $weight): float { return 9.99; }
}
class WeightBasedStrategy implements ShippingStrategyInterface
{
public function __construct(private float $ratePerKg = 2.5) {}
public function calculate(float $weight): float
{
return $weight * $this->ratePerKg;
}
}
class FreeShippingStrategy implements ShippingStrategyInterface
{
public function calculate(float $weight): float { return 0.0; }
}
class ShippingCalculator
{
public function __construct(private ShippingStrategyInterface $strategy) {}
public function setStrategy(ShippingStrategyInterface $strategy): void
{
$this->strategy = $strategy;
}
public function calculate(float $weight): float
{
return $this->strategy->calculate($weight);
}
}
$calculator = new ShippingCalculator(new WeightBasedStrategy(3.0));
echo $calculator->calculate(5.0); // 15.0
$calculator->setStrategy(new FreeShippingStrategy());
echo $calculator->calculate(5.0); // 0.0
Practical benefits
- Each strategy is a separate class – test it in isolation
- A new shipping method is a new class, zero changes to existing code (Open/Closed principle)
- The strategy can be injected via DI – easy to swap in tests and configuration
Strategy in Magento 2
Magento 2 uses this pattern in price calculation. The interface Magento\Framework\Pricing\Price\PriceInterface defines the contract, and concrete classes like RegularPrice, SpecialPrice, and TierPrice implement it independently. You can add your own implementation via DI without touching existing classes.
Summary
Strategy works whenever you have a set of interchangeable algorithms doing “the same thing, but differently”. Instead of a growing if-else, you get a set of small, testable classes with a clearly defined contract.
