sebastian/money
Sebastian/money is an abandoned PHP value object library for monetary values using a currency's smallest unit, with formatting and currency subclasses. It was intended as a PHPUnit example, not for production; use moneyphp/money instead.
First, calibrate expectations: sebastian/money is abandoned (last release 1.6.0, 2013-11-25). The author explicitly states it was an example for PHPUnit features, not production code. Treat this assessment as "how you'd use it if it's already in the codebase," and default to moneyphp/money for anything new.
Minimal setup:
composer require sebastian/money:^1.6
Pin the exact minor (1.6.*) — there will be no patches, so a loose constraint buys you nothing but surprise if a mirror or fork republishes a tag.
Where to look first (the package is tiny — read it all in one sitting):
src/Money.php — constructor, getAmount(), getConvertedAmount(), arithmetic methods, equals().src/Currency.php — a thin value object around an ISO code string.src/Exception/ — the exception types you'll be catching.First use case — represent and total a line item:
use SebastianBergmann\Money\Currency;
use SebastianBergmann\Money\Money;
$currency = new Currency('EUR');
// 100 = smallest unit, i.e. 1.00 EUR
$line = new Money(100, $currency);
$total = $line->add(new Money(250, $currency)); // 3.50 EUR
print $total->getAmount(); // 350 <- what you persist
print $total->getConvertedAmount(); // 3.50 <- what you display
Rule of thumb from day one: getAmount() is the truth, getConvertedAmount() is presentation. Store the integer, format at the edge.
Persist amount + currency code, never a float. Schema: amount BIGINT NOT NULL, currency CHAR(3) NOT NULL. Hydrate in a repository or Eloquent cast:
protected $casts = ['amount' => 'integer'];
public function money(): Money
{
return new Money($this->amount, new Currency($this->currency));
}
Wrap construction in one factory. User input arrives as "1.234,56", "$12.00", 1.5, etc. Centralize parsing so the rest of the app only ever sees Money:
final class MoneyFactory
{
public static function fromDecimalString(string $value, string $code): Money
{
// avoid float math entirely: shift the decimal, then cast
$normalized = preg_replace('/[^\d.\-]/', '', $value);
$amount = (int) round(((float) $normalized) * 100); // see Gotchas
return new Money($amount, new Currency($code));
}
}
Keep arithmetic inside the domain layer. add()/subtract() return new instances; assign them. Services should accept and return Money, and only convert at the boundary (controller, API resource, Blade view).
Format with intl, not with string concatenation:
$fmt = new \NumberFormatter('de_DE', \NumberFormatter::CURRENCY);
echo $fmt->formatCurrency($money->getConvertedAmount(), $money->getCurrency()->getCode());
Currency guard at the edges. Do comparisons before arithmetic ($a->getCurrency()->getCode() === $b->getCurrency()->getCode()), and throw your own domain exception rather than relying on whatever the library throws. This keeps the library's exception surface out of your domain.
Splitting/allocating amounts (invoices, tax lines, discounts): don't hope for a built-in allocate(); verify against src/Money.php in your version. Safer to write your own integer split — divide getAmount() with intdiv(), distribute the remainder one unit at a time — and construct Money objects from the results. That logic is ten lines and stays testable.
Integration tip: wrap the library behind your own Money interface if the codebase is non-trivial. You are almost certainly going to migrate to moneyphp/money, and a one-file adapter (getAmount() → getMinorAmount(), Currency → Money::EUR()) turns a codebase-wide rewrite into a single class change.
getConvertedAmount() divides by 100. That is wrong for JPY (0), BHD/KWD/JOD (3), and ISK. If you accept non-2-exponent currencies, compute base units yourself from an exponent table and never call getConvertedAmount().Currency is not validated against ISO 4217. new Currency('XXX') will happily construct. Validate codes at your boundary (config whitelist or intl's currency list).$money->add($other); silently does nothing. Static analysis won't catch it. Consider enabling a "no unused expression" style rule (PHPStan/Psalm) or make the habit of always assigning.(float) $money->getConvertedAmount() * 3 reintroduces exactly the rounding bugs the value object exists to prevent. Multiply the integer: new Money($m->getAmount() * 3, $m->getCurrency()).fromString/factory: round($x * 100) uses PHP's default half-up-away-from-zero; bankers rounding differs. Document the rule and test with 0.005, 1.005, and negative values.SebastianBergmann\Money\Exception\* leak into controllers.var_dump() of the object shows the integer amount only — no formatted value, no __toString(). Add a helper Money::__debugInfo-equivalent or a test helper that prints amount . ' ' . code so failing assertions are readable.getAmount() → getMinorAmount()/getAmount(), new Currency('EUR') → Money::EUR($amount), and getConvertedAmount() → formatToDecimal()/getAmount()->toFloat() in moneyphp/money. Run both implementations against a fixture table of amounts (including JPY and BHD) before cutting over — that fixture is the cheapest insurance you can buy here.How can I help you explore Laravel packages today?