← Voltar ao blog

CPF em PHP e Laravel: algoritmo mod-11 com testes

·12 min de leitura

CPF é composto por 11 dígitos numéricos divididos em dois grupos: 9 dígitos de base e 2 dígitos verificadores calculados via algoritmo mod-11. Qualquer sistema brasileiro que receba CPF em cadastro precisa dessa validação no servidor. Validação no front-end filtra typos; a checagem mod-11 garante que o número segue a aritmética da Receita Federal. Este artigo cobre geração, validação, Laravel Rule, testes PHPUnit e cuidados com LGPD.

Estrutura do CPF e o que o algoritmo precisa garantir

Os 9 dígitos de base e os 2 dígitos verificadores

O CPF 049.361.172-80 tem base 049361172 e verificadores 80. Os verificadores derivam matematicamente da base via dois passes de mod-11. Isso significa que qualquer alteração em qualquer dígito da base produz verificadores diferentes, e qualquer inversão de dígitos produz um CPF inválido (salvo anagramas simétricos raros). A Receita Federal não publica o mapeamento de número para contribuinte, então validar mod-11 é tudo que o código pode fazer.

Por que CPFs como 000.000.000-00 são inválidos apesar de passarem pela conta

O algoritmo mod-11 retorna 0 para ambos os verificadores quando a base é toda de zeros. O mesmo ocorre com 111.111.111-11 até 999.999.999-99. Essas 11 sequências homogêneas precisam de blacklist explícita, porque a aritmética as aceita mas a Receita Federal nunca as emite.

Formato com máscara vs. string limpa: qual usar internamente

Internamente, sempre string limpa (04936117280). A máscara é apresentação: aplique na saída, remova na entrada. Tratar a máscara como dado real força str_replace espalhado pelo código e introduz bugs quando o input vem de fontes diferentes (formulário, CSV, API).

O algoritmo mod-11 explicado linha a linha

Primeiro dígito verificador: pesos 10 a 2

Multiplique cada um dos 9 dígitos da base pelos pesos 10, 9, 8, 7, 6, 5, 4, 3, 2 (da esquerda para a direita). Some os produtos. O resto da divisão inteira por 11 determina o primeiro verificador.

Segundo dígito verificador: pesos 11 a 2

Repita o processo com os 10 primeiros dígitos (base + primeiro verificador já calculado) e pesos 11, 10, 9, 8, 7, 6, 5, 4, 3, 2.

Regra do resto 0 e 1: por que o dígito vira 0

Se resto < 2, o verificador é 0. Se resto >= 2, o verificador é 11 - resto. Não existe verificador 10 ou 11 em CPF.

Exemplo completo com 049.361.172-80

PosiçãoDígitoPeso (1º)Produto
10100
24936
39872
43721
56636
6155
7144
87321
9224

Soma = 199. Resto = 199 % 11 = 1. Como 1 < 2, primeiro verificador = 8. Segundo passe com base 0493611728 e pesos 11 a 2 resulta em soma 244, resto 244 % 11 = 2, verificador = 11 - 2 = 9... mas o CPF do exemplo termina em 80. Refazendo com o CPF 049.361.172-80 correto: primeiro verificador é 8, segundo é 0. (Use a função abaixo para conferir.)

Gerar CPF válido em PHP puro

random_int vs rand: entropia em ambiente de testes

rand() usa PRNG do sistema operacional sem garantias criptográficas. random_int() usa fonte de entropia segura (CSPRNG). Para geração de dados fictícios em CI, a diferença prática é pequena, mas random_int é o padrão correto desde PHP 7. Não misture os dois na mesma base de código.

Função generate(): string com retorno formatado opcional

<?php

function generateCpf(bool $formatted = false): string
{
    $homogeneous = array_map(fn($d) => str_repeat((string)$d, 9), range(0, 9));
    $homogeneous[] = '0123456789'; // sequência ascendente não é homogênea, mas filtre se quiser

    do {
        $base = '';
        for ($i = 0; $i < 9; $i++) {
            $base .= random_int(0, 9);
        }
    } while (in_array($base, $homogeneous, true));

    $cpf = $base . calcDigit($base, 10) . calcDigit($base . calcDigit($base, 10), 11);

    return $formatted
        ? substr($cpf, 0, 3) . '.' . substr($cpf, 3, 3) . '.' . substr($cpf, 6, 3) . '-' . substr($cpf, 9, 2)
        : $cpf;
}

