Strategy — swappable algorithms behind an interface
Concept
Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from the clients that use it.
The problem: You're sorting products. Sometimes by price, sometimes by name, sometimes by popularity. Without Strategy: a giant if/switch in the sorting method. With Strategy: each sorting algorithm is a class implementing SortStrategyInterface. The sorter accepts any strategy.
Structure:
- Strategy Interface: The common interface all algorithms implement.
sort(array $items): array. - Concrete Strategies:
PriceSortStrategy,NameSortStrategy,PopularitySortStrategy. - Context: The class that uses the strategy. Holds a reference to a
StrategyInterface. Delegates the work to it.
Strategy vs if/else: Same behavior, different structure. Strategy makes each branch a class (testable, extensible, named). Use Strategy when you have multiple algorithms for the same task and you want to switch them at runtime or test them independently.
Strategy vs Template Method: Strategy uses composition (inject the algorithm). Template Method uses inheritance (override the algorithm in a subclass). Strategy is generally preferred — easier to test and less coupled.
Real-world in Laravel:
- Cache stores (Redis, Memcached, File) as strategies.
- Session handlers.
- Queue drivers.
- Mail transport strategies.
- Payment gateway implementations.
Code Example
<?php
// Strategy Interface
interface SortStrategyInterface
{
public function sort(array $products): array;
}
// Concrete Strategies
class PriceAscendingStrategy implements SortStrategyInterface
{
public function sort(array $products): array
{
usort($products, fn($a, $b) => $a->price <=> $b->price);
return $products;
}
}
class PriceDescendingStrategy implements SortStrategyInterface
{
public function sort(array $products): array
{
usort($products, fn($a, $b) => $b->price <=> $a->price);
return $products;
}
}
class NameStrategy implements SortStrategyInterface
{
public function sort(array $products): array
{
usort($products, fn($a, $b) => strcmp($a->name, $b->name));
return $products;
}
}
class PopularityStrategy implements SortStrategyInterface
{
public function sort(array $products): array
{
usort($products, fn($a, $b) => $b->views <=> $a->views);
return $products;
}
}
// Context
class ProductCatalog
{
private SortStrategyInterface $sortStrategy;
public function __construct(SortStrategyInterface $strategy = null)
{
$this->sortStrategy = $strategy ?? new NameStrategy();
}
public function setSortStrategy(SortStrategyInterface $strategy): void
{
$this->sortStrategy = $strategy;
}
public function getProducts(array $products): array
{
return $this->sortStrategy->sort($products);
}
}
// Runtime strategy selection from user input
class ProductController
{
public function index(Request $request): JsonResponse
{
$products = Product::all()->toArray();
$strategy = match($request->query('sort')) {
'price_asc' => new PriceAscendingStrategy(),
'price_desc' => new PriceDescendingStrategy(),
'popular' => new PopularityStrategy(),
default => new NameStrategy(),
};
$catalog = new ProductCatalog($strategy);
return response()->json($catalog->getProducts($products));
}
}
// Testing strategies in isolation — no HTTP needed
$strategy = new PriceAscendingStrategy();
$sorted = $strategy->sort([
(object)['name' => 'B', 'price' => 30],
(object)['name' => 'A', 'price' => 10],
(object)['name' => 'C', 'price' => 20],
]);
assert($sorted[0]->price === 10); // cheapest first