stonkeep/cpf-cnpj
Validate, format, and generate Brazilian CPF and CNPJ documents in Laravel. Includes ready-to-use validation rules, mask and formatting helpers, and clean value objects so you can handle tax IDs consistently across your application.
Install and register
composer require stonkeep/cpf-cnpj
php artisan vendor:publish --provider="Stonkeep\CpfCnpj\FieldServiceProvider" --tag=assets # Nova 3 only
In Nova 4+ fields are plain classes — no service provider or asset publish step is needed unless the package ships custom JS you must copy. Check vendor/stonkeep/cpf-cnpj/src for a FieldServiceProvider; if it exists and is not auto-discovered, add it to config/app.php.
First use case — a "document" column that accepts either a CPF (11 digits) or CNPJ (14 digits), masks it in the UI, and stores digits only:
use Stonkeep\CpfCnpj\CpfCnpj;
public function fields(Request $request)
{
return [
ID::make()->sortable(),
Text::make('Name', 'name')->rules('required'),
CpfCnpj::make('Document', 'document')
->rules('required', 'max:18')
->help('CPF or CNPJ'),
];
}
Where to look first
src/*.php) — confirm the exact make() signature, the component string, and whether it exposes mask/format helpers.resources/js/components/Field.vue (or dist/js/field.js) — this is where the masking happens, and where you find out whether it emits formatted or raw values.composer.json for the nova constraint — this tells you if it supports Nova 4/5 at all.Before adopting, note the repo has no stars and no dependents. Read the ~100 lines of source before wiring it into a production resource; if the field is thin, copying it into app/Nova/Fields is often cheaper than owning a dependency.
Separate storage from presentation. The single most common pattern is: mask in the UI, digits in the database.
// Model
public function setDocumentAttribute($value): void
{
$this->attributes['document'] = preg_replace('/\D/', '', (string) $value);
}
public function getDocumentAttribute($value): string
{
return $value; // raw digits; mask at the presentation layer
}
Then on the Nova index, format for readability:
CpfCnpj::make('Document', 'document')
->displayUsing(fn ($value) => $this->formatDocument($value))
->onlyOnIndex(),
If the field ships a ->mask() / ->formatted() fluent method, prefer it over displayUsing so index, detail, and form stay consistent.
Server-side validation is your responsibility. Nova fields are Vue components; a JS mask is not validation. Add a rule (Laravel 10+ Rule object or laravel-validation-rules/cpf-cnpj if the package doesn't ship one):
CpfCnpj::make('Document')->rules([
'required',
function ($attribute, $value, $fail) {
$digits = preg_replace('/\D/', '', (string) $value);
if (! in_array(strlen($digits), [11, 14], true) || ! $this->isValidCpfCnpj($digits)) {
$fail('Invalid CPF/CNPJ.');
}
},
]),
Two-field vs one-field designs. If the package exposes distinct Cpf and Cnpj fields, model them as two nullable columns and show the right one conditionally:
Select::make('Person Type')->options(['pf' => 'Individual', 'pj' => 'Company']),
Cpf::make('CPF')->rules('required_if:person_type,pf')->hideFromIndex(),
Cnpj::make('CNPJ')->rules('required_if:person_type,pj')->hideFromIndex(),
A single polymorphic field is friendlier for data entry but pushes the pf/pj decision into your validation logic.
Uniqueness and search. ->rules('unique:customers,document') works only if you store digits consistently — a masked value in the column will silently defeat both the unique rule and Nova's ->searchable() LIKE query.
Legacy imports. Use fillUsing to normalise rows coming from spreadsheets where the value may arrive as 123.456.789-09, 12345678909, or an Excel float:
CpfCnpj::make('Document')->fillUsing(function ($request, $model, $attribute) {
$model->{$attribute} = preg_replace('/\D/', '', (string) $request->input($attribute));
}),
dd() the raw column. If the component emits 123.456.789-09, either strip it in a mutator or file a bug. This is the #1 failure mode for mask-based packages.Rule.01234567890 are valid. Anything that casts to int or uses (int) $value will destroy them — keep the column string/char(14) and never (int) cast.[A-Z0-9]{12}\d{2}, so a package that hasn't been updated will start rejecting legitimate new companies. Verify the regex in the source.FieldServiceProvider + Nova::script()) break on Nova 4's Vite-bundled asset pipeline. Check the composer.json constraint against your nova requirement before installing; a mismatch usually shows as a blank form field with no console error.component name collisions. If two installed packages register a component named cpf-cnpj-field, one silently wins. Grep resources/js for the component string in the field class.->searchable() generates WHERE document LIKE '%...%'; a user typing digits will never match a masked column. Either store digits or override searchableColumns().input event payload — that tells you instantly what reaches the server. Alternatively Log::info($request->all()) in the resource's store path.component() or add fluent setters of your own; publish views with php artisan vendor:publish --tag=cpf-cnpj-views if templates are exposed. Because the codebase is small and unmaintained, forking the field into app/Nova/Fields (and crediting the original) removes upgrade risk entirely — for a field this small it is usually the better long-term call than pinning an abandoned dependency.How can I help you explore Laravel packages today?