bermudaphp/money

Precision money and currency handling library for PHP with support for arithmetic operations, currency conversion, and formatting

v1.0 2025-05-09 15:21 UTC

This package is auto-updated.

Last update: 2025-05-09 16:31:39 UTC


README

PHP Version License Ask DeepWiki

A comprehensive PHP library for handling monetary values and currency operations with precision. This library provides immutable objects for reliable money handling, precise decimal calculations, and robust currency conversion functionality.

Русская версия документации

Table of Contents

Features

  • Immutable Value Objects: All classes follow immutable design principles, preventing accidental state changes and ensuring thread safety.
  • Decimal Precision: Uses BCMath for precise calculations without floating-point errors that commonly plague monetary calculations.
  • ISO 4217 Support: Complete implementation of currency codes with associated metadata like symbols and decimal places.
  • Comprehensive Comparison: Compare money objects even across different currencies when exchange rates are available.
  • Currency Conversion: Convert between currencies with customizable exchange rates and support for both direct and indirect conversions.
  • Commission Handling: Support for conversion fees and commissions in currency conversions.
  • Type Safety: Strict typing throughout the library to catch errors at compile time rather than runtime.
  • Allocation & Distribution: Built-in methods for splitting money amounts according to ratios or equal parts.
  • Formatting: Format money values according to locale-specific conventions.
  • Path Finding Algorithm: Automatically finds conversion paths between currencies when direct rates are not available.

Installation

Install the library via Composer:

composer require bermudaphp/money

Requirements

  • PHP 8.4 or higher
  • BCMath PHP Extension
  • Intl PHP Extension (recommended for locale-aware formatting)

Components

CurrencyCode Enum

An enumeration of ISO 4217 currency codes with additional metadata and utility methods.

Key Methods:

  • CurrencyCode::isValid(string $code): Check if a string is a valid currency code
  • CurrencyCode::getAllCodes(): Get all currency codes as an array
  • CurrencyCode->getName(): Get the full name of the currency
  • CurrencyCode->getSymbol(): Get the symbol for the currency
  • CurrencyCode->getDecimals(): Get number of decimal places commonly used

Money Class

An immutable representation of monetary values with precise decimal calculation. This class prevents common floating-point precision errors when dealing with currency.

Key Properties:

  • $amount: The amount in the smallest currency unit (e.g., cents for USD)
  • $currency: The currency code enum
  • $scale: Number of decimal places for this currency
  • $decimal: Decimal value as a string (for precision)

Creation Methods:

  • new Money(int $amount, CurrencyCode $currency): Create from integer amount in smallest unit
  • Money::fromDecimal(float|string $value, CurrencyCode $currency): Create from decimal value
  • Money::fromString(string $moneyString): Create from string like "10.99 USD"
  • Money::zero(CurrencyCode $currency): Create a zero money object
  • Money::one(CurrencyCode $currency): Create a money object with value of one unit

Arithmetic Methods:

  • add(Money $other): Add another Money object
  • subtract(Money $other): Subtract another Money object
  • multiply(number $factor): Multiply by a factor
  • divide(number $divisor): Divide by a divisor
  • increment(int $amount = 1): Increment by smallest units
  • decrement(int $amount = 1): Decrement by smallest units
  • absolute(): Get absolute value
  • negate(): Get negated value

Comparison Methods:

  • equals(Money|array $other, string $mode = COMPARE_ANY): Check equality
  • greaterThan(Money|array $other, string $mode = COMPARE_ALL): Check if greater than
  • greaterThanOrEqual(Money|array $other, string $mode = COMPARE_ALL): Check if greater than or equal
  • lessThan(Money|array $other, string $mode = COMPARE_ALL): Check if less than
  • lessThanOrEqual(Money|array $other, string $mode = COMPARE_ALL): Check if less than or equal
  • between(Money $min, Money $max, bool $inclusive = true): Check if between two values
  • isZero(): Check if amount is zero
  • isPositive(): Check if amount is positive
  • isNegative(): Check if amount is negative

Distribution Methods:

  • allocate(array $ratios): Allocate amount according to ratios
  • split(int $n): Split amount equally into n parts

Other Methods:

  • format(?string $locale = null, bool $includeSymbol = true): Format according to locale
  • convertTo(CurrencyCode $targetCurrency, $exchangeRateOrConverter): Convert to another currency

