ymsoft/filament-money
Filament Money provides currency-aware money input fields and table columns for Laravel Filament admin panels, making it easy to enter, format, and display monetary values consistently with locale-friendly output.
Install
composer require ymsoft/filament-money
php artisan vendor:publish --tag="filament-money-config" # if a config is shipped
Filament auto-discovers the service provider. If the package registers views/assets, add it to your panel:
// app/Providers/Filament/AdminPanelProvider.php
public function panel(Panel $panel): Panel
{
return $panel
->plugin(\Ymsoft\FilamentMoney\FilamentMoneyPlugin::make());
}
Where to look first
config/filament-money.php — default currency, decimal separator, and the minor-unit exponent live here. Set the default currency before writing any field, because it silently becomes the currency for every field that doesn't override it.src/ tree — at ~10 stars and 0 dependents there is essentially no community knowledge base. The source is the documentation. Read the form component, the table column, and the cast (three or four files) before designing your schema.composer.json — confirm the Filament major version constraint. A plugin released in 2026 is almost certainly Filament v4 (Filament\Schemas), and the v3/v4 namespace split is the most common install failure.First use case: a price on a product
The package's whole value proposition is that it stores money as integer minor units (cents) in the database while the Filament form/table speaks major units (dollars) to the user. Wire up the three pieces together:
// migration
$table->unsignedBigInteger('price'); // 1999, not 19.99
$table->char('currency', 3)->default('USD');
// app/Models/Product.php
protected $casts = [
'price' => \Ymsoft\FilamentMoney\Casts\MoneyCast::class,
];
// app/Filament/Resources/ProductResource.php
use Ymsoft\FilamentMoney\Forms\Components\MoneyInput;
use Ymsoft\FilamentMoney\Tables\Columns\MoneyColumn;
MoneyInput::make('price')
->currency('USD')
->required(),
MoneyColumn::make('price')
->currency('USD'),
Verify the exact class names against the repo's src/ — with this few stars the README may lag the code, and the namespaces are the one thing you cannot guess cheaply.
One currency per record, not per field. If your model can hold more than one currency, store it in a column and drive the components from the record instead of hard-coding:
MoneyInput::make('price')
->currency(fn (?Product $record) => $record?->currency ?? 'USD'),
MoneyColumn::make('price')
->currency(fn (Product $record) => $record->currency),
Hard-coded ->currency('EUR') on a field whose rows are in USD is the single most common source of "the number is right but the symbol is wrong" bugs.
Keep the cast at the persistence boundary. The idiomatic layering is: form component → MoneyInput state (major units, string) → cast → integer minor units in the DB. Do your arithmetic in the domain layer with a real money library (moneyphp/money, brick/money), not with the cast's return value:
$total = $order->lines->reduce(
fn (Money $c, Line $l) => $c->add(new Money($l->price_minor, new Currency($order->currency))),
new Money(0, new Currency($order->currency)),
);
MoneyCast is a storage/formatting concern; it is not a calculator.
Bulk columns and summaries. Table columns usually delegate formatting to Filament. If MoneyColumn supports summarize(), remember that sum() and avg() are computed by the database over the integer column and returned as a plain int — the cast does not apply. Either accept raw cents output or format the summary yourself:
MoneyColumn::make('price')
->summarize(
Summarizers\Sum::make()
->formatStateUsing(fn ($state) => '$'.number_format($state / 100, 2))
),
Filters need conversion in both directions. A ->query() closure receives what the user typed in major units:
Filter::make('price')
->form([MoneyInput::make('min')])
->query(fn (Builder $q, array $data) => $q->when(
$data['min'] ?? null,
fn ($q, $v) => $q->where('price', '>=', (int) round($v * 100)),
)),
Repeaters and nested state. MoneyInput inside a Repeater inherits the repeater's state path, so casting/formatting hooks that assume a flat price key break. Prefer calling the component's public API rather than overriding afterStateHydrated yourself.
Testing. Livewire form tests fill major units and assert against minor units:
livewire(CreateProduct::class)
->fillForm(['price' => '19.99'])
->call('create')
->assertHasNoFormErrors();
$this->assertDatabaseHas('products', ['price' => 1999]);
Writing 1999 into fillForm is the classic mistake — the component will treat it as $1,999.00.
Exports, APIs, and seeders bypass the plugin entirely. They see the raw integer. Document that price is minor units in your API resource definitions and cast explicitly there; don't assume MoneyCast travels with toArray().
Filament\Schemas; a v3-only plugin will install fine and then throw Class not found on the first form render. A 2026 release date is a good sign, but confirm.(int) ($state * 100), you get 1998 for 19.99 on some inputs. Round only at the boundary and prefer string/decimal math./ 100 is not universal. JPY has 0 decimal places, KWD/BHD have 3. If the package reads the exponent from config or a currency table, use that; hard-coded 100 produces wrong values on non-2-decimal currencies. If it doesn't, this is your first extension point.unsignedBigInteger is right; unsignedInteger caps you at ~$42.9M in cents and overflows silently in MySQL's non-strict mode.bigInteger for anything that isn't strictly a price... Users on a , locale will type 19,99 and either get a validation error or, worse, 1999. Verify with an actual browser in your target locale, not just a Livewire test.string ("19.99") while a freshly-loaded record's state is int (1999) if the cast is missing from the model. If the input shows 19.99 on edit but 1999.00 on create, your cast is on the wrong model or the field name doesn't match the column.Model::sum('price'), withSum(), and Filament summarizers all return raw integers. Any formatter you attach to a column does not run on them.default_currency read at boot is cached by config:cache. Changing it in .env during a deploy without php artisan config:clear silently keeps the old symbol.->view() override (fastest way to fix a formatting/markup quirk without a fork), the config file for currency/format defaults, and a thin custom cast wrapping the package's cast if you need multi-currency attributes. With 10 stars and 0 dependents, fork-and-patch is a legitimate strategy — the surface is small, and you will not find Stack Overflow answers. Pin the version in composer.json and note the fork in your README.dd($livewire->data) show before save? That sequence resolves almost every issue with this class of package.How can I help you explore Laravel packages today?