Designing a Flexible Discount Calculation Engine in PHP

In e-commerce systems, handling various promotional rules—such as bulk discounts, percentage reductions, VIP-exclusive deals, buy-one-get-one offers, and tiered pricing—can quickly become complex. While each rule may be straightforward individually, combining them often leads to tangled logic that's hard to maintain.

I've encountered this problem repeatedly in real projects. My initial approach usually involved chains of if/else statements and switch blocks, wich became unmaintainable over time. To solve this, I developed a reusable library called php-promotion-engine, designed to encapsulate common discount calculation patterns into an organized and extensible system.

Why Build This Library?

Let’s consider a simple scenario:

  • A cart qualifeis for a "Spend ¥200, get ¥50 off" promotion
  • The user is a VIP eligible for a 10% discount
  • Certain items qualify for a "Buy 3, save ¥20" deal

How should these be applied together? Should all discounts apply independently? Or should one modify the base before another kicks in? Can a single item receive multiple discounts?

To avoid reinventing the wheel every time, I abstracted core promosional logic into a package that can be reused across different projects.

Three Core Discount Application Modes

1. Independent Mode

In this mode, each rule calculates its discount based on the original item price. All resulting discounts are summed up at the end.

  • Each rule operates in isolation
  • Total savings tend to be higher since no adjustments affect other calculations
  • Suitable when promotions don't interact (e.g., separate store-wide and brand-specific coupons)

Example:

// Original total: ¥300
// Rule 1: Spend ¥200, get ¥50 off => ¥50 saved
// Rule 2: VIP 10% off => 10% * 300 = ¥30 saved
// Final total: 300 - (50 + 30) = ¥220

2. Sequential Mode

This applies rules one after another, where subsequent rules use updated prices from prior ones.

  • Discounts cascade; later rules see already-adjusted values
  • Can lead to lower overall savings due to compounding effects
  • Mirrors many real-world e-commerce flows like applying a coupon followed by a membership discount

Example:

// Original total: ¥300
// Step 1: Apply full reduction => ¥50 off, new subtotal = ¥250
// Step 2: Apply VIP discount => 10% * 250 = ¥25 off
// Final total: 300 - (50 + 25) = ¥225

3. Locking Mode

In locking mode, once an item has been processed under one rule, it becomes locked and cannot benefit from further promotions.

  • Ensures exclusivity—for instance, sale items might not accept additional member discounts
  • Gives more control to marketing teams who want clear boundaries between offers

Example:

// Item A gets special flash-sale pricing => locked
// Item B receives standard discount
// Even if Item A would qualify for another offer, it remains unaffected

By categorizing most promotional scenarios into these three modes, we gain flexibility without sacrificing clarity or consistency.

Usage Example

You can install the engine via Composer:

composer require hejunjie/promotion-engine

Here’s how you’d set up a basic usage example:

<?php

use Hejunjie\PromotionEngine\PromotionEngine;
use Hejunjie\PromotionEngine\Rules\FullReductionRule;
use Hejunjie\PromotionEngine\Rules\VipDiscountRule;
use Hejunjie\PromotionEngine\Models\Cart;
use Hejunjie\PromotionEngine\Models\User;

// Define shopping cart contents
$cart = new Cart();
$cart->addItem('T-shirt', 120, 1, ['basic']);
$cart->addItem('Jeans', 150, 1, ['basic', 'special']);

// Set user status
$user = new User(vip: true);

// Initialize engine
$engine = new PromotionEngine();
$engine->setMode('sequential');

// Add rules with execution order and filtering tags
$engine->addRule(new FullReductionRule(threshold: 100, amount: 30, tags: ['special'], priority: 1));
$engine->addRule(new VipDiscountRule(rate: 0.9, tags: ['basic'], priority: 2));

// Perform calculation
$result = $engine->calculate($cart, $user);

echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

This produces output similar to:

{
    "original": 270,
    "discount": 54,
    "final": 216,
    "details": [
        "Spend ¥100, get ¥30 off (-¥30)",
        "VIP 10% discount (-¥24)"
    ],
    "items": [
        {
            "name": "T-shirt",
            "price": 108,
            "qty": 1,
            "tags": ["basic"],
            "original_price": 120,
            "locked": false
        },
        {
            "name": "Jeans",
            "price": 108,
            "qty": 1,
            "tags": ["basic", "special"],
            "original_price": 150,
            "locked": false
        }
    ]
}

Benefits of Using This Approach

  • Cleaner Codebase: Eliminates messy conditional logic by centralizing business rules
  • Flexible Discount Models: Easily switch between independent, sequential, or locking behavior
  • Ease of Extension: New types of promotions can be implemented by extending rule classes rather than modifying existing logic

This isn’t meant to be a comprehensive framework—it’s a lightweight tool built out of practical experience solving recurring problems in e-commerce applications. You're welcome to try it out, contribute improvements, or adapt it for your own needs.

Repository link: https://github.com/zxc7563598/php-promotion-engine

Tags: PHP E-commerce promotions discount-engine composer-package

Posted on Tue, 20 Oct 2026 16:33:24 +0000 by greens85