Originally published on phpdesignpatterns.dev — part of the PHP design patterns course. View this lesson on the original site.
Strategy in a second domain: shipping
The discount calculator is the textbook case. To stop the pattern feeling like a one-trick, here's a different domain with the same shape.
Before — shipping cost as a switch
<?php
final class ShippingCalculator
{
public function calculateShippingCost(string $method, int $weightGrams, string $country): int
{
if ($method === 'standard' && $country === 'GB') {
return $weightGrams < 2000 ? 295 : 595;
}
if ($method === 'standard' && $country !== 'GB') {
return $weightGrams < 2000 ? 995 : 1995;
}
if ($method === 'next-day') {
if ($country !== 'GB') {
throw new RuntimeException('Next-day not available outside GB');
}
return 695;
}
if ($method === 'collection') {
return 0;
}
throw new RuntimeException("Unknown method: {$method}");
}
}
The same smell as the discount calculator: each method is a different rule, glued together by if/elseif, with the country flag complicating things further.
After — each method is a strategy
<?php
final class ShippingInput
{
public function __construct(
public readonly int $weightGrams,
public readonly string $country,
) {}
}
interface CalculatesShippingCost
{
public function calculateShippingCost(ShippingInput $input): int;
}
final class StandardShipping implements CalculatesShippingCost
{
public function calculateShippingCost(ShippingInput $input): int
{
if ($input->country === 'GB') {
return $input->weightGrams < 2000 ? 295 : 595;
}
return $input->weightGrams < 2000 ? 995 : 1995;
}
}
final class NextDayShipping implements CalculatesShippingCost
{
public function calculateShippingCost(ShippingInput $input): int
{
if ($input->country !== 'GB') {
throw new DomainException('Next-day not available outside GB');
}
return 695;
}
}
final class CollectionShipping implements CalculatesShippingCost
{
public function calculateShippingCost(ShippingInput $input): int
{
return 0;
}
}
What's different from the discount example
- The strategy needs more than one input, so we used a small
ShippingInputvalue object instead of widening the method signature. - One strategy can throw.
NextDayShippingrejects non-GB orders rather than silently returning a default. The contract is honest about what each strategy supports. - The "country" branching has moved inside the strategies that care about it.
CollectionShippingdoesn't know about countries — and shouldn't have to.
The pattern is the same shape; the parameters and the return are tuned to the domain. That's the part teams usually get wrong: copying a Strategy implementation from one place to another verbatim instead of fitting the contract to this domain's question.
From PHP design patterns — Strategy: replacing branching with swappable behaviour