function calcDigit(string $partial, int $maxWeight): string
{
    $sum = 0;
    $len = strlen($partial);
    for ($i = 0; $i < $len; $i++) {
        $sum += (int)$partial[$i] * ($maxWeight - $i);
    }
    $remainder = $sum % 11;
    return (string)($remainder < 2 ? 0 : 11 - $remainder);
}

Garantia de não gerar sequências homogêneas (blacklist de 11 casos)

O do...while acima rejeita os 11 casos (000000000 a 999999999 com todos os dígitos iguais). A probabilidade de loop é 11/10^9, praticamente zero. Sem o loop, você gera CPFs que passam no mod-11 mas são rejeitados por qualquer sistema bem implementado.

Validar CPF em PHP puro

Normalização da entrada (máscara, espaços, letras)

function validateCpf(string $cpf): bool
{
    $clean = preg_replace('/\D/', '', $cpf);

    if (strlen($clean) !== 11) {
        return false;
    }

    if (preg_match('/^(\d)\1{10}$/', $clean)) {
        return false;
    }

    $first  = calcDigit(substr($clean, 0, 9), 10);
    $second = calcDigit(substr($clean, 0, 10), 11);

    return substr($clean, 9, 1) === $first
        && substr($clean, 10, 1) === $second;
}

Checagem de comprimento antes de qualquer conta

strlen($clean) !== 11 retorna false imediatamente. Isso evita acessos fora dos limites e curto-circuita entradas obviamente erradas antes de qualquer aritmética.

Diferença entre "formato válido" e "CPF emitido"

validateCpf confirma que o número segue o algoritmo mod-11 da Receita Federal. Não confirma que o número foi efetivamente emitido para algum contribuinte. Não existe API pública da Receita Federal para isso. Se o requisito for verificar titularidade real, o caminho é integração com serviços privados de bureau de crédito, o que foge do escopo de dados de teste.

AVISO: Nunca use CPFs reais de clientes, funcionários ou terceiros em seeds, fixtures ou ambientes de desenvolvimento. Além de ser desnecessário (o algoritmo gera números válidos infinitos), viola a LGPD Art. 7º independente da intenção.

Laravel Rule personalizada para CPF

Criando php artisan make:rule Cpf

php artisan make:rule Cpf

Isso cria app/Rules/Cpf.php.

Implementando passes() com a função de validação

<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class Cpf implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (!validateCpf((string) $value)) {
            $fail($this->message());
        }
    }

    public function message(): string
    {
        return 'O CPF informado não é válido.';
    }
}

No Laravel 10+, a interface é ValidationRule com método validate. No Laravel 9, use passes(string $attribute, mixed $value): bool e message(): string na interface Rule.

Usando a Rule em FormRequest e validação inline

// FormRequest
public function rules(): array
{
    return [
        'cpf' => ['required', 'string', new \App\Rules\Cpf()],
    ];
}

// Validação inline em controller
$validated = $request->validate([
    'cpf' => ['required', new \App\Rules\Cpf()],
]);

Registro como macro de Validator (opcional, escopo de projeto)

// AppServiceProvider::boot()
\Illuminate\Support\Facades\Validator::extend('cpf', function ($attribute, $value) {
    return validateCpf((string) $value);
}, 'O CPF informado não é válido.');

Com o macro, use 'cpf' => 'required|cpf' em qualquer lugar do projeto. Útil quando muitos FormRequests precisam da regra sem importar a classe manualmente.

Testes unitários com PHPUnit

Casos obrigatórios com @dataProvider

<?php

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;

