singra / laravel-br-validation
Laravel bridge for singra/br-validation: validation rules, localised messages, Eloquent casts and Faker providers for CPF, CNPJ and CEP.
Requires
- php: ^8.3
- illuminate/contracts: ^12.0 || ^13.0
- illuminate/database: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
- illuminate/validation: ^12.0 || ^13.0
- singra/br-validation: ^1.0
Requires (Dev)
- fakerphp/faker: ^1.23
- larastan/larastan: ^3.10
- laravel/pint: ^1.30
- orchestra/testbench: ^10.0 || ^11.0
- pestphp/pest: ^4.7
- pestphp/pest-plugin-type-coverage: ^4.0
- phpstan/phpstan: ^2.2
- rector/rector: ^2.6
Suggests
- fakerphp/faker: Required for the fake()->cpf(), ->cnpj() and ->cep() factory providers.
This package is not auto-updated.
Last update: 2026-08-08 17:43:54 UTC
README
English · Português
The Laravel bridge for singra/br-validation:
validation rules with localised messages, Eloquent casts, and Faker providers
for CPF, CNPJ and CEP.
The document logic lives upstream and is not restated here. What this package
adds is the part that is genuinely Laravel's: a rule that speaks the user's
language, a cast that turns a column into a value object, and a fake()->cpf()
that survives its own validation.
composer require singra/laravel-br-validation
Laravel 12 and 13, PHP 8.3+. Auto-discovered — there is nothing to register.
Quick start
use Singra\LaravelBrValidation\Casts\AsCpf; use Singra\LaravelBrValidation\Rules\CpfRule; $request->validate([ 'document' => ['required', new CpfRule], 'postcode' => 'required|cep', ]); // app/Models/Holder.php protected function casts(): array { return ['document' => AsCpf::class]; } $holder->document; // Singra\BrValidation\Documents\Cpf $holder->document->formatted(); // '111.444.777-35' $holder->document->redacted(); // '***.444.777-**'
Rules
Each document has a rule object and a string rule. They are the same rule — same verdict, same message — and the test suite asserts it rather than assuming it.
| Document | Rule object | String rule |
|---|---|---|
| CPF | Rules\CpfRule |
cpf |
| CNPJ | Rules\CnpjRule |
cnpj |
| CEP | Rules\CepRule |
cep |
'document' => ['required', new CpfRule], 'document' => 'required|cpf',
The rules carry the Rule suffix that Laravel's own do not, because the thing
being validated is already called Cpf. A controller that validates input and
then builds the value object needs both names in scope.
A value that is already a Cpf, Cnpj or Cep instance passes without being
re-checked. Holding one is proof the check digits were verified when it was
built, so validating it again would only re-derive a fact already established.
null
Without nullable, null fails — Laravel's convention is that nullable is
what grants permission to omit an answer, and it skips the rule outright when
present.
'document' => 'nullable|cpf', // null passes 'document' => 'cpf', // null fails
Integers
An integer is refused rather than read as a string.
'document' => 'cpf', // 12345678909 as a JSON number → fails
012.345.678-90 is a valid CPF, and out of an INT column or a JSON number it
arrives ten characters long. Repairing that belongs at the boundary where the
data was damaged, not in a rule that would then appear to vouch for a value it
guessed at.
Messages
Shipped in English and Brazilian Portuguese, and rendered in the application's current locale. Three messages per document, not five.
| Reason | cpf message (en) |
|---|---|
WrongLength |
The document field must have 11 digits. |
IllegalCharacter |
The document field must contain digits only. |
RepeatedCharacters InvalidCheckDigit UnallocatedRange |
The document field must be a valid CPF. |
Only length and alphabet tell the person at the keyboard something they can act
on. "The check digits do not match" and "every character is the same" both mean,
to them, an invalid CPF — which is why Brazilian government portals render them
the same way. Code that needs the distinction should catch InvalidDocument and
read its reason, exactly as the upstream README describes.
CEP is the one where the collapsed message earns its own wording: with no check digit, a rejection means the code belongs to no block Correios ever allocated. It does not exist rather than fails, and the message says so.
Overriding a message
Everything Laravel already offers works, in its usual order of precedence:
$request->validate( ['document' => 'required|cpf'], ['document.cpf' => 'Informe um CPF válido.'], );
To change them globally, publish the language files:
php artisan vendor:publish --tag=br-validation-lang
That writes lang/vendor/br-validation/{en,pt_BR}/validation.php. Add a locale
by dropping a directory beside them; nothing else needs changing.
:attribute is left in the message for Laravel to substitute, so
validation.attributes and custom attribute names apply as they do anywhere
else.
Casts
use Singra\LaravelBrValidation\Casts\AsCep; use Singra\LaravelBrValidation\Casts\AsCnpj; use Singra\LaravelBrValidation\Casts\AsCpf; protected function casts(): array { return [ 'document' => AsCpf::class, 'company' => AsCnpj::class, 'postcode' => AsCep::class, ]; }
Stored unpunctuated — eleven characters for CPF, fourteen uppercase for CNPJ,
eight for CEP. Punctuation stays a display decision made on the way out, so a
CHAR(11) column never receives a mask by accident.
Writing accepts either a string or the value object, and validates both:
$holder->document = '111.444.777-35'; // stored as '11144477735' $holder->document = Cpf::from('11144477735') // likewise $holder->document = null; // empty column $holder->document = '11144477730'; // throws InvalidDocument
Reading a row that does not validate
It throws. This is the same bargain Laravel's own enum cast strikes with
ValueError, and it is deliberate. Returning null would be indistinguishable
from an empty column, so a bad legacy row would read as missing data — and the
day you noticed would be the day something had already overwritten it.
If a table holds documents entered before anything validated them, do not cast
the column. Cpf::mask() renders those without vouching for them:
Cpf::isValid('11144477700'); // false Cpf::mask('11144477700'); // '111.444.777-00' Cpf::redact('11144477700'); // '***.444.777-**'
CEP is the likeliest to trip on legacy data, because its validation includes the Correios allocation table: a code vacated by the Rondônia renumbering was a real CEP when the row was written and is not one now.
Faker
fake()->cpf(); // '11144477735' fake()->cnpj(); // '11222333000181' fake()->cep(); // '01310100' fake()->cpf(formatted: true); // '111.444.777-35' fake()->cpf(region: FiscalRegion::Eighth); fake()->cnpj(alphanumeric: true); // 'K9VWWFYR000134' fake()->cep(uf: Uf::PE);
Registered for every locale, so fake() works whatever app.faker_locale is
set to. Generated documents pass this package's own rules — a generated CEP is
drawn from the allocation table rather than being eight random digits.
Two differences from Faker's built-in pt_BR provider, which this one
shadows:
- Output is unpunctuated by default, where Faker's
cpf()andcnpj()default to formatted. A generated document is nearly always on its way into a column. Passformatted: truefor the mask. cnpj()can generate the post-2026 alphanumeric format, andcep()exists.
Output is reproducible under fake()->seed(). Faker seeds mt_srand() while
the generators upstream take a Random\Randomizer, and the package bridges the
two so that a seeded suite can reproduce its own failures. That bridge is for
fixtures only; Cpf::generate() with no argument uses the CSPRNG.
Not included
- A config file. There is nothing to configure. Messages are overridden by publishing the language files, which is the mechanism Laravel already has.
- Blade directives and
Strmacros.$holder->document->formatted()is already a method call on a value object; wrapping it in@cpf()adds a name to learn and a thing to maintain without removing a step. - Form request base classes, middleware, or a facade. Nothing here needs state or a container binding beyond the three rules.
- RG, Inscrição Estadual, and network lookups. Excluded upstream, for reasons the base README gives.
Versioning and support
Strict SemVer. The previous major receives bug fixes and security fixes for 12 months after its successor ships, then is archived.
Development
composer test # Pest composer test:coverage # 100% line coverage, enforced composer test:mutate # mutation testing, ≥85% composer analyse # PHPStan, level max, src and tests composer lint # Rector then Pint, writing composer lint:test # both, dry run composer check # lint:test, analyse, test:types, test:coverage
Coverage needs Xdebug or PCOV. Tests run against a real Laravel application via Orchestra Testbench, and the casts through a real Eloquent write and read — the interesting failures live in the round trip.
The one rule worth knowing before changing anything: rules, casts and the string
rules all reduce to Support\Document. That single seam is why the three
spellings cannot drift into disagreeing about what a CPF is, and the suite
asserts the agreement rather than trusting it.
License
MIT. See LICENSE.