mathiasverraes/money
Small PHP money library that treats monetary values as immutable value objects to avoid floating-point errors. Includes currency support, arithmetic and comparisons, and formatting helpers—useful for modeling prices, totals, and discounts in a robust, domain-driven way.
## Getting Started
### Minimal Setup
1. **Installation**
```bash
composer require mathiasverraes/money
Add to composer.json if using strict mode:
"config": {
"preferred-install": "dist"
}
First Use Case: Creating a Money Object
use Mathiasverraes\Money\Money;
$amount = Money::EUR(100); // 100 EUR
$amount->getAmount(); // Returns 100 (integer)
$amount->getCurrency(); // Returns "EUR"
// New in v4.9.0: Zero currency shortcut
$zero = Currency::zero(); // Returns a zero-amount Money object for the default currency
$zeroEUR = Currency::EUR()->zero(); // Returns a zero-amount Money object for EUR
Key Classes to Know
Money: Core class for monetary values.Currency: Represents currencies (e.g., Currency::EUR() or new Currency::zero()).MoneyFactory: For creating Money objects (e.g., Money::EUR(100)).Where to Look First
src/Money.php for core functionality.src/Currency.php for currency logic (including new zero() method).$price = Money::EUR(100);
$tax = Money::EUR(20);
$total = $price->add($tax); // 120 EUR
$discounted = $price->subtract(Money::EUR(10)); // 90 EUR
$usd = Money::USD(100);
$eur = $usd->convertTo(Currency::EUR()); // Uses default exchange rate
Mathiasverraes\Money\Currency\ExchangeRateProvider. Use a custom provider (e.g., API-based) for real-world rates.$amount = Money::EUR(1234.56);
echo $amount->format(); // "€1,234.56"
echo $amount->format('%!'); // "€1,234.56" (with currency symbol)
use Mathiasverraes\Money\Exception\InvalidMoneyException;
try {
$invalid = Money::EUR(-100); // Throws InvalidMoneyException
} catch (InvalidMoneyException $e) {
// Handle error
}
// New in v4.9.0: Zero validation
$zero = Currency::EUR()->zero();
$this->assertEquals(0, $zero->getAmount());
// Store as two columns: amount (integer) and currency (string)
$amount = Money::EUR(100);
$storedAmount = $amount->getAmount(); // 100
$storedCurrency = $amount->getCurrency()->getCode(); // "EUR"
// Reconstruct on retrieval
$reconstructed = Money::of($storedAmount, $storedCurrency);
// Zero handling
$zero = Currency::zero(); // New shortcut
$zeroStoredAmount = $zero->getAmount(); // 0
$zeroStoredCurrency = $zero->getCurrency()->getCode(); // Default currency
Request Validation:
use Mathiasverraes\Money\Money;
use Illuminate\Validation\Rule;
$rules = [
'price' => [
'required',
function ($attribute, $value, $fail) {
try {
Money::of($value, 'EUR');
} catch (\Exception $e) {
$fail('Invalid money format. Use `amount,currency` (e.g., `100,EUR`).');
}
},
],
];
Eloquent Casting:
use Mathiasverraes\Money\Money;
use Illuminate\Database\Eloquent\Casts\Attribute;
public function price(): Attribute
{
return Attribute::make(
get: fn ($value) => Money::of($value['amount'], $value['currency']),
set: fn ($value) => [
'amount' => $value->getAmount(),
'currency' => $value->getCurrency()->getCode(),
],
);
}
// Zero handling in casting public function zeroPrice(): Attribute { return Attribute::make( get: fn ($value) => $value ? Money::of($value['amount'], $value['currency']) : Currency::zero(), set: fn ($value) => $value ? [ 'amount' => $value->getAmount(), 'currency' => $value->getCurrency()->getCode(), ] : null, ); }
use Mathiasverraes\Money\Money;
use PHPUnit\Framework\TestCase;
class MoneyTest extends TestCase
{
public function testAddition()
{
$this->assertEquals(
Money::EUR(120),
Money::EUR(100)->add(Money::EUR(20))
);
}
public function testZeroCurrency()
{
$zero = Currency::EUR()->zero();
$this->assertEquals(0, $zero->getAmount());
$this->assertEquals('EUR', $zero->getCurrency()->getCode());
}
}
Floating-Point Precision
Money::EUR(100) for €1.00, not Money::EUR(1.00)).Money::EUR(100) = €1.00).Currency Code Case Sensitivity
EUR, USD) are case-sensitive. Always use uppercase.Exchange Rates
ExchangeRateProvider:
use Mathiasverraes\Money\Currency\ExchangeRateProvider;
class ApiExchangeRateProvider implements ExchangeRateProvider
{
public function getRate(string $from, string $to): float
{
// Fetch from API (e.g., European Central Bank)
return 1.1; // Example: 1 EUR = 1.1 USD
}
}
$this->app->singleton(ExchangeRateProvider::class, function () {
return new ApiExchangeRateProvider();
});
Negative Amounts
Money objects cannot have negative amounts. Override if needed:
$negative = Money::of(-100, 'EUR'); // Throws InvalidMoneyException
$negative = Money::of(-100, 'EUR', true); // Allows negative (3rd param: $allowNegative)
Serialization
Money objects are not JSON-serializable by default. Use:
$json = json_encode([
'amount' => $amount->getAmount(),
'currency' => $amount->getCurrency()->getCode(),
]);
JsonSerializable:
class SerializableMoney implements JsonSerializable
{
public function jsonSerialize(): array
{
return [
'amount' => $this->getAmount(),
'currency' => $this->getCurrency()->getCode(),
];
}
}
Laravel Caching
Money::of() frequently, cache Currency objects:
$eur = app(Currency::class)->get('EUR'); // Reuse instance
$zeroEUR = $eur->zero(); // New zero shortcut
Use Static Factory Methods
Prefer Money::EUR(100) or Currency::EUR()->zero() (new) over new Money(100, Currency::EUR()) for readability.
Custom Rounding Override rounding behavior for specific use cases:
$amount = Money::of(1234, 'EUR', 0, RoundingMode::UP); // Rounds up
Localization
Use NumberFormatter for locale-aware formatting:
use NumberFormatter;
$formatter = new NumberFormatter('de_DE', NumberFormatter::CURRENCY);
echo $amount->format($formatter); // "1.2
How can I help you explore Laravel packages today?