akaunting/money
Akaunting Money is a PHP/Laravel package for handling monetary values with precision. It offers currency formatting, arithmetic, conversion, and multi-currency support, making it easy to build invoices, accounting, and e-commerce features reliably.
Install and publish
composer require akaunting/money
php artisan vendor:publish --tag=money
This drops config/money.php into your app. Laravel's package discovery registers the service provider and (depending on version) the @money Blade directive automatically.
Configure the defaults first
Open config/money.php and set the three things that bite people later:
defaults.currency — the ISO code used when none is passed (usually your accounting currency).defaults.precision — decimal places to render; note the package still reads per-currency precision for known ISO codes.exchange.* — the driver and API key used for currency conversion. Leave conversion alone until you actually need it; most apps only need formatting.If you use php artisan config:cache in production, re-run it after touching this file.
First use case: format a stored integer
The core mental model is that Money takes an integer amount in minor units and a Currency, then renders it:
use Akaunting\Money\Money;
use Akaunting\Money\Currency;
echo Money::USD(1299); // "$12.99"
echo new Money(1299, new Currency('EUR')); // "€12.99"
The static Money::XXX(...) form is a convenience wrapper around new Money($amount, new Currency('XXX')). If you have never seen this package before, read Akaunting\Money\Money and Akaunting\Money\Currency before anything else — they are small, and they wrap brick/money, which is where the real arithmetic lives.
Where to look next
Money::getAmount(), getValue(), getCurrency() — the raw data.Money::format(), formatSimple(), formatWithoutZeroes(), formatLocale() — the rendering variants.Currency::getSymbol(), getName(), getCode(), getPrecision(), getRate() — per-currency metadata.Store minor units, never formatted strings
The single most important convention: your database columns hold integers (amount_minor) plus an ISO code (currency). Formatting is a presentation concern and belongs in resources, mailables, and Blade — not in the schema. A $table->integer('total') that silently means cents will save you from float drift, but only if the whole team agrees it means cents.
Custom Eloquent cast
Wrap the package in a cast so controllers stay clean:
class MoneyCast implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return new Money((int) $value, new Currency($attributes['currency']));
}
public function set($model, $key, $value, $attributes)
{
return $value instanceof Money ? $value->getAmount() : (int) $value;
}
}
Pair it with a currency column on the same model. Be explicit in the cast about which column holds the code; hardcoding 'USD' defeats the point of a currency column.
Multi-currency transactions
For invoices, orders, and payments, store three things, not one:
amount_minor + currency — what the customer saw.base_amount_minor + base_currency — your reporting currency at the time of the transaction.exchange_rate — the rate you applied, snapshotted.Display-time conversion is fine for dashboards, but ledger rows must freeze the rate. Otherwise last month's revenue will silently change every time the FX API updates.
Conversion workflow
Keep conversion at the edges. A typical flow:
$usd = Money::USD(1000);
// convert only when crossing a boundary (checkout, payout, report)
$try = $usd->convert('TRY'); // or your version's equivalent; check the class
Cache the underlying rates (Redis with a short TTL is plenty) and never call the exchange API per row in a loop. If you render a table of 200 orders, convert once per distinct currency pair, not once per order.
Blade and API output
new Money(...) inline.return [
'amount' => $this->total->getAmount(), // 1299
'value' => $this->total->getValue(), // 12.99 (float, display only)
'currency' => $this->total->getCurrency()->getCode(),
'formatted' => $this->total->format(),
];
Validation
Validate incoming currency codes against the ISO list the Currency class knows about, and validate amounts as integers. A Rule::in([...]) built from known codes plus an integer rule on the minor-unit field is usually enough. Do not accept 12.99 from a form and multiply by 100 in the controller — parse it once, in a dedicated request or DTO.
Testing
Freeze or stub the exchange driver in tests, then assert on format() and toArray(). If your tests hit the live FX API you will eventually have a red build on a Tuesday for no reason.
Money::USD(5) is five cents, not five dollars. Every newcomer trips on this. Name your variables $amountMinor and $amountCents to make it obvious in review.Currency::getPrecision() is authoritative; hardcoding number_format($v, 2) next to this package will produce wrong output for a dozen currencies.format() vs formatLocale(). The default format() follows the currency's own symbol placement (symbol before/after, spacing) — it is not locale-aware. Use formatLocale('de_DE') when you need German separators, and expect differences between environments depending on whether intl is installed. Snapshot tests are your friend here.brick/money's parsers, not * 100.Currency::getRate() (and conversion) reflect now. Never use it to recompute historical totals. That is what the snapshotted rate column is for.config:cache staleness. Changing defaults.currency in .env and not re-caching produces confusing behavior in production but not locally. When in doubt, php artisan config:clear && php artisan config:cache.new Currency('USDD') will not quietly fall back — it will raise. Normalize user input to uppercase and validate before constructing. Store currency codes in a char(3) column and add a check constraint if your DB supports it.@money, a money() helper, or neither). Check the service provider in vendor/akaunting/money/src before wiring it into shared layouts, and prefer the plain Money::USD(...) class call in code you want to survive minor upgrades.__toString. The object casts to a formatted string, which is convenient for views and dangerous in APIs — json_encode of a Money will hand your frontend a pre-formatted string and a currency you can't recover. Always map to an explicit array in resources.brick/money for real math. Allocation (splitting an invoice three ways without losing a cent), custom rounding modes, and currency-aware comparisons are all better handled by the underlying library. Akaunting\Money\Money is a formatting/conversion facade, so reach through to the wrapped Brick\Money\Money if you need allocate() or roundTo() rather than writing your own remainder logic.Currency class is the seam.How can I help you explore Laravel packages today?