bermudaphp / money
Precision money and currency handling library for PHP with support for arithmetic operations, currency conversion, and formatting
Requires
- php: >=8.4
- ext-bcmath: *
Requires (Dev)
- phpstan/phpstan: ^2.1.14
- phpunit/phpunit: ^10.5
- squizlabs/php_codesniffer: ^3.7
Suggests
- ext-intl: Required for locale-aware formatting
This package is auto-updated.
Last update: 2025-05-09 16:31:39 UTC
README
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 codeCurrencyCode::getAllCodes()
: Get all currency codes as an arrayCurrencyCode->getName()
: Get the full name of the currencyCurrencyCode->getSymbol()
: Get the symbol for the currencyCurrencyCode->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 unitMoney::fromDecimal(float|string $value, CurrencyCode $currency)
: Create from decimal valueMoney::fromString(string $moneyString)
: Create from string like "10.99 USD"Money::zero(CurrencyCode $currency)
: Create a zero money objectMoney::one(CurrencyCode $currency)
: Create a money object with value of one unit
Arithmetic Methods:
add(Money $other)
: Add another Money objectsubtract(Money $other)
: Subtract another Money objectmultiply(number $factor)
: Multiply by a factordivide(number $divisor)
: Divide by a divisorincrement(int $amount = 1)
: Increment by smallest unitsdecrement(int $amount = 1)
: Decrement by smallest unitsabsolute()
: Get absolute valuenegate()
: Get negated value
Comparison Methods:
equals(Money|array $other, string $mode = COMPARE_ANY)
: Check equalitygreaterThan(Money|array $other, string $mode = COMPARE_ALL)
: Check if greater thangreaterThanOrEqual(Money|array $other, string $mode = COMPARE_ALL)
: Check if greater than or equallessThan(Money|array $other, string $mode = COMPARE_ALL)
: Check if less thanlessThanOrEqual(Money|array $other, string $mode = COMPARE_ALL)
: Check if less than or equalbetween(Money $min, Money $max, bool $inclusive = true)
: Check if between two valuesisZero()
: Check if amount is zeroisPositive()
: Check if amount is positiveisNegative()
: Check if amount is negative
Distribution Methods:
allocate(array $ratios)
: Allocate amount according to ratiossplit(int $n)
: Split amount equally into n parts
Other Methods:
format(?string $locale = null, bool $includeSymbol = true)
: Format according to localeconvertTo(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 converterCurrencyConverter::fromArray(array $data)
: Create from configuration arraytoArray()
: Export configuration to arraywithBaseCurrency(CurrencyCode $currency)
: Create new instance with different base currencywithBaseRate(CurrencyCode $currency, $rate)
: Add base exchange ratewithBaseRates(array $rates)
: Add multiple base exchange rates in single operationwithDirectRate(CurrencyCode $from, CurrencyCode $to, $rate)
: Add direct exchange ratewithDirectRates(CurrencyCode $from, array $rates)
: Add multiple direct rates in single operationwithCommissionRate(CurrencyCode $from, CurrencyCode $to, $rate)
: Add commission ratewithDefaultCommissionRate($rate)
: Set default commission ratewithScale(int $scale)
: Set calculation precision
Conversion Methods:
convert($amount, CurrencyCode $from, CurrencyCode $to, bool $includeCommission = true)
: Convert an amountconvertMoney(Money $money, CurrencyCode $targetCurrency)
: Convert a Money objectcalculateCommission($amount, CurrencyCode $from, CurrencyCode $to)
: Calculate commission for a conversion
Exchange Rate Methods:
getExchangeRate(CurrencyCode $from, CurrencyCode $to)
: Get exchange rate between currenciesgetDirectRate(CurrencyCode $from, CurrencyCode $to)
: Get direct exchange rate if availablegetBaseRate(CurrencyCode $currency)
: Get base exchange rate if availablegetCommissionRate(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
-
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);
-
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');
-
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' ]);
-
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 }