a21ns1g4ts/filament-brl-money-field
A Filament form field for Brazilian Real (BRL) monetary values, offering currency masking, formatting, and smooth integration with Filament forms. Makes entering and displaying amounts in Brazilian currency simple in Laravel admin panels.
Install the package and confirm compatibility before writing any form code:
composer require a21ns1g4ts/filament-brl-money-field
Then, in order:
composer.json for the supported Filament (^3 vs ^4), Laravel, Livewire, and PHP constraints. The field APIs differ between Filament majors — do not assume v1.0.1 supports yours.src/ for the exact field class namespace and available fluent methods (prefix, suffix, mask, dehydrateStateUsing, etc.). The README is thin and this package has one star, so treat the source as the source of truth.php artisan vendor:publish --tag="filament-brl-money-field-config"
bigInteger) or decimal(15,2) — never float.First use case — a resource form field with a matching model cast:
use A21ns1g4ts\FilamentBrlMoneyField\Fields\BrlMoneyField;
BrlMoneyField::make('price')
->label('Preço')
->required();
protected $casts = [
'price' => 'integer', // stored in cents
];
Smoke-test with three inputs — 0, 1.234,56, and a negative like -10,00 — then check both the Livewire state and the raw DB value. That single test surfaces most mask/persistence bugs.
v1.0.1 is a patch release with no published notes beyond a diff link. Pin to
^1.0, review thev1.0.0...v1.0.1diff once, and treat upgrades as bug-fix-only.
Field in form, standard Filament in table
Keep editing localized to the package and display with core Filament so exports/sorting behave normally:
// Form
BrlMoneyField::make('price')->required();
// Table
TextColumn::make('price')
->money('BRL', divideBy: 100)
->sortable();
Normalize at the dehydration boundary — only once
If the field already dehydrates to cents, do nothing. If it doesn't, convert explicitly and verify against source first to avoid double-multiplying:
BrlMoneyField::make('price')
->dehydrateStateUsing(function ($state) {
$normalized = str_replace(['.', ','], ['', '.'], (string) $state);
return (int) round(((float) $normalized) * 100);
});
Reactive Livewire forms
When another field depends on the amount, use live() + afterStateUpdated(), normalizing before arithmetic:
BrlMoneyField::make('price')
->live()
->afterStateUpdated(function ($state, Set $set) {
$cents = /* normalize $state */;
$set('tax', (int) round($cents * 0.1));
});
Custom validation via normalized value
Standard numeric / decimal rules reject "1.234,56", so validate the converted value:
BrlMoneyField::make('price')
->rules([
fn () => function ($attribute, $value, $fail) {
if (/* normalized cents */ < 0) {
$fail('O valor não pode ser negativo.');
}
},
]);
Feature-test through Livewire, assert the DB
Skip unit-testing the mask itself. Exercise the real form flow and assert persistence:
Livewire::test(CreateProduct::class)
->fillForm(['price' => '1.234,56'])
->call('create')
->assertHasNoFormErrors();
$this->assertDatabaseHas('products', ['price' => 123456]);
Integration with a decimal(15,2) column
If you can't use cents, pick decimal strings consistently — field dehydrates to "1234.56", model casts to decimal:2, and the table uses money('BRL') without divideBy. Do not mix conventions across resources.
. = thousands, , = decimal. Browser input, Livewire state, validation, and DB each may see a different representation. Log::info($state) during development until you're confident.float. Use integer cents or decimal. Convert once at dehydration and never again.dehydrateStateUsing() multiplies by 100 twice — a silent 100x bug.composer.json and don't assume compatibility.$refresh, dependent field updates, or partial re-renders, confirm the formatted value survives. Test with ->call('$refresh').numeric/decimal rules reject the masked string. Either normalize in a custom rule or validate a separate hidden raw field.ext-intl and app locale. Formatting may depend on intl or pt_BR locale. Behavior can differ between local and production — verify PHP extensions on the server.0,00, -10,00, empty input, and a value with only whitespace. Mask libraries sometimes swallow the minus sign or coerce empty to 0.decimal(15,2), dehydrate to a decimal string, not cents. If bigInteger, dehydrate to cents only. Enforce with a model cast and keep all resources consistent.prefix, suffix, or protected methods before subclassing. Subclassing a Filament field is brittle across minor releases — prefer config or formatStateUsing() over overriding internals.TextInput with a mask + a single dehydrateStateUsing() — often less risk than an unmaintained dependency.How can I help you explore Laravel packages today?