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

Maintainers

Package info

github.com/zxc7563598/php-trade-splitter

pkg:composer/hejunjie/trade-splitter

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 9

Open Issues: 0

v1.1.0 2026-07-16 07:58 UTC

This package is auto-updated.

Last update: 2026-07-16 08:00:51 UTC


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 StrategyInterface to 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 require is 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. PercentageParticipant vs RecursiveParticipant)
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!