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

Nova Money Field Laravel Package

vyuldashev/nova-money-field

A Laravel Nova field for handling monetary values with moneyphp/money. Store amounts as integers, format them by locale and currency, and edit money safely in your Nova resources.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up

composer require vyuldashev/nova-money-field

There is no config file and no migration — this is a pure Nova field. Setup is: install the package, drop the field into a resource, and make sure the underlying model attribute is shaped the way the field expects (integer minor units paired with a currency, by default).

Smallest working example — a single-currency price column stored as integer cents:

use Vyuldashev\NovaMoneyField\Money;

public function fields(NovaRequest $request)
{
    return [
        Money::make('Price')
            ->currency('USD')
            ->rules('required', 'numeric', 'min:0'),
    ];
}

That single field renders correctly across the index, detail, and form views — you do not need separate fields for each context.

Where to look first (in order):

  1. Your model. Before touching the package, decide what the attribute is: an int in minor units, a decimal string, or a Money object. The field only behaves predictably when this is consistent. This decision gates everything else.
  2. src/Money.php — the field is small. Read resolve() and fill() first; they define exactly how it converts between the model attribute and Money\Money. This is the ground truth when the README is ambiguous.
  3. The README "Usage" block — a quick sanity check on the method names for the version you pulled in (make, currency, locale).
  4. composer.lock — confirm the resolved moneyphp/money version; formatting and API differ subtly between major versions.

First use case to ship: a low-risk resource — a plan price, a product price, an invoice line — where the value is single-currency and never null. Prove the round-trip (form → save → re-open → index) before rolling out to refunds, payouts, or anything an operator could act on incorrectly.

Implementation Patterns

Integer minor units + a cast (the default shape)

Store cents in the database, expose a Money object to the field via an Eloquent cast:

// Migration
$table->unsignedBigInteger('price');      // cents
$table->string('price_currency', 3);      // 'USD'

// Model
protected $casts = [
    'price' => MoneyCast::class,
];
class MoneyCast implements CastsAttributes
{
    public function get($model, $key, $value, $attributes)
    {
        return new Money($value, new Currency($attributes[$key.'_currency']));
    }

    public function set($model, $key, $value, $attributes)
    {
        return [$key => $value->getAmount()];
    }
}

Register the cast once, use it on every monetary attribute, and the fields stay free of business logic. This is the pattern that survives Nova upgrades with the least churn.

Accessors instead of casts

If you do not want a cast, the accessor/mutator pair works identically from the field's perspective:

public function getPriceAttribute($value): Money
{
    return new Money($value, new Currency('USD'));
}

public function setPriceAttribute(Money $value): void
{
    $this->attributes['price'] = $value->getAmount();
}

Useful for a single legacy column; unhelpful once you have five of them.

Per-record currency

When currency varies per row, do not hardcode ->currency(). Resolve it from the record so the form and index display match the stored currency:

Money::make('Amount')
    ->currency(fn ($resource) => $resource->currency ?? 'USD');

Verify this against src/Money.php before relying on it — closure support is not documented in every version. If it is not there, the fallback is a small subclass of the field that overrides currency resolution.

Validation that matches the storage

The field does not generate validation rules. Add them explicitly and match the unit convention:

Money::make('Price')
    ->currency('USD')
    ->rules('required', 'numeric', 'min:0')
    ->rulesForIndex('sometimes');

A min:0 rule that assumes minor units is fine for prices but wrong for refunds (negatives). Be explicit per field.

Formatting outside Nova

Do not let display formatting fork. If your API or customer-facing views also render prices, put the formatting in a service or cast that both Nova and the app consume. The field should be the only place the money value object touches the admin layer — everything else flows through the cast.

Gotchas and Tips

The dominant risk is Nova version compatibility, not money correctness. Nova is closed-source with breaking major releases, and this field ships a frontend component that couples to Nova internals. A Nova upgrade can break the field silently — the form renders, but values do not round-trip. Pin Nova, the field, and moneyphp/money to exact versions, and add a smoke test that boots a Nova resource route and asserts the field renders and round-trips a value. That test is your early-warning system.

Decide minor vs. major units before any data exists. If the field stores minor units as integers, that is correct but locks in your schema. If it stores decimals, you inherit float and rounding defects. Confirm which by reading resolve() and fill() — do not assume the README matches the code for your version.

Silent 100× bugs happen when the field and the cast disagree. If the field treats the attribute as Money and your cast returns an int, you get a value off by a factor of 100 with no error. Keep one convention, documented in the team wiki, and enforce it in code review.

Locale and ICU version drift. IntlMoneyFormatter output depends on the ICU/locale data version. Display can differ across local, staging, and production. Pin the PHP image's ICU or accept cosmetic differences; do not treat them as data bugs.

Null, zero, and negatives are frequently untested upstream. Check each explicitly against your resource. Zero should render as $0.00, not blank. Null should render as an empty cell, not $0.00. Negatives should work for refunds and credits. Write a test for each.

Keep the field thin so a fork is cheap. The dependency is single-maintainer, zero dependents, and no visible release cadence. Wrap the field in your own subclass so swapping it is a one-file change, and leave business logic out of the Nova layer so a fork does not drag domain code with it. Half a day of work for a competent Laravel engineer is the fallback.

Multi-currency is a per-record concern, not a per-field one. Do not hardcode ->currency('USD') on a resource whose records carry their own currency column. Confirm closure support in the source; if it is absent, subclass the field to resolve currency from the resource.

Do not let the field define storage. If you are already on decimals or floats, the field will not fix that. Backfill to integer minor units + a currency column first, with a staging run against production-shaped data, then introduce the field and cast together.

Testing. One round-trip test (amount + currency) and one render test per resource is sufficient. It catches the two failure modes that matter: unit mismatch and frontend-component breakage on Nova upgrade.

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