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,
];
}| Cast | Stores |
|---|---|
AsMoney::class | Amount 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::class | One JSON column holding minor and currency — for snapshots. |
AsCurrency::class | A 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 cryptoBy 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.