Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Money Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

Implementation Patterns

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:

  1. amount_minor + currency — what the customer saw.
  2. base_amount_minor + base_currency — your reporting currency at the time of the transaction.
  3. 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

  • In Blade, reach for the directive/helper the package registers rather than new Money(...) inline.
  • In API resources, return a structured object rather than only the formatted string, because mobile clients often need the numeric value:
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.

Gotchas and Tips

  • Off-by-100 bugs. 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.
  • Precision is per-currency, not always 2. JPY has 0 decimals, KWD/BHD have 3. 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.
  • Floats in, sadness out. The constructor accepts scalars, but floats invite rounding errors. Convert decimal input to minor units with a string-aware parser or brick/money's parsers, not * 100.
  • Exchange rate APIs fail. Network timeouts, rate limits, and missing API keys will throw at the worst moment — usually during checkout. Wrap conversion in a try/catch with a fallback (last known rate from cache, or refuse the transaction). Always cache aggressively.
  • Rates drift. 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.
  • Unknown ISO codes throw. 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.
  • Blade directive vs helper. Older and newer releases expose slightly different sugar (@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.
  • JSON and __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.
  • Drop down to 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.
  • Extension point: the currency data. If you operate in a currency the package doesn't ship, or you need custom symbols/names, override the relevant config/currency definitions in your own service provider rather than forking — the Currency class is the seam.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource