brick/money
Immutable money & currency library for PHP with exact arithmetic, explicit rounding control, and support for any-sized amounts. Built on brick/math to avoid floating-point errors; works with ISO currencies and integrates well with GMP/BCMath for speed.
Installation:
composer require brick/money
Ensure PHP 8.2+ is used (or downgrade for older versions if needed).
First Use Case:
Create a Money object from a numeric value and currency code:
use Brick\Money\Money;
$price = Money::of(19.99, 'USD'); // USD 19.99
Key Classes to Know:
Money: Immutable money object.MoneyBag: Container for mixed-currency amounts.Currency: Represents currencies (e.g., Currency::of('USD')).Context\*: Customize rounding (e.g., CashContext, AutoContext).Where to Look First:
Immutable Operations:
Always return new Money instances for operations (e.g., plus(), minus()).
$total = $subtotal->plus($tax);
Currency Safety:
Validate currencies before operations to avoid CurrencyMismatchException:
if ($order->currency !== $payment->currency) {
throw new \InvalidArgumentException("Currency mismatch");
}
Rounding Modes:
Pass RoundingMode (e.g., RoundingMode::Up) for precision-critical operations:
$rounded = $amount->dividedBy(3, RoundingMode::Down);
Contexts for Special Cases:
CashContext for currencies with cash rounding rules (e.g., CHF):
$chf = Money::of(10, 'CHF', new CashContext(step: 5));
AutoContext for dynamic precision:
$dynamic = Money::of(1.1, 'USD', new AutoContext());
MoneyBag for Mixed Currencies: Aggregate amounts across currencies, then convert to a target currency:
$bag = new MoneyBag([$eur, $jpy]);
$totalUsd = $bag->getTotal('USD'); // Converts EUR/JPY to USD
Allocation for Splitting: Distribute amounts proportionally (e.g., revenue splits):
[$share1, $share2] = $profit->allocate([60, 40], AllocationMode::FloorToFirst);
Money as minor units (e.g., cents) in a decimal column:
$minor = $money->getAmount()->toScale(2)->toString(); // "1999" for USD 19.99
$data = [
'amount' => $money->getAmount()->toString(),
'currency' => $money->getCurrency()->getCode(),
];
RationalMoney for exact arithmetic in tests:
$expected = Money::of(1, 'USD')->toRational()->dividedBy(3);
$actual = $money->dividedBy(3);
$this->assertTrue($expected->isEqualTo($actual->toRational()));
Rounding Mode Scope:
Money with RoundingMode::Up doesn’t affect later operations.Currency Updates:
Currency::of().brick/money version (e.g., 0.13.*) or handle exceptions:
try {
$currency = Currency::of('XBD'); // New currency
} catch (UnknownCurrencyException $e) {
// Fallback logic
}
Floating-Point Assumptions:
Money to float/double for storage/calculation—use getAmount()->toString() or minor units.AutoContext Limitations:
AutoContext throws exceptions for infinite decimals (e.g., 1/3).RationalMoney for intermediate steps.MoneyBag Precision:
MoneyBag uses exchange rates from Brick\Money\ExchangeRateProvider.Thread Safety:
Money is immutable, but MoneyBag or shared ExchangeRateProvider instances may need synchronization in concurrent environments.Unexpected Rounding:
dividedBy(3) for USD 1.00 → 0.33).RoundingMode::Up/Down explicitly.Currency Mismatch:
assertSame($a->getCurrency(), $b->getCurrency())).Performance:
// In php.ini
extension=gmp
extension=bcmath
Serialization:
Money::parse() to reconstruct objects from strings:
$money = Money::parse('USD 19.99');
Custom Contexts:
Extend Context for domain-specific rules (e.g., TaxContext):
class TaxContext extends Context {
public function __construct(public int $taxRate) {}
public function round(Money $money): Money {
return $money->multipliedBy(1 + $this->taxRate / 100);
}
}
Exchange Rate Providers:
Implement ExchangeRateProvider for custom rates:
class ApiExchangeRateProvider implements ExchangeRateProvider {
public function getRate(Currency $from, Currency $to): string {
return $this->fetchFromApi($from->getCode(), $to->getCode());
}
}
MoneyBag Aggregation:
Override MoneyBag::getTotal() to apply business logic (e.g., discounts):
class DiscountedMoneyBag extends MoneyBag {
public function getTotal(Currency $currency): Money {
$total = parent::getTotal($currency);
return $total->minus($this->calculateDiscount($total));
}
}
Rounding Strategies:
Create custom RoundingMode implementations for specific rounding rules:
class BankersRoundingMode implements RoundingMode {
public function round(Money $money, int $scale): Money {
// Custom logic (e.g., round to even)
}
}
How can I help you explore Laravel packages today?