CurrencyConverter Class

An immutable currency conversion utility that supports direct exchange rates and conversion through a path discovery algorithm.

Key Properties:

  • $baseCurrency: Base currency for indirect conversions
  • $directRates: Direct exchange rates between currency pairs
  • $commissionRates: Commission rates for currency pairs
  • $defaultCommissionRate: Default commission rate
  • $scale: Precision for calculations

Creation and Configuration:

  • new CurrencyConverter(CurrencyCode $baseCurrency = CurrencyCode::USD): Create a converter
  • CurrencyConverter::fromArray(array $data): Create from configuration array
  • toArray(): Export configuration to array
  • withBaseCurrency(CurrencyCode $currency): Create new instance with different base currency
  • withBaseRate(CurrencyCode $currency, $rate): Add base exchange rate
  • withBaseRates(array $rates): Add multiple base exchange rates in single operation
  • withDirectRate(CurrencyCode $from, CurrencyCode $to, $rate): Add direct exchange rate
  • withDirectRates(CurrencyCode $from, array $rates): Add multiple direct rates in single operation
  • withCommissionRate(CurrencyCode $from, CurrencyCode $to, $rate): Add commission rate
  • withDefaultCommissionRate($rate): Set default commission rate
  • withScale(int $scale): Set calculation precision

Conversion Methods:

  • convert($amount, CurrencyCode $from, CurrencyCode $to, bool $includeCommission = true): Convert an amount
  • convertMoney(Money $money, CurrencyCode $targetCurrency): Convert a Money object
  • calculateCommission($amount, CurrencyCode $from, CurrencyCode $to): Calculate commission for a conversion

Exchange Rate Methods:

  • getExchangeRate(CurrencyCode $from, CurrencyCode $to): Get exchange rate between currencies
  • getDirectRate(CurrencyCode $from, CurrencyCode $to): Get direct exchange rate if available
  • getBaseRate(CurrencyCode $currency): Get base exchange rate if available
  • getCommissionRate(CurrencyCode $from, CurrencyCode $to): Get commission rate

Usage Examples

Working with Currency Codes

use Bermuda\Stdlib\CurrencyCode;

// Access currency code
$usd = CurrencyCode::USD;
$eur = CurrencyCode::EUR;

// Get metadata
echo $usd->getName(); // "United States Dollar"
echo $usd->getSymbol(); // "$"
echo $usd->getDecimals(); // 2

// Check if a code is valid
$isValid = CurrencyCode::isValid('USD'); // true
$isValid = CurrencyCode::isValid('XYZ'); // false

// Get all currency codes
$allCodes = CurrencyCode::getAllCodes(); // Returns array of all codes

Creating Money Objects

use Bermuda\Stdlib\Money;
use Bermuda\Stdlib\CurrencyCode;

// From amount in smallest unit (cents)
$tenDollars = new Money(1000, CurrencyCode::USD); // $10.00

// From decimal
$tenEuros = Money::fromDecimal(10.00, CurrencyCode::EUR);
$tenEuros = Money::fromDecimal('10.00', CurrencyCode::EUR); // String input for precision

// From string
$tenPounds = Money::fromString('10.00 GBP');

// Zero and one
$zeroYen = Money::zero(CurrencyCode::JPY);
$oneYen = Money::one(CurrencyCode::JPY);

// Custom scale (decimal places)
$highPrecisionDollars = new Money(1000, CurrencyCode::USD, 4); // $0.1000

Money Arithmetic

use Bermuda\Stdlib\Money;
use Bermuda\Stdlib\CurrencyCode;

$fiveDollars = Money::fromDecimal(5, CurrencyCode::USD);
$tenDollars = Money::fromDecimal(10, CurrencyCode::USD);

// Addition
$fifteenDollars = $fiveDollars->add($tenDollars);

// Subtraction
$fiveDollars = $tenDollars->subtract($fiveDollars);

// Multiplication
$twentyDollars = $tenDollars->multiply(2);
$twentyDollars = $tenDollars->multiply('2.0'); // String input for precision

// Division
$twoDollars = $tenDollars->divide(5);

// Increment/Decrement (smallest unit)
$tenDollarsAndOneCent = $tenDollars->increment();
$tenDollarsMinusOneCent = $tenDollars->decrement();
$tenDollarsAndFiveCents = $tenDollars->increment(5);

