Skip to Content
Course content

80: The Strategy Pattern in PHP

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're planning a trip to the airport. Your goal is simple: get from your front door to the terminal. But how you actually do that depends on a few variables. If you're feeling fancy and have the budget, you call an Uber. If you're trying to save money, you take the bus. If you're in a rush and the traffic is a nightmare, you take the train.

In this scenario, the "goal" remains constant (Airport Arrival), but the "strategy" changes based on the situation. You don't reinvent the concept of travel every time you leave the house; you just swap out the method of transport. That is exactly what the Strategy Pattern does for your code.

The Trap of the Growing Switch Statement

I've seen this a hundred times in legacy codebases. You start with one way of doing something—say, calculating shipping costs via FedEx. You write a simple function. Then, your boss asks for UPS support. You add an if statement. Then comes DHL, then USPS, then some local courier. Before you know it, you have a 200-line switch block that is a nightmare to test and even scarier to modify.

The Strategy Pattern lets us pull those separate algorithms out of that bloated conditional block and put them into their own dedicated classes. Here is how we map the airport analogy to PHP:

  • The Interface (The Goal): This defines what every strategy must be able to do. In our case, "get me to the airport."
  • Concrete Strategies (The Methods): These are the actual implementations—the Uber, the Bus, and the Train.
  • The Context (The Traveler): This is the class that uses the strategy. The traveler doesn't care how the car works; they just know they can call move().

Defining the Shipping Contract

Let's build a shipping calculator. First, we define the interface. This ensures that no matter which shipping provider we add in the future, they all adhere to the same method signature. I like to keep my interfaces lean—one primary method is usually enough.

public interface ShippingStrategy 
{
    public function calculateRate(float $weight): float;
}

Now, we create our concrete strategies. Notice how each class is only responsible for its own specific logic. If FedEx changes its pricing API, you only touch the FedExStrategy class. Everything else stays frozen and safe.

class FedExStrategy implements ShippingStrategy 
{
    public function calculateRate(float $weight): float 
    {
        return $weight * 1.50; // FedEx flat rate per kg
    }
}

class UpsStrategy implements ShippingStrategy 
{
    public function calculateRate(float $weight): float 
    {
        return $weight * 1.20 + 5.00; // UPS base fee + per kg
    }
}

class PostalServiceStrategy implements ShippingStrategy 
{
    public function calculateRate(float $weight): float 
    {
        return $weight * 0.80; // Cheap and slow
    }
}

Connecting the Context to the Strategy

Finally, we need a class to use these strategies. The key here is composition. The ShippingCalculator doesn't hardcode which strategy it uses; instead, it asks for a ShippingStrategy object via the constructor or a setter method. I prefer a setter method here because it allows you to change the shipping method at runtime without recreating the whole calculator object.

class ShippingCalculator 
{
    private ShippingStrategy $strategy;

    public function __construct(ShippingStrategy $strategy) 
    {
        $this->strategy = $strategy;
    }

    public function setStrategy(ShippingStrategy $strategy): void 
    {
        $this->strategy = $strategy;
    }

    public function calculate(float $weight): float 
    {
        return $this->strategy->calculateRate($weight);
    }
}

Now, look how clean the implementation becomes. No if/else, no switch. Just clean, polymorphic behavior:

$weight = 10.5;

// Start with FedEx
$calculator = new ShippingCalculator(new FedExStrategy());
echo "FedEx Cost: " . $calculator->calculate($weight); 

// User changes their mind to UPS in the UI
$calculator->setStrategy(new UpsStrategy());
echo "UPS Cost: " . $calculator->calculate($weight);

By decoupling the "how" (the strategy) from the "when" (the context), your code becomes infinitely more extensible. When a new shipping provider signs on next month, you just create one new class and you're done. You don't even have to open the ShippingCalculator file.




📋 Practical Task

Build a Dynamic E-commerce Discount Engine

You are tasked with building a discount system for an online store. The store has different pricing strategies depending on the current promotion.

Your requirements:

  • Create a DiscountStrategy interface with a method applyDiscount(float $total): float.
  • Implement three concrete strategies:
    • PercentageDiscount: Takes a percentage (e.g., 20%) and deducts it from the total.
    • FlatDiscount: Deducts a fixed amount (e.g., $10) from the total.
    • NoDiscount: Returns the total as is.
  • Create a Checkout class that accepts a DiscountStrategy and has a method calculateFinalPrice(float $amount): float.
  • Instantiate the Checkout class and demonstrate switching from a PercentageDiscount to a FlatDiscount for a cart total of $100.00.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.