moneyphp/money
moneyphp/money is a PHP value-object library for safe money handling without floats. Uses string-based big integers, supports arithmetic, allocation, currencies/ISO repositories, formatting (incl. intl), JSON serialization, and exchange rates. Requires BCMath.
Installation:
composer require moneyphp/money
Ensure bcmath extension is enabled in php.ini.
First Use Case:
Create a Money object with a currency and amount (in smallest unit, e.g., cents):
use Money\Money;
use Money\Currency;
$eur = Money::EUR(500); // 5.00 EUR (500 cents)
$usd = Money::USD(100); // 1.00 USD
Key Classes to Know:
Money: Immutable value object for monetary amounts.Currency: Represents currencies (e.g., Currency::EUR).MoneyFactory: Static factory for creating Money objects.Converter: Handles currency conversion (requires moneyphp/currency for exchange rates).Basic Arithmetic:
$total = $eur->add($usd); // Adds two Money objects (requires same currency)
$difference = $eur->subtract(Money::EUR(200));
$allocated = $eur->allocate([1, 2]); // Splits money proportionally
Currency Conversion:
$converter = new \Money\Converter(new \Money\Currency\ISOCurrencies());
$converter->addCurrencyPair(new \Money\Currency\CurrencyPair('EUR', 'USD', 1.1));
$condollar = $converter->convert($eur, 'USD'); // Converts EUR to USD
Formatting:
use Money\Formatter\DecimalMoneyFormatter;
$formatter = new DecimalMoneyFormatter();
echo $formatter->format($eur); // "5.00"
Serialization:
$json = json_encode($eur); // '{"amount":500,"currency":"EUR"}'
$decoded = json_decode($json, true);
$restored = Money::fromArray($decoded);
Laravel Models:
Use accessors/mutators to convert between Money and database storage (e.g., store as cents):
public function getPriceAttribute($value) {
return Money::USD($value * 100);
}
Validation:
Use Money in Laravel’s FormRequest validation:
$this->validate($request, [
'price' => 'required|numeric|min:0',
]);
$money = Money::USD((int)($request->price * 100));
Aggregations:
Use Money::min(), Money::max(), or Money::sum() for collections:
$prices = collect([Money::EUR(100), Money::EUR(200)]);
$total = $prices->sum();
Testing:
Use Money\Comparator for assertions:
$this->assertTrue(Money::Comparator::equals($eur, Money::EUR(500)));
Floating-Point Traps:
$bad = Money::USD(1.99); // ❌ Avoid floats!
$good = Money::USD(199); // ✅ Use integers (cents)
Currency Codes:
EUR, not eur). The library throws InvalidArgumentException otherwise.Exchange Rates:
moneyphp/currency for dynamic rates. For testing, use FixedExchange:
$exchange = new \Money\Exchange\FixedExchange([new \Money\Currency\CurrencyPair('EUR', 'USD', '1.1')]);
BCMath/GMP Dependencies:
bcmath is unavailable, fall back to gmp or plain PHP (slower). Configure via Money\Calculator\Calculator:
$calculator = new \Money\Calculator\GmpCalculator();
Allocation Edge Cases:
allocate() may not distribute the full amount if ratios sum to zero:
$money->allocate([0, 0]); // Returns [Money::zero(), Money::zero()]
Negative Values:
Assertions:
Use Money\Comparator for precise comparisons in tests:
$this->assertTrue(Money::Comparator::equals($money1, $money2));
Serialization Issues:
amount is a numeric string or integer:
$money->jsonSerialize(); // Returns ['amount' => '500', 'currency' => 'EUR']
Performance:
CurrencyPair objects:
$cache = new \Psr\SimpleCache\CacheItemPool();
$converter->setCache($cache);
Custom Calculators:
Implement \Money\Calculator\CalculatorInterface for custom arithmetic (e.g., custom rounding):
class CustomCalculator implements CalculatorInterface {
public function add(string $augend, string $addend): string { ... }
// Implement other methods...
}
Custom Currencies:
Extend \Money\Currency\Currency for domain-specific currencies (e.g., "LOYALTY_POINTS"):
class LoyaltyPoints extends Currency {
public static function LOYALTY(): self { return new self('LPT'); }
}
Parsers/Formatters:
Create custom parsers (e.g., for legacy formats) by implementing \Money\Parser\ParserInterface:
class LegacyParser implements ParserInterface {
public function parse(string $amount, Currency $currency): Money { ... }
}
Exchange Strategies:
Extend \Money\Exchange\ExchangeInterface for custom exchange logic (e.g., API-based rates):
class ApiExchange implements ExchangeInterface {
public function getRate(CurrencyPair $currencyPair): string { ... }
}
Database Storage:
Store Money as cents in a bigint column to avoid precision loss:
// Migration
Schema::create('orders', function (Blueprint $table) {
$table->bigInteger('amount_cents')->unsigned();
$table->string('currency');
});
Eloquent Accessors:
Use accessors to convert between Money and database values:
public function getPriceAttribute($value) {
return Money::USD($value);
}
public function setPriceAttribute($money, $value) {
$this->attributes['price'] = $money->getAmount();
}
API Responses:
Format Money in API responses using Money\Formatter\IntlMoneyFormatter:
use Money\Formatter\IntlMoneyFormatter;
$formatter = new IntlMoneyFormatter('en_US');
return response()->json(['price' => $formatter->format($money)]);
Caching Exchange Rates:
Cache CurrencyPair objects in Laravel’s cache:
$cacheKey = "exchange_rate_{$from}_{$to}";
$rate = Cache::remember($cacheKey, now()->addHours(1), function () use ($from, $to) {
return $converter->getRate(new CurrencyPair($from, $to));
});
Testing:
Use Laravel’s RefreshDatabase with Money seed data:
public function setUp(): void {
parent::setUp();
$this->seed(MoneySeeder::class);
}
Example seeder:
class MoneySeeder implements Seeder {
public function run() {
DB::table('products')->insert([
['price_cents' => 1000, 'currency' => 'EUR'],
]);
}
}
How can I help you explore Laravel packages today?