// Absolute and negation
$minusTenDollars = $tenDollars->negate();
$tenDollarsAgain = $minusTenDollars->absolute();

// Chaining is possible because of immutability
$result = Money::fromDecimal(10, CurrencyCode::USD)
    ->add(Money::fromDecimal(5, CurrencyCode::USD))
    ->multiply(2)
    ->divide(3);

Money Comparison

use Bermuda\Stdlib\Money;
use Bermuda\Stdlib\CurrencyCode;
use Bermuda\Stdlib\CurrencyConverter;

$fiveDollars = Money::fromDecimal(5, CurrencyCode::USD);
$tenDollars = Money::fromDecimal(10, CurrencyCode::USD);
$twentyDollars = Money::fromDecimal(20, CurrencyCode::USD);

// Basic comparison
$isEqual = $fiveDollars->equals($fiveDollars); // true
$isGreater = $tenDollars->greaterThan($fiveDollars); // true
$isLess = $fiveDollars->lessThan($tenDollars); // true
$isGreaterOrEqual = $tenDollars->greaterThanOrEqual($tenDollars); // true
$isLessOrEqual = $fiveDollars->lessThanOrEqual($tenDollars); // true

// Between
$isBetween = $tenDollars->between($fiveDollars, $twentyDollars); // true

// State checks
$isZero = $fiveDollars->isZero(); // false
$isPositive = $fiveDollars->isPositive(); // true
$isNegative = $fiveDollars->isNegative(); // false

// Cross-currency comparison (requires converter)
$converter = new CurrencyConverter();
$converter = $converter->withBaseRate(CurrencyCode::EUR, '0.85');

$tenEuros = Money::fromDecimal(10, CurrencyCode::EUR)->withConverter($converter);
$tenDollars = Money::fromDecimal(10, CurrencyCode::USD)->withConverter($converter);

$isEqual = $tenDollars->equals($tenEuros); // false
$isGreater = $tenDollars->greaterThan($tenEuros); // false (10 USD < 10 EUR if 1 USD = 0.85 EUR)

// Array comparison
$moneyArray = [
    Money::fromDecimal(5, CurrencyCode::USD),
    Money::fromDecimal(10, CurrencyCode::USD),
    Money::fromDecimal(15, CurrencyCode::USD)
];

// Check if $twentyDollars is greater than ALL amounts in array
$isGreaterThanAll = $twentyDollars->greaterThan($moneyArray, Money::COMPARE_ALL); // true

// Check if $fiveDollars is less than ANY amount in array
$isLessThanAny = $fiveDollars->lessThan($moneyArray, Money::COMPARE_ANY); // true

// Check if $tenDollars equals ANY amount in array
$equalsAny = $tenDollars->equals($moneyArray, Money::COMPARE_ANY); // true

Currency Conversion

use Bermuda\Stdlib\Money;
use Bermuda\Stdlib\CurrencyCode;
use Bermuda\Stdlib\CurrencyConverter;

// Create converter with USD as base currency
$converter = new CurrencyConverter(CurrencyCode::USD);

// Efficient setup of multiple base rates at once
$converter = $converter->withBaseRates([
    CurrencyCode::EUR => '0.85',  // 1 USD = 0.85 EUR
    CurrencyCode::GBP => '0.75',  // 1 USD = 0.75 GBP
    CurrencyCode::JPY => '110.25' // 1 USD = 110.25 JPY
]);

// Add direct rates between non-base currencies
$converter = $converter->withDirectRates(CurrencyCode::EUR, [
    CurrencyCode::GBP => '0.88',  // 1 EUR = 0.88 GBP
    CurrencyCode::CHF => '1.07'   // 1 EUR = 1.07 CHF
]);

// Add commission rates
$converter = $converter->withCommissionRate(CurrencyCode::USD, CurrencyCode::EUR, 2.5); // 2.5%
$converter = $converter->withDefaultCommissionRate(1.0); // 1% default

// Convert string amounts
$usdAmount = "100";
$eurAmount = $converter->convert($usdAmount, CurrencyCode::USD, CurrencyCode::EUR); // With commission
$eurAmountNoCommission = $converter->convert($usdAmount, CurrencyCode::USD, CurrencyCode::EUR, false); // Without commission

