singra/laravel-br-validation

Laravel bridge for singra/br-validation: validation rules, localised messages, Eloquent casts and Faker providers for CPF, CNPJ and CEP.

Maintainers

Package info

github.com/singraworks/laravel-br-validation

pkg:composer/singra/laravel-br-validation

Transparency log

Statistics

Installs: 5

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-08-07 19:14 UTC

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:

  1. Output is unpunctuated by default, where Faker's cpf() and cnpj() default to formatted. A generated document is nearly always on its way into a column. Pass formatted: true for the mask.
  2. cnpj() can generate the post-2026 alphanumeric format, and cep() 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 Str macros. $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.