hejunjie / trade-splitter
一个灵活、可扩展的交易/利润分账组件,提供百分比、固定金额、阶梯与递归分账等内置策略,并支持注册自定义策略 | A flexible and scalable transaction/profit sharing component, offering built-in strategies such as percentage, fixed amount, tiered, and recursive sharing, and supporting the registration of custom strategies
Requires
- php: ^8.1
Requires (Dev)
- phpstan/phpstan: ^1.0
- phpunit/phpunit: ^10.0
README
English | 简体中文
A flexible, extensible trade/profit distribution component. Features 8 built-in split strategies covering percentage, fixed amount, weighted, equal, ladder, recursive, ceiling, and priority scenarios — with support for custom strategies.
Perfect for multi-level commissions, e-commerce settlements, platform fees, and agent profit sharing.
This project has been parsed by Zread — click to view an AI-generated summary of the code structure and logic.
Features
- 🧮 8 Built-in Strategies: percentage, fixed, weighted, equal, ladder, recursive, ceiling, and priority — covering virtually all split scenarios
- 🏗️ Typed Participants: each strategy provides a corresponding Participant class with constructor-time validation (fail fast) and IDE autocomplete support
- 🔌 Extensible: implement the
StrategyInterfaceto register custom strategies with the same first-class experience as built-in ones - 📦 Backward Compatible: works with plain arrays for quick calls or typed participant objects — both styles can be mixed
- 🛡️ Strict Validation: overflow rates, excessive amounts, conflicting ladder rules — errors are thrown explicitly rather than silently computing incorrect results
- ⚡ Zero Dependencies: requires only PHP ^8.1; a single
composer requireis all it takes
Installation
composer require hejunjie/trade-splitter
Quick Start
Option 1: Plain Arrays (Quick)
use Hejunjie\TradeSplitter\Splitter; // Percentage split: A gets 60%, B gets 40% $result = Splitter::split(1000, [ ['name' => 'A', 'rate' => 0.6], ['name' => 'B', 'rate' => 0.4], ], 'percentage'); foreach ($result as $allocation) { printf("%s: %.2f (%.2f%%)\n", $allocation->name, $allocation->amount, $allocation->ratio * 100); } // Output: // A: 600.00 (60.00%) // B: 400.00 (40.00%)
Option 2: Typed Participants (Recommended)
use Hejunjie\TradeSplitter\Splitter; use Hejunjie\TradeSplitter\Participants\PercentageParticipant; use Hejunjie\TradeSplitter\Participants\PriorityParticipant; // Percentage split $result = Splitter::split(1000, [ new PercentageParticipant('A', 0.6), new PercentageParticipant('B', 0.4), ], 'percentage'); // Priority split: platform fee → commission → merchant revenue $result = Splitter::split(1000, [ PriorityParticipant::fixed('Platform Fee', 100), PriorityParticipant::rate('Commission', 0.1), PriorityParticipant::residual('Merchant'), ], 'priority');
Typed participants validate parameters at construction time. Combined with IDE autocomplete, they significantly reduce typos and invalid data entering the calculation logic.
Built-in Strategies
| Strategy | Class | Description |
|---|---|---|
percentage |
PercentageStrategy | Proportional split; all rates must sum to 1.0 |
fixed |
FixedStrategy | Fixed-amount split; a participant named "platform" automatically receives the remainder |
weighted |
WeightedStrategy | Weight-based split with automatic normalization — no need to pre-calculate percentages |
equal |
EqualStrategy | Equal split among N parties |
ladder |
LadderStrategy | Tiered rates based on amount thresholds; ideal for sales commission tiers |
recursive |
RecursiveStrategy | Multi-level commission chain; each level takes a cut from the previous level's earnings |
ceiling |
CeilingStrategy | Proportional split with per-participant caps; overflow is redistributed to uncapped participants |
priority |
PriorityStrategy | Sequential deduction in array order; supports fixed, rate, and residual modes |
Percentage Split percentage
All rate values must sum to exactly 1.0.
$result = Splitter::split(1000, [ ['name' => 'Platform', 'rate' => 0.1], ['name' => 'Author', 'rate' => 0.9], ], 'percentage'); // Platform: 100.00, Author: 900.00
Fixed Amount Split fixed
Each participant receives a predefined fixed amount. A participant named platform is handled specially: it does not participate in the fixed allocation and instead receives whatever remains.
$result = Splitter::split(3000, [ ['name' => 'Agent A', 'amount' => 200], ['name' => 'Agent B', 'amount' => 300], ], 'fixed'); // Agent A: 200, Agent B: 300 (remaining 2500 unallocated)
Weighted Split weighted
Amounts are distributed proportionally by weight — normalization is automatic.
$result = Splitter::split(1000, [ ['name' => 'A', 'weight' => 3], ['name' => 'B', 'weight' => 2], ['name' => 'C', 'weight' => 1], ], 'weighted'); // A: 500 (3/6), B: 333.33 (2/6), C: 166.67 (1/6)
Equal Split equal
The total amount is split equally among all participants.
$result = Splitter::split(1000, [ ['name' => 'A'], ['name' => 'B'], ['name' => 'C'], ['name' => 'D'], ], 'equal'); // 250 each
Ladder Split ladder
Matches the total amount against tiered thresholds. Use null for an unbounded top tier (only one allowed).
$result = Splitter::split(5000, [ [ 'name' => 'Agent A', 'ladders' => [ ['max' => 1000, 'rate' => 0.05], ['max' => 5000, 'rate' => 0.10], ['max' => null, 'rate' => 0.15], ], ], ['name' => 'Platform', 'rate' => 0.05], ], 'ladder'); // 5000 hits the second tier: Agent A = 5000 × 10% = 500, Platform = 5000 × 5% = 250
Recursive Split recursive
Multi-level commission chain — each level takes a percentage of the previous level's earnings.
$result = Splitter::split(10000, [ ['name' => 'Level 1', 'rate' => 0.2], ['name' => 'Level 2', 'rate' => 0.2], ['name' => 'Level 3', 'rate' => 0.2], ], 'recursive'); // Level 1 net: 1600, Level 2 net: 320, Level 3 net: 80
Ceiling Split ceiling
Proportional split with optional per-participant caps (max). Overflow from capped participants is redistributed among uncapped ones.
$result = Splitter::split(1000, [ ['name' => 'A', 'rate' => 0.5, 'max' => 300], // capped at 300 ['name' => 'B', 'rate' => 0.5, 'max' => null], // uncapped, receives overflow ], 'ceiling'); // A: 300 (cap reached), B: 700 (includes A's overflow of 200)
Priority Split priority
Participants are processed in array order, each deducting from the remaining pool. First in line gets served first.
$result = Splitter::split(1000, [ ['name' => 'Gateway Fee', 'fixed' => 5], ['name' => 'Commission', 'rate' => 0.05], ['name' => 'Merchant', 'residual' => true], ], 'priority'); // Gateway Fee: 5, Commission: 49.75, Merchant: 945.25
Typed Participants
Each strategy has a corresponding Participant class. Recommended for production use. Compared to plain arrays, typed participants offer:
- Constructor-time validation: invalid parameters throw immediately, not deep inside the calculation
- IDE-friendly: autocomplete for property names and factory methods
- Clear semantics: different strategy participants have distinct types, preventing mix-ups (e.g.
PercentageParticipantvsRecursiveParticipant)
| Strategy | Participant Class | Key Parameters |
|---|---|---|
| percentage | PercentageParticipant |
name, rate |
| fixed | FixedParticipant |
name, amount |
| weighted | WeightedParticipant |
name, weight |
| equal | EqualParticipant |
name |
| ladder | LadderParticipant |
name, rate or ladders |
| recursive | RecursiveParticipant |
name, rate |
| ceiling | CeilingParticipant |
name, rate, max (optional) |
| priority | PriorityParticipant |
fixed() / rate() / residual() factory methods |
use Hejunjie\TradeSplitter\Participants\LadderParticipant; use Hejunjie\TradeSplitter\Participants\Ladder; use Hejunjie\TradeSplitter\Participants\PriorityParticipant; // Ladder participant (ladder mode + fixed-rate mode mixed) Splitter::split(5000, [ new LadderParticipant('Agent A', ladders: [ new Ladder(1000, 0.05), new Ladder(5000, 0.10), new Ladder(null, 0.15), ]), new LadderParticipant('Platform', rate: 0.05), ], 'ladder'); // Priority participant (all three modes combined) Splitter::split(1000, [ PriorityParticipant::fixed('Gateway Fee', 5), PriorityParticipant::rate('Commission', 0.05), PriorityParticipant::residual('Merchant'), ], 'priority');
Custom Strategy
Implement StrategyInterface and register it with registerStrategy():
use Hejunjie\TradeSplitter\Contracts\StrategyInterface; use Hejunjie\TradeSplitter\Models\SplitContext; use Hejunjie\TradeSplitter\Models\Allocation; use Hejunjie\TradeSplitter\Splitter; class MyStrategy implements StrategyInterface { public function split(SplitContext $context): array { // $context->total — total amount to split // $context->participants — participant configuration array return [ new Allocation('someone', $context->total, 1.0), ]; } } Splitter::registerStrategy('my_strategy', MyStrategy::class); $result = Splitter::split(1000, [], 'my_strategy');
Tip
Use Splitter::availableStrategies() to list all currently registered strategy names.
Core Concepts
| Concept | Description |
|---|---|
| Splitter | Main entry point — manages strategy registration and dispatching |
| StrategyInterface | Strategy contract; all strategies must implement split(SplitContext): Allocation[] |
| SplitContext | Split context containing total (amount to split) and participants (participant configuration) |
| Allocation | Split result with name, amount, and ratio; provides toArray() and getter methods |
| Participant | Abstract base class for typed participants; subclasses validate fields on construction and bridge to the strategy layer via toArray() |
Directory Structure
src/
├── Contracts/
│ ├── Participant.php # Abstract participant base class
│ └── StrategyInterface.php # Strategy interface
├── Exceptions/
│ └── SplitException.php # Unified exception class
├── Models/
│ ├── Allocation.php # Split result object
│ └── SplitContext.php # Split context
├── Participants/ # Typed participants (recommended)
│ ├── PercentageParticipant.php
│ ├── FixedParticipant.php
│ ├── WeightedParticipant.php
│ ├── EqualParticipant.php
│ ├── LadderParticipant.php
│ ├── Ladder.php # Ladder rule value object
│ ├── RecursiveParticipant.php
│ ├── CeilingParticipant.php
│ └── PriorityParticipant.php
├── Strategies/ # Strategy implementations
│ ├── PercentageStrategy.php
│ ├── FixedStrategy.php
│ ├── WeightedStrategy.php
│ ├── EqualStrategy.php
│ ├── LadderStrategy.php
│ ├── RecursiveStrategy.php
│ ├── CeilingStrategy.php
│ └── PriorityStrategy.php
└── Splitter.php # Split dispatcher
Run the Demo
php tests/demo.php
Motivation
This component was born from frustration with rigid, hard-coded profit-sharing logic across various projects. The goal was a clear, pluggable, and reusable split component that could be integrated into any project — without reinventing the wheel.
If you've encountered unusual or complex split scenarios in your own work, feel free to collaborate and make this tool better.
Contributing
Questions, suggestions, or bugs? PRs and Issues are always welcome.
If you find this project helpful, please give it a ⭐ Star — that's the biggest motivation to keep improving it!