ariaieboy/filament-currency
A Filament PHP package for Laravel that provides currency form fields and formatting helpers. Easily handle locale-aware monetary input and display in Filament resources, forms, and tables.
This package gives Filament forms a currency-aware input that formats values for humans while keeping the stored value machine-friendly (minor units). The current line is 3.x; 3.0.2 is a patch release on that line — it fixes the currency mask echoing programmatic updates back to Livewire (PR #58). There are no API changes, no new config keys, and no deprecations. If you're on 3.0.0/3.0.1, upgrading is a composer update.
Minimal setup:
composer require ariaieboy/filament-currencyphp artisan vendor:publish --tag="filament-currency-config"use Ariaieboy\FilamentCurrency\Forms\Components\CurrencyInput; // verify namespace against the README
CurrencyInput::make('price')
->currency('USD')
->required();
First use case worth doing: take a single price field on your most-edited resource, back it with an integer price_cents column, and verify the round-trip (edit → save → reload) before rolling it out anywhere else. The whole value of the package is in how it converts between displayed decimals and stored minor units, so prove that path first.
Where to look first: the published config (default currency, locale, allowed currencies), then the field class itself to see its formatStateUsing/dehydrateStateUsing behaviour. That pair is where every surprise lives.
Store minor units, always. Treat the field as a presentation-layer widget over an integer column. In the model, use a money cast (the package builds on the Akaunting/MoneyPHP family) rather than hand-rolling $value * 100 accessors:
protected function casts(): array
{
return ['price' => \Akaunting\Money\Money::class . ':USD'];
}
Per-record currency. For multi-currency resources, make the currency dynamic rather than hardcoded:
CurrencyInput::make('price')
->currency(fn (?Model $record) => $record?->currency ?? 'USD');
Pair it with a Select for currency and ->live() so the symbol updates when the user switches. Avoid letting currency change after the amount is entered without an explicit confirmation — silently reinterpreting 100 JPY as 100 USD is a data-integrity bug, not a UX quirk. On 3.0.2+ this reactive pairing is reliable; on older releases the mask could echo state back and fight your select (see Gotchas).
Programmatic state is now safe to set. ->state(), fillForm(), afterStateUpdated, and dependent-select updates no longer bounce back through the mask into Livewire. That means you can freely drive the field from bulk actions, "duplicate record", or a calculateTotal() reactive callback without guarding it.
Table and infolist display. Don't reuse the form field for output. Format in the column:
TextColumn::make('price')
->money('USD', divideBy: 100) // if stored in minor units
->sortable();
divideBy is the single most common source of "why is everything 100x" bugs. Decide once per column whether the value is minor or major units and be consistent.
Repeaters and relation managers. The field works inside repeaters, but each row needs its own currency context. Pass ->currency(fn (array $state) => $state['currency'] ?? 'USD') from the sibling select, or you'll format every row with the panel default.
Validation. ->rules() and ->minValue() see the stored value. If you store minor units and want "at least $1.00", the rule is min:100, not min:1. Do the multiplication in code or assert on the dehydrated state, not on what the user typed.
Testing. Assert against the dehydrated/form state, not the displayed string — assertFormSet compares raw state. For Livewire form tests, fill with the human format and assert the saved integer:
Livewire::test(ProductForm::class)
->fillForm(['price' => '19.99'])
->call('save')
->assertHasNoFormErrors();
$this->assertSame(1999, Product::first()->price_cents);
_cents columns or decimal columns, never both.->live(false), ->debounce(), a wire:ignore wrapper, or a manual afterStateHydrated, remove those hacks when you upgrade — they'll now mask the real behaviour.$1,234.56 as 1.234,56 $. Set it per panel deliberately rather than inheriting whatever the server's intl default is.config:cache changes to the published file need a re-cache. If defaults seem ignored in production, this is why.brick/money or raw MoneyPHP, you'll get duplicate helper functions and confusing casts. Standardise on the one the package ships with.->view() for custom markup, and use ->formatStateUsing() for one-off display tweaks. Prefer a small custom field class over repeating the same three chained calls across a dozen resources.sortable() behaves as expected on a 100x-scaled integer rather than assuming.How can I help you explore Laravel packages today?