Strategy Pattern: Interchangeable Algorithms in Magento
· Reading time: approx. 13 minutes · Category: Magento 2 · Design Patterns
Strategy Pattern:
Interchangeable Algorithms
Price calculation, shipping costs, taxes, payment methods: Magento uses the Strategy Pattern everywhere. How it is implemented, how di.xml swaps algorithms, and how you integrate your own strategies.
When Algorithms Need to Be Interchangeable
How does Magento calculate the price of a product? That depends: does the customer belong to a customer group with a discount? Is there an active price rule? Is the customer in a tax collective? Which currency is active? The answer is different every time, and that is exactly what the Strategy Pattern is ideal for.
The Strategy Pattern (GoF, 1994) defines a family of algorithms, encapsulates each one individually, and makes them interchangeable. The context (the calling class) only knows the interface, the configuration decides which concrete implementation is executed.
Magento uses this pattern extensively: shipping cost calculation (CarrierInterface), payment methods (MethodInterface), tax calculations, price calculators, and many other interchangeable algorithms are all Strategy implementations.
- 1. The GoF Strategy Pattern
- 2. Strategy Pattern in the Magento Core: Overview
- 3. Shipping Costs: CarrierInterface as a Strategy
- 4. Price Calculation: PriceInterface Strategies
- 5. Implementing Your Own Strategy
- 6. di.xml: Configuring and Swapping Strategies
- 7. Strategy Chain: Combining Multiple Strategies
- 8. Testing the Strategy Pattern
- 9. Strategy vs. Plugin: Which Pattern When?
- 10. Summary
- 11. FAQ
1. The GoF Strategy Pattern
The classic Strategy Pattern consists of three parts: the strategy interface, concrete implementations, and a context that uses a strategy:
<?php
// Classic Strategy Pattern:
// 1. Strategy interface
interface SortStrategyInterface
{
public function sort(array $data): array;
}
// 2. Concrete strategies
class QuickSort implements SortStrategyInterface
{
public function sort(array $data): array { /* QuickSort */ return $data; }
}
class MergeSort implements SortStrategyInterface
{
public function sort(array $data): array { /* MergeSort */ return $data; }
}
class BubbleSort implements SortStrategyInterface
{
public function sort(array $data): array { /* BubbleSort */ return $data; }
}
// 3. Context: knows only the interface
class DataSorter
{
public function __construct(
private readonly SortStrategyInterface $strategy // Interchangeable!
) {}
public function sort(array $data): array
{
return $this->strategy->sort($data); // Which implementation? Doesn't matter!
}
}
// Configuration decides which strategy:
$sorter = new DataSorter(new QuickSort());
$sorter = new DataSorter(new MergeSort()); // No code change in the context!
2. Strategy Pattern in the Magento Core: Overview
Strategy Pattern in the Magento Core:
Shipping costs:
Interface: Magento\Shipping\Model\Carrier\AbstractCarrier
Strategies: FlatRate, FreeShipping, TableRate, UPS, DHL, FedEx
Context: Magento\Shipping\Model\Shipping
Payment methods:
Interface: Magento\Payment\Model\Method\AbstractMethod
(+ Magento\Payment\Gateway\CommandInterface)
Strategies: CashOnDelivery, BankTransfer, Stripe, PayPal, Klarna
Context: Magento\Payment\Helper\Data
Price calculators:
Interface: Magento\Catalog\Pricing\Price\PriceInterface
Strategies: RegularPrice, SpecialPrice, GroupPrice, TierPrice
Context: Magento\Framework\Pricing\Amount\AmountFactory
Tax calculation:
Interface: Magento\Tax\Model\Calculation\AbstractAggregateCalculator
Strategies: UnitBaseCalculation, RowBaseCalculation, TotalBaseCalculation
Context: Magento\Tax\Model\Calculation
URL generation:
Interface: Magento\Catalog\Model\ResourceModel\Url
Strategies: different URL builders depending on product type
Product types:
Interface: Magento\Catalog\Model\Product\Type\AbstractType
Strategies: Simple, Configurable, Bundle, Virtual, Downloadable
3. Shipping Costs: CarrierInterface as a Strategy
The clearest example: each shipping method is its own independent strategy. They share the same interface but have completely different calculation logic:
<?php
// Magento\Shipping\Model\CarrierInterface (simplified):
interface CarrierInterface
{
/**
* Collect and get rates.
* Each carrier implements its own rate-calculation algorithm.
*/
public function collectRates(RateRequest $request): ?Result;
/**
* Returns allowed shipping methods for this carrier.
*/
public function getAllowedMethods(): array;
}
// Concrete strategies:
class Flatrate extends AbstractCarrier implements CarrierInterface
{
public function collectRates(RateRequest $request): ?Result
{
// Always the same price from configuration
$price = $this->getConfigData('price');
$method = $this->_rateMethodFactory->create();
$method->setPrice($price);
// ...
return $result;
}
}
class Tablerate extends AbstractCarrier implements CarrierInterface
{
public function collectRates(RateRequest $request): ?Result
{
// Price from a table based on weight/price/destination
$rate = $this->_tablerateFactory->create()
->getRate($request);
// ...
return $result;
}
}
// Context: the Shipping class knows only the interface
class Shipping
{
public function collectCarrierRates(string $carrierCode, RateRequest $request): ?Result
{
$carrier = $this->_carrierFactory->create($carrierCode);
if (!$carrier) {
return null;
}
// Calls collectRates(), which implementation? Doesn't matter!
return $carrier->collectRates($request);
}
}
4. Price Calculation: PriceInterface Strategies
<?php
// Magento\Framework\Pricing\Price\PriceInterface:
interface PriceInterface
{
/**
* Get price value: each price type has its own calculation.
*/
public function getValue(): float|false;
/**
* Get the price amount object with adjustments.
*/
public function getAmount(): AmountInterface;
}
// Price strategies:
class RegularPrice implements PriceInterface
{
public function getValue(): float|false
{
return $this->product->getPrice(); // Direct catalog price
}
}
class SpecialPrice implements PriceInterface
{
public function getValue(): float|false
{
$specialPrice = $this->product->getSpecialPrice();
if ($specialPrice === null) {
return false;
}
// Checks validity (from/to date)
if ($this->isDateInRange($this->product->getSpecialFromDate(), $this->product->getSpecialToDate())) {
return (float) $specialPrice;
}
return false;
}
}
class TierPrice implements PriceInterface
{
public function getValue(): float|false
{
// Lowest tier price for the current customer group
$tierPrices = $this->product->getTierPrices();
// ... calculation based on quantity and customer group
return $lowestTierPrice;
}
}
5. Implementing Your Own Strategy
A concrete example: your own discount strategy for different customer groups:
<?php
declare(strict_types=1);
namespace Mironsoft\Pricing\Model\Discount;
// 1. Define the strategy interface
interface DiscountStrategyInterface
{
/**
* Calculate discount amount for a given price and context.
*/
public function calculate(float $price, DiscountContext $context): float;
/**
* Returns true if this strategy applies for the given context.
*/
public function isApplicable(DiscountContext $context): bool;
}
// 2. Concrete context (value object)
class DiscountContext
{
public function __construct(
public readonly int $customerGroupId,
public readonly float $cartTotal,
public readonly \DateTimeImmutable $orderDate
) {}
}
// 3. Strategy A: customer group discount
class CustomerGroupDiscount implements DiscountStrategyInterface
{
private const VIP_GROUP_ID = 4;
private const VIP_DISCOUNT = 0.15; // 15%
public function calculate(float $price, DiscountContext $context): float
{
return $price * self::VIP_DISCOUNT;
}
public function isApplicable(DiscountContext $context): bool
{
return $context->customerGroupId === self::VIP_GROUP_ID;
}
}
// 4. Strategy B: cart value discount
class CartValueDiscount implements DiscountStrategyInterface
{
public function calculate(float $price, DiscountContext $context): float
{
return match(true) {
$context->cartTotal >= 500.0 => $price * 0.10,
$context->cartTotal >= 200.0 => $price * 0.05,
default => 0.0,
};
}
public function isApplicable(DiscountContext $context): bool
{
return $context->cartTotal >= 200.0;
}
}
// 5. Context class: knows only the interface
class DiscountCalculator
{
public function __construct(
private readonly DiscountStrategyInterface $strategy // Interchangeable!
) {}
public function getDiscount(float $price, DiscountContext $context): float
{
if (!$this->strategy->isApplicable($context)) {
return 0.0;
}
return $this->strategy->calculate($price, $context);
}
}
6. di.xml: Configuring and Swapping Strategies
The strength of the Strategy Pattern in Magento: the strategy is configured via di.xml. No code change to the context is needed, only a configuration change:
<!-- app/code/Mironsoft/Pricing/etc/di.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<!-- Default strategy: CustomerGroupDiscount -->
<preference for="Mironsoft\Pricing\Model\Discount\DiscountStrategyInterface"
type="Mironsoft\Pricing\Model\Discount\CustomerGroupDiscount"/>
<!-- Alternative: CartValueDiscount instead of CustomerGroupDiscount -->
<!-- Uncomment to swap: -->
<!--
<preference for="Mironsoft\Pricing\Model\Discount\DiscountStrategyInterface"
type="Mironsoft\Pricing\Model\Discount\CartValueDiscount"/>
-->
<!-- Different strategy for a specific class (Virtual Type Pattern): -->
<virtualType name="Mironsoft\Pricing\Model\VipDiscountCalculator"
type="Mironsoft\Pricing\Model\Discount\DiscountCalculator">
<arguments>
<argument name="strategy" xsi:type="object">
Mironsoft\Pricing\Model\Discount\CustomerGroupDiscount
</argument>
</arguments>
</virtualType>
<virtualType name="Mironsoft\Pricing\Model\CartDiscountCalculator"
type="Mironsoft\Pricing\Model\Discount\DiscountCalculator">
<arguments>
<argument name="strategy" xsi:type="object">
Mironsoft\Pricing\Model\Discount\CartValueDiscount
</argument>
</arguments>
</virtualType>
</config>
Virtual Types: With Virtual Types, the same context (DiscountCalculator) can exist multiple times with different strategies, without having to write new PHP classes. VipDiscountCalculator and CartDiscountCalculator are the same class, just injected with a different strategy.
7. Strategy Chain: Combining Multiple Strategies
<?php
declare(strict_types=1);
namespace Mironsoft\Pricing\Model\Discount;
/**
* Composite strategy: tries multiple strategies, applies the first that matches.
* Implements the Chain of Responsibility variant of Strategy Pattern.
*/
class CompositeDiscountStrategy implements DiscountStrategyInterface
{
/**
* @param DiscountStrategyInterface[] $strategies Ordered list of strategies
*/
public function __construct(
private readonly array $strategies = []
) {}
public function calculate(float $price, DiscountContext $context): float
{
foreach ($this->strategies as $strategy) {
if ($strategy->isApplicable($context)) {
return $strategy->calculate($price, $context);
}
}
return 0.0;
}
public function isApplicable(DiscountContext $context): bool
{
foreach ($this->strategies as $strategy) {
if ($strategy->isApplicable($context)) {
return true;
}
}
return false;
}
}
<!-- Configure the composite strategy via di.xml: -->
<type name="Mironsoft\Pricing\Model\Discount\CompositeDiscountStrategy">
<arguments>
<argument name="strategies" xsi:type="array">
<!-- sortOrder determines priority (VIP first, then cart value) -->
<item name="vip" xsi:type="object" sortOrder="10">
Mironsoft\Pricing\Model\Discount\CustomerGroupDiscount
</item>
<item name="cart" xsi:type="object" sortOrder="20">
Mironsoft\Pricing\Model\Discount\CartValueDiscount
</item>
</argument>
</arguments>
</type>
8. Testing the Strategy Pattern
<?php
declare(strict_types=1);
use Mironsoft\Pricing\Model\Discount\CustomerGroupDiscount;
use Mironsoft\Pricing\Model\Discount\DiscountContext;
use Mironsoft\Pricing\Model\Discount\DiscountCalculator;
class CustomerGroupDiscountTest extends \PHPUnit\Framework\TestCase
{
private CustomerGroupDiscount $strategy;
protected function setUp(): void
{
$this->strategy = new CustomerGroupDiscount();
}
public function testCalculateVipDiscount(): void
{
$context = new DiscountContext(
customerGroupId: 4, // VIP group
cartTotal: 100.0,
orderDate: new \DateTimeImmutable()
);
$discount = $this->strategy->calculate(100.0, $context);
$this->assertSame(15.0, $discount); // 15% of 100 = 15
}
public function testNotApplicableForNonVip(): void
{
$context = new DiscountContext(customerGroupId: 1, cartTotal: 0.0, orderDate: new \DateTimeImmutable());
$this->assertFalse($this->strategy->isApplicable($context));
}
}
class DiscountCalculatorTest extends \PHPUnit\Framework\TestCase
{
public function testUsesStrategy(): void
{
$context = new DiscountContext(customerGroupId: 4, cartTotal: 100.0, orderDate: new \DateTimeImmutable());
$strategy = $this->createMock(\Mironsoft\Pricing\Model\Discount\DiscountStrategyInterface::class);
$strategy->method('isApplicable')->with($context)->willReturn(true);
$strategy->method('calculate')->with(100.0, $context)->willReturn(20.0);
$calculator = new DiscountCalculator($strategy);
$this->assertSame(20.0, $calculator->getDiscount(100.0, $context));
}
}
9. Strategy vs. Plugin: Which Pattern When?
| Criterion | Strategy Pattern | Plugin (Interceptor) |
|---|---|---|
| Purpose | Replace the entire algorithm | Extend an existing method before/after/around |
| Configuration | di.xml preference or argument | di.xml plugin element |
| Multiple at once | Only one (or a composite) | Several with sortOrder |
| Original code | Completely replaced | Preserved (before/after) or optional (around) |
| Use case | Custom business logic, extensibility points | Modify the behavior of core classes |
Mironsoft
Magento 2 Architecture & Development
Want to develop your own strategies for your Magento store?
We implement interchangeable algorithms for price calculation, shipping costs, tax rules and more, using the Strategy Pattern, full test coverage and di.xml configuration.
10. Summary
The Strategy Pattern is the most commonly used design pattern in Magento 2. It enables interchangeable algorithms for price calculation, shipping costs, payment methods and more. The interface defines the contract, di.xml decides which implementation is active, and the calling context knows no concrete classes.
Strategy Pattern in Magento, Overview
Interface
Defines the algorithm contract. The context knows only the interface. All strategies implement the same interface.
di.xml configuration
A preference or argument selects the concrete implementation. Swapping means a configuration change, no code change in the context.
Virtual Types
The same context with different strategies as different virtual classes, no new PHP classes needed.
Composite strategy
Combine multiple strategies in a composite strategy. Order is configurable via sortOrder in di.xml.
11. FAQ: Strategy Pattern in Magento 2
1 Main advantage of the Strategy Pattern in Magento?
2 How do I swap a standard strategy?
di.xml preference: <preference for="..." type="MyImplementation"/>. Active after setup:di:compile. No code change needed in the context.