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

Filament Ptbr Form Fields Laravel Package

leandrocfe/filament-ptbr-form-fields

Brazilian-ready form fields for Filament: CPF/CNPJ, CEP, phone, and currency inputs with masks, validation, and PT-BR labels. Drop-in Laravel components to speed up admin panels and forms for Brazil.

View on GitHub
Deep Wiki
Context7

Getting Started

leandrocfe/filament-ptbr-form-fields is a small Filament plugin that swaps generic TextInputs for pt-BR–aware fields (CPF, CNPJ, CPF/CNPJ, CEP, phone, currency/BRL) with input masks and validation baked in. It is a convenience layer, not a full localization package.

composer require leandrocfe/filament-ptbr-form-fields

No panel registration or config publish is required — the components are ordinary Filament form fields resolved by class name. First place to look is the README's field table (class names + supported chainable methods), then the src/Filament/Forms/Components directory if a method is undocumented.

First use case — replace a plain document input in a Resource form:

use Leandrocfe\FilamentPtbrFormFields\Filament\Forms\Components\CpfCnpj;

CpfCnpj::make('document')
    ->label('CPF/CNPJ')
    ->required()
    ->unique(ignoreRecord: true),

Then run the form once in the browser, submit an invalid and a valid document, and inspect the Livewire payload to see what actually lands in $state — that single check answers most of the questions you'll have later.

Version note for 5.0.1: this is a patch release (dependency bumps plus removal of the illuminate/contracts constraint and added Laravel 13.x compatibility). It is a drop-in upgrade from 5.0.0; the 5.x line targets Filament 4. If you are still on Filament 3, stay on the 4.x package line.

Implementation Patterns

Normalize on dehydrate, mask on display. The mask lives in the UI; your database should store digits only. Do the conversion at the field boundary so models, policies, and API resources never see punctuation:

Cpf::make('cpf')
    ->required()
    ->dehydrateStateUsing(fn (?string $state) => preg_replace('/\D/', '', (string) $state))
    ->rule('digits:11');

Validation belongs on the unmasked value. Filament runs rules against the dehydrated state in most flows, but if you also validate on the client/Livewire side, prefer digits: or a dedicated Rule object over regex patterns written for the masked format. If you need both, add a ->rule() for the raw digits and let the component handle formatting.

CEP autofill is a two-field pattern, not a package feature. Wire the lookup yourself with afterStateUpdated, keeping it debounced and guarded:

Cep::make('zip')
    ->live(onBlur: true)
    ->afterStateUpdated(function (?string $state, Set $set) {
        $digits = preg_replace('/\D/', '', (string) $state);
        if (strlen($digits) !== 8) return;

        $data = Cache::remember("cep:$digits", now()->addDay(),
            fn () => Http::timeout(3)->get("https://viacep.com.br/ws/{$digits}/json/")->json());

        if (empty($data) || ($data['erro'] ?? false)) return;

        $set('street', $data['logradouro']);
        $set('district', $data['bairro']);
        $set('city', $data['localidade']);
        $set('state', $data['uf']);
    });

Currency: store cents, cast on read. Treat the currency field as a formatted string in the form and an integer (or decimal:2) in the database:

Currency::make('price')
    ->dehydrateStateUsing(fn ($state) => (int) round(
        (float) str_replace(['.', ','], ['', '.'], (string) $state) * 100
    ));

and in the model: protected $casts = ['price' => 'integer']; with an accessor if you need R$ 1.234,56 in the UI.

Wrap for house style. If several resources share the same CPF setup (label, uniqueness, normalization, helper text), subclass the package component once and reuse it. That also gives you a single place to change when the package bumps a major.

Testing. Use Filament's form test helpers, filling with the unmasked value and asserting on the persisted attribute:

livewire(CreateClient::class)
    ->fillForm(['document' => '12345678909'])
    ->call('create')
    ->assertHasNoFormErrors();

$this->assertDatabaseHas('clients', ['document' => '12345678909']);

If a test fails only when a masked value is filled, that's the mask/validation mismatch described below — fix the field, not the test.

Gotchas and Tips

  • Masked state leaking into persistence. The most common bug: cpf stored as 123.456.789-09 and then unique() or digits: failing on unrelated records. Always pair the field with dehydrateStateUsing stripping non-digits, and apply the same stripping in importers/seeders so both paths agree.
  • unique() on a masked column. ->unique() compares what the field produces. Dehydrate first, or the rule will query for the punctuated string.
  • Empty state is often '', not null. With ->nullable(), a cleared masked field can still dehydrate to an empty string, breaking nullable DB columns and unique semantics. Normalize with fn ($state) => filled($state) ? $state : null.
  • Client-side masks don't validate. A user can paste an invalid CPF that formats fine. Add a real validation rule (the package's own document validation if exposed in your version, otherwise a custom Rule with the check-digit algorithm).
  • CEP lookups fail in tests and queues. External HTTP in afterStateUpdated will hit the network in Livewire tests. Wrap in Cache::remember, guard with Http::timeout, and use Http::fake() in tests.
  • Asset/Alpine dependency. The masks rely on Alpine's mask plugin being loaded by Filament. Custom themes, strict CSP, or a heavily customized panel can break formatting silently — symptom is an unformatted but otherwise working field.
  • Floating point in money fields. Never cast a currency field's state directly to float and store it; round-trip through cents as integers.
  • Upgrade path. 5.0.1 is safe from 5.0.0. The dropped illuminate/contracts constraint means fewer version conflicts, but if you pinned that package for other reasons, re-run composer update --dry-run to confirm the resolution before deploying. Laravel 13.x support is claimed here — verify against your own laravel/framework constraint rather than assuming.
  • Debugging tip. When a field behaves oddly, dd($state) inside dehydrateStateUsing and inside afterStateUpdated: the value at those two points is usually different, and knowing which one your validation sees resolves most confusion.
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core