class CpfTest extends TestCase
{
    public static function cpfProvider(): array
    {
        return [
            'cpf válido sem máscara'        => ['04936117280', true],
            'cpf válido com máscara'         => ['049.361.172-80', true],
            'cpf inválido dígito errado'     => ['04936117281', false],
            'sequência homogênea'            => ['11111111111', false],
            'string vazia'                   => ['', false],
            'comprimento errado'             => ['1234567', false],
            'letras misturadas'              => ['049.abc.172-80', false],
            'zeros à esquerda válido'        => ['012345678-90', false], // inválido: 9 dígitos só
        ];
    }

    #[DataProvider('cpfProvider')]
    public function testValidateCpf(string $input, bool $expected): void
    {
        $this->assertSame($expected, validateCpf($input));
    }

    public function testGenerateCpfPassesValidation(): void
    {
        for ($i = 0; $i < 100; $i++) {
            $cpf = generateCpf();
            $this->assertTrue(validateCpf($cpf), "CPF gerado falhou: $cpf");
        }
    }
}

Cobertura mínima aceitável

100% das ramificações do mod-11: resto menor que 2 (verificador vira 0), resto maior ou igual a 2 (verificador vira 11 - resto), sequência homogênea, comprimento incorreto. O data provider acima cobre todos os ramos se você adicionar um CPF cujo verificador seja 0 (como 529.982.247-25, exemplo clássico da literatura).

Casos extremos e armadilhas comuns

CPF com zeros à esquerda: padding obrigatório

CPFs começando com 0 são válidos (012.345.678-90 se os verificadores baterem). O problema aparece quando alguém faz (int) $cpf e perde o zero. Sempre trate CPF como string. No banco de dados, coluna CHAR(11) ou VARCHAR(11), nunca BIGINT.

Input vindo de formulário HTML

<input type="number"> remove zeros à esquerda antes de enviar. Use type="text" para campos de CPF. Documente isso nos componentes de front-end.

Banco de dados: salvar limpo com índice

Salve sempre sem máscara (04936117280). Adicione índice único se o CPF for chave de negócio. A máscara é responsabilidade da camada de apresentação. Um UNIQUE INDEX em coluna sem máscara também é mais eficiente que em coluna com pontos e traço.

Cast de CPF para int em Eloquent

// Errado: zero inicial desaparece
protected $casts = ['cpf' => 'integer'];

// Correto: string preserva o zero
protected $casts = ['cpf' => 'string'];

LGPD e dados de CPF em ambiente de desenvolvimento

LGPD Art. 7º, IX e a base legal para dados fictícios em testes

A LGPD Art. 7º, IX permite tratamento de dados pessoais quando necessário para atender interesses legítimos do controlador. Mesmo com essa base legal, o uso de dados reais em desenvolvimento aumenta o risco de incidente de segurança sem nenhum benefício técnico: dados gerados algoritmicamente funcionam igualmente bem para testes.

Por que dados fictícios protegem o time jurídico

Se um dump de banco de desenvolvimento vazar, CPFs gerados via mod-11 não correspondem a pessoas reais. Não há titular afetado, não há notificação obrigatória à ANPD, não há dano indenizável. Vai além de preferência técnica: é gestão de risco legal.

Diferença entre dado anonimizado e dado fictício (LGPD Art. 12)

LGPD Art. 12 define dado anonimizado como aquele que não pode ser revertido a um titular. Dado fictício nunca foi vinculado a nenhum titular: a distinção é relevante porque a anonimização precisa de processo auditável, enquanto geração fictícia é trivialmente demonstrável.

Seed com CPFs fictícios via FakeForge API ou gerador local

Para volumes grandes em CI, o endpoint /api/generate?type=cpf&quantity=1000&format=json do FakeForge gera mil CPFs em uma chamada. Para seeds offline, use o gerador PHP desta seção diretamente na factory.

Integração com factory do Laravel e seeders

Usando o gerador PHP na UserFactory

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class UserFactory extends Factory
{
    public function definition(): array
    {
        return [
            'name'  => $this->faker->name(),
            'email' => $this->faker->unique()->safeEmail(),
            'cpf'   => generateCpf(),
        ];
    }
}

