---
title: "Eloquent casts & schema — Money for Laravel | Roundly"
description: "Cast attributes to Money in one or two columns, create exact decimal money columns with schema macros, and query them correctly."
url: https://roundly-consulting.com/open-source/docs/money-for-laravel/eloquent
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Money for Laravel](https://roundly-consulting.com/open-source/docs/money-for-laravel.md)

# 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:

```php
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:

```php
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

```php
$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.
- The casts never cache the Money object: every read builds it from the current columns, config or attribute, so reading $order->total never pins its currency for a later save() or toArray().
- 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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/money-for-laravel.md)