// Convert Money objects
$usdMoney = Money::fromDecimal(100, CurrencyCode::USD);
$eurMoney = $converter->convertMoney($usdMoney, CurrencyCode::EUR); // With commission
$eurMoneyNoCommission = $converter->convertMoney($usdMoney, CurrencyCode::EUR, Money::ROUND_HALF_UP, false); // Without commission

// Money objects can have a default converter
$usdMoneyWithConverter = $usdMoney->withConverter($converter);
$eurMoney = $usdMoneyWithConverter->convertTo(CurrencyCode::EUR);

// Complex path conversion (USD -> EUR -> GBP -> CHF)
// Assumes the rates for these pairs exist in the converter
$chfAmount = $converter->convert('100', CurrencyCode::USD, CurrencyCode::CHF);

Allocation and Distribution

use Bermuda\Stdlib\Money;
use Bermuda\Stdlib\CurrencyCode;

$tenDollars = Money::fromDecimal(10, CurrencyCode::USD);

// Allocate in ratio 3:7
$allocated = $tenDollars->allocate([3, 7]);
// $allocated[0] = $3.00
// $allocated[1] = $7.00

// Split into 3 equal parts (handling cents remainder)
$split = $tenDollars->split(3);
// $split[0] = $3.34
// $split[1] = $3.33
// $split[2] = $3.33

Formatting

use Bermuda\Stdlib\Money;
use Bermuda\Stdlib\CurrencyCode;

$money = Money::fromDecimal(1234.56, CurrencyCode::USD);

// Default formatting
echo $money->format(); // "1234.56 $"

// Locale-specific formatting
echo $money->format('en_US'); // "$1,234.56"
echo $money->format('de_DE'); // "1.234,56 $"
echo $money->format('fr_FR'); // "1 234,56 $"
echo $money->format('ja_JP'); // "¥1,234.56"

// Without currency symbol
echo $money->format('en_US', false); // "1,234.56"

// String representation
echo (string)$money; // "1234.56 USD"

Best Practices

  1. Always Use Immutable Methods: Remember that all operations return new objects. Never assume original objects are modified.

    // WRONG:
    $money->add($otherMoney); // Result is discarded!
    
    // CORRECT:
    $result = $money->add($otherMoney);
  2. Use Strings for High Precision: When precision is critical, pass decimal values as strings.

    // Better for guaranteed precision:
    $money = Money::fromDecimal('1234.56789', CurrencyCode::USD);
    $result = $money->multiply('1.12345');
  3. Use Batch Methods for Multiple Rates: When setting multiple exchange rates, use the batch methods for better performance.

    // LESS EFFICIENT (creates multiple objects):
    $converter = $converter->withBaseRate(CurrencyCode::EUR, '0.85');
    $converter = $converter->withBaseRate(CurrencyCode::GBP, '0.75');
    $converter = $converter->withBaseRate(CurrencyCode::JPY, '110.25');
    
    // MORE EFFICIENT (creates only one new object):
    $converter = $converter->withBaseRates([
        CurrencyCode::EUR => '0.85',
        CurrencyCode::GBP => '0.75',
        CurrencyCode::JPY => '110.25'
    ]);
  4. Initialize Converters Comprehensively: Configure your converter with all rates at initialization.

    $config = [
        'baseCurrency' => 'USD',
        'directRates' => [
            'USD' => ['EUR' => '0.85', 'GBP' => '0.75'],
            'EUR' => ['USD' => '1.17', 'GBP' => '0.88']
        ],
        'commissionRates' => [
            'USD' => ['EUR' => '2.5']
        ],
        'defaultCommissionRate' => '1.0',
        'scale' => 6
    ];
    
    $converter = CurrencyConverter::fromArray($config);

Error Handling

The library uses exceptions to signal errors. Main exception types:

  • \InvalidArgumentException: Invalid parameters or operations
  • \RuntimeException: Runtime errors like unavailable exchange rates

Best practice is to catch these exceptions at appropriate levels in your application.

try {
    $convertedMoney = $money->convertTo(CurrencyCode::EUR);
} catch (\InvalidArgumentException $e) {
    // Handle invalid arguments
} catch (\RuntimeException $e) {
    // Handle runtime errors like missing exchange rates
} catch (\Exception $e) {
    // Handle other exceptions
}