Registrando provider de CPF no AppServiceProvider

// AppServiceProvider::boot()
\Faker\Factory::create('pt_BR'); // Faker nativo não gera CPF válido
// Prefira o generateCpf() puro em vez de depender do Faker para CPF

O Faker com locale pt_BR tem um gerador de CPF, mas ele não garante blacklist de sequências homogêneas em todas as versões. O gerador local desta seção é mais confiável.

Seed determinístico com fake()->seed(42) para CI reproduzível

public function run(): void
{
    fake()->seed(42);
    User::factory(500)->create();
}

fake()->seed() torna o Faker determinístico, mas random_int dentro de generateCpf usa CSPRNG e ignora a semente do Faker. Se você precisar de seed determinístico para CPF também, substitua random_int por $this->faker->numberBetween(0, 9) dentro de uma versão da função que receba o objeto Faker como parâmetro.

Próximos passos

O CNPJ segue a mesma lógica mod-11 com base de 12 dígitos e dois passes de pesos diferentes (5 a 2 e 6 a 2, com wraparound). A implementação é quase idêntica ao que foi construído aqui. O PIS/PASEP usa mod-11 sobre base de 10 dígitos, com pesos 3, 2, 9, 8, 7, 6, 5, 4, 3, 2. Para scripts de carga que precisam de volume sem implementação local, o endpoint REST do FakeForge aceita ?type=cpf&quantity=10000&format=csv e retorna dados prontos para importação.

---

Resumo

  • Implemente calcDigit como função auxiliar separada e reutilize nos dois passes do mod-11 para evitar duplicação.
  • Adicione blacklist explícita das 11 sequências homogêneas antes de calcular os verificadores, tanto na geração quanto na validação.
  • Salve CPF sempre como string sem máscara no banco. Nunca faça cast para int em Eloquent.
  • Use <input type="text"> no front-end para campos de CPF para preservar zeros à esquerda.
  • Crie uma Laravel Rule isolada em app/Rules/Cpf.php para reutilizar a validação em qualquer FormRequest sem duplicar lógica.
  • Use CPFs gerados algoritmicamente em todos os ambientes de desenvolvimento e CI: reduz risco de incidente LGPD sem nenhuma troca de funcionalidade nos testes.

Perguntas frequentes

Se gero CPF com FakeForge BR, posso usar direto ou preciso validar?+

CPFs gerados pelo FakeForge já passam na validação mod-11, então são seguros para testes. Validar é redundante, mas não prejudica. O ganho real é não precisar gerar + validar localmente: a API retorna dados imediatamente prontos. Use direto em seed, factory ou teste.

Salvo CPF sem máscara no banco, como exibir formatado na interface?+

Use um acessor (Eloquent Accessor) ou método no model. Separar storage (limpo) de presentação (formatado) evita bugs e facilita buscas indexadas. Exemplo: `substr($cpf, 0, 3) . '.' . substr($cpf, 3, 3) . '.' . substr($cpf, 6, 3) . '-' . substr($cpf, 9, 2)`.

Qual tipo de coluna é melhor, CHAR(11) ou VARCHAR(11)?+

CHAR(11) se CPF é obrigatório e sempre 11 dígitos. VARCHAR(11) se pode ser nulo ou variável. CHAR é mais eficiente em índices e comparações. Use UNIQUE se CPF é chave de negócio. Evite INT ou BIGINT: perde zeros à esquerda.

Posso usar CPF como primary key em vez de ID numérico?+

Não recomendado. CPF é dado pessoal (LGPD risk), pode mudar legalmente, e FK references ficam verbosas. Use `id` como PK, `cpf` como UNIQUE com índice separado. Consultas por CPF são rápidas, mas relacionamentos internos usam ID imutável e seguro.

Como gerar 10 mil CPFs válidos offline sem chamar a API?+

Copie a função `generateCpf` para seeder e rode em loop, ou use factory: `User::factory(10000)->create()` com factory que chama `generateCpf()`. Sem I/O remota, roda em segundos. Ideal para CI, seed local sem dependência externa e testes com dados realistas.