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