brick/money
Brick\Money is a PHP library for precise, immutable money and currency values. It provides exact arithmetic (no float errors), explicit rounding control, and supports large amounts via brick/math, with optional GMP/BCMath acceleration.
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 string or numeric value with a currency code:
use Brick\Money\Money;
$price = Money::of('19.99', 'USD'); // USD 19.99
$price = Money::ofMinor(1999, 'USD'); // USD 19.99 (from cents)
Where to Look First:
Immutable Operations:
All operations return new Money instances. Avoid modifying state directly:
$total = $subtotal->plus($tax)->minus($discount);
Currency-Safe Arithmetic:
Validate currencies before operations to avoid CurrencyMismatchException:
if ($order->getAmount()->getCurrency() === $payment->getAmount()->getCurrency()) {
$balance = $order->getAmount()->plus($payment->getAmount());
}
Contextual Money Handling:
Use contexts for domain-specific rules (e.g., cash rounding for CHF):
$cashMoney = Money::of(10, 'CHF', new CashContext(step: 5));
Rounding Strategies:
Pass RoundingMode explicitly for operations requiring precision control:
$rounded = $amount->dividedBy(3, RoundingMode::HalfUp);
Database Storage:
Store Money as minor units (e.g., cents) in a numeric column to avoid floating-point issues:
$minorAmount = $money->getAmount()->toScale(2)->toEngine()->toString();
Validation:
Use isPositive() or isZero() for business logic checks:
if (!$order->getAmount()->isPositive()) {
throw new InvalidOrderException("Amount must be positive");
}
API Responses:
Serialize Money to JSON with currency and formatted amount:
return response()->json([
'amount' => $money->getAmount()->toString(),
'currency' => $money->getCurrency()->getCode(),
]);
Testing:
Use isEqualTo() for assertions (avoids floating-point errors):
$this->assertTrue($expected->isEqualTo($actual));
Rounding Modes:
Money; they must be passed to each operation.
// ❌ Fails: No rounding mode provided
$money->plus('0.999'); // RoundingNecessaryException
// ✅ Works: Explicit rounding
$money->plus('0.999', RoundingMode::Down);
RoundingMode::HalfUp (bankers' rounding) for financial calculations.Currency Mismatches:
CurrencyMismatchException.
$usd->plus($eur); // Throws exception
MoneyBag for multi-currency totals or validate currencies first.Context Inconsistency:
Money with different contexts (e.g., DefaultContext vs. CashContext) may yield unexpected results.
$defaultMoney->plus($cashMoney); // May throw or behave unpredictably
Money to DefaultContext before arithmetic).RationalMoney Precision:
RationalMoney avoids rounding but can’t represent infinite decimals (e.g., 1/3).
$rational->dividedBy(3); // May throw ArithmeticException
toContext() to convert to Money with a defined rounding mode.Currency Updates:
XBD for digital currencies) may break code using Currency::of().
Currency::of('XBD'); // May fail in future versions
brick/money version (e.g., 0.13.*) or use numeric codes for stability.Check Minor Units:
Use getAmount()->toEngine()->toString() to inspect raw values:
$money->getAmount()->toEngine()->toString(); // "12345678901234567890"
Enable GMP/BCMath: Install PHP extensions for faster calculations:
sudo apt-get install php-gmp php-bcmath # Linux
Verify with:
php -m | grep -E 'gmp|bcmath'
Log Rounding Decisions: Explicitly log rounding modes for auditing:
$result = $amount->dividedBy(3, RoundingMode::Up);
logger()->info("Rounded {$amount} / 3 to {$result} using Up mode");
Custom Contexts:
Extend Context for domain-specific rules (e.g., tax rounding):
class TaxContext extends Context {
public function __construct() {
parent::__construct(2, 1, 100); // Scale, step, rounding increment
}
}
Currency Providers:
Implement IsoCurrencyProvider for custom currency sources (e.g., ERP integrations):
class CustomCurrencyProvider implements IsoCurrencyProvider {
public function getCurrency(string $code): Currency {
return new Currency($code, 'Custom Currency', 2);
}
}
MoneyBag Aggregation:
Override MoneyBag to add custom logic (e.g., exchange rates):
class ExchangeMoneyBag extends MoneyBag {
public function getTotal(): Money {
$total = parent::getTotal();
return $total->multipliedBy($this->exchangeRate);
}
}
Event Listeners:
Use Laravel events to validate Money before persistence:
// In a service provider
Event::listen(OrderCreating::class, function (OrderCreating $event) {
if (!$event->order->amount->isPositive()) {
throw new \InvalidArgumentException("Amount must be positive");
}
});
Batch Operations:
Use MoneyBag to aggregate multiple Money objects before processing:
$bag = new MoneyBag([$order1->amount, $order2->amount]);
$total = $bag->getTotal();
Avoid RationalMoney in Loops:
Convert to Money early to reduce overhead:
// ❌ Slow: RationalMoney in loop
foreach ($items as $item) {
$rational = $item->price->toRational()->multipliedBy($item->quantity);
}
// ✅ Faster: Convert to Money upfront
foreach ($items as $item) {
$money = $item->price->multipliedBy($item->quantity);
}
How can I help you explore Laravel packages today?