NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Money for Laravel

Eloquent casts & schema

Cast model attributes straight to Money. The casts are strict: only Money or null can be assigned, and only in a registered currency:

use RoundlyConsulting\Money\Casts\AsCurrency;
use RoundlyConsulting\Money\Casts\AsMoney;
use RoundlyConsulting\Money\Casts\AsMoneyJson;

protected function casts(): array
{
    return [
        'price'            => AsMoney::class,                          // price + price_currency
        'compare_at_price' => AsMoney::currencyColumn('currency'),     // shared currency column
        'budget'           => AsMoney::fixedCurrency('EUR'),           // amount column only
        'store_credit'     => AsMoney::configCurrency('shop.currency'),
        'total'            => AsMoney::attributeCurrency('shop_currency'),
        'snapshot'         => AsMoneyJson::class,                      // {"minor":"1050","currency":"EUR"}
        'currency'         => AsCurrency::class,
    ];
}
CastStores
AsMoney::classAmount column + {key}_currency column (the default).
AsMoney::currencyColumn('currency')Amount + a named currency column, which may be shared.
AsMoney::fixedCurrency('EUR')Amount only; the currency is a constant.
AsMoney::configCurrency('shop.currency')Amount only; the currency is read from config at read time.
AsMoney::attributeCurrency('shop_currency')Amount only; the currency comes from another attribute.
AsMoneyJson::classOne JSON column holding minor and currency — for snapshots.
AsCurrency::classA currency code column ⇄ a registered Currency object.

Schema macros

$table->money() creates a signed decimal(38, 0) amount column plus a currency column; moneyJson() and currencyCode() cover the rest. The macros are always registered, because published migrations depend on them:

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->money('price');                                         // decimal(38,0) + price_currency
    $table->money('compare_at_price', currency: 'currency', nullable: true);
    $table->money('budget', currency: false);
    $table->moneyJson('snapshot', nullable: true);
    $table->currencyCode('shop_currency');
    $table->timestamps();
});
  • money($column, currency: null, nullable: false) — returns the amount column. A string currency names a column that can be shared (it is added once per blueprint); currency: false adds none.
  • moneyJson($column, nullable: false) — jsonb on PostgreSQL, json on MySQL, text on SQLite.
  • currencyCode($column = 'currency', nullable: false) — varchar(schema.currency_length).

Writing and querying

$product->price = Money::ofMajor('99.90', 'EUR');   // writes price + price_currency
$product->price = 9990;                             // throws InvalidMoneyValue — a number has no currency

// compare_at_price shares the "currency" column: set the currency first to change it on purpose
$product->fill(['currency' => 'USD', 'compare_at_price' => Money::ofMajor('120', 'USD')]);

Product::query()->orderBy('price')->get();                          // numeric order
Product::query()->where('price', '>', $threshold->minor())->get();  // exact filter

$sum = Product::query()->where('price_currency', 'EUR')->sum('price');
Money::ofMinor($sum, 'EUR');                                         // exact total — group sums by currency
  • A bare int or string throws InvalidMoneyValue — a number has no currency and no unit.
  • A shared currency column is never silently re-denominated: writing USD next to an EUR amount throws CurrencyMismatch. Set the currency column first to change it on purpose.
  • More digits than schema.precision throw InvalidMoneyValue; on SQLite, amounts beyond int64 are refused instead of silently stored as a float.
  • Re-assigning an equal Money is not an UPDATE — dirty tracking compares values, not bytes.
  • orderBy(), where() and sum() on money columns are numerically exact, also beyond 2^53 minor units where a double would round; group sums by currency. The test suite pins this on SQLite, PostgreSQL and MySQL 8.

Existing bigint money columns keep working with AsMoney; beyond int64 the engine refuses the write. Use AsMoneyJson for snapshots, and two-column money for anything you filter or sort by.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.