Desafio PHP

Value Object CPF com validação e readonly

Enunciado

Implemente um Value Object Cpf como readonly class, recebendo a string no construtor. A propriedade valor deve ser privada e acessível via método valor(): string. No construtor, valide que a string tem 11 dígitos numéricos (use preg_match('/^\d{11}$/', $cpf)); se não, lance InvalidArgumentException. Implemente também equals(Cpf $outro): bool comparando os valores. Crie um try/catch demonstrando um CPF válido e um inválido.

Requisitos

  • A classe deve ser readonly class com propriedade private string $valor.
  • O construtor deve lançar InvalidArgumentException se o formato não casar com /^\d{11}$/.
  • Deve existir valor(): string e equals(Cpf $outro): bool.
  • O programa deve capturar a exceção do CPF inválido e imprimir a mensagem.

Código inicial

<?php

declare(strict_types=1);

readonly class Cpf
{
    // TODO: propriedade privada, validação, valor() e equals()
}

try {
    $a = new Cpf('12345678901');
    $b = new Cpf('12345678901');
    echo $a->valor(), PHP_EOL;
    echo $a->equals($b) ? 'iguais' : 'diferentes', PHP_EOL;
    new Cpf('123');
} catch (InvalidArgumentException $e) {
    echo 'Erro: ', $e->getMessage(), PHP_EOL;
}

Saída esperada

12345678901
iguais
Erro: CPF inválido
Ver dica

No construtor, valide antes de atribuir. Use preg_match para checar o formato e lance InvalidArgumentException com mensagem clara.

Mostrar solução
<?php

declare(strict_types=1);

readonly class Cpf
{
    // privado: ninguém de fora mexe direto no valor
    private string $valor;

    public function __construct(string $cpf)
    {
        // Valida antes de atribuir; readonly só aceita uma escrita
        if (!preg_match('/^\d{11}$/', $cpf)) {
            throw new InvalidArgumentException('CPF inválido');
        }
        $this->valor = $cpf;
    }

    public function valor(): string
    {
        return $this->valor;
    }

    public function equals(Cpf $outro): bool
    {
        return $this->valor === $outro->valor();
    }
}

try {
    $a = new Cpf('12345678901');
    $b = new Cpf('12345678901');
    echo $a->valor(), PHP_EOL;
    echo $a->equals($b) ? 'iguais' : 'diferentes', PHP_EOL;

    new Cpf('123'); // dispara a exceção
} catch (InvalidArgumentException $e) {
    echo 'Erro: ', $e->getMessage(), PHP_EOL;
}

Passo a passo

  1. readonly class Cpf garante que a instância não mude depois de criada.
  2. private string $valor impede acesso direto de fora; só o método valor() expõe.
  3. No construtor, preg_match valida exatamente 11 dígitos; se falhar, lançamos InvalidArgumentException.
  4. A atribuição $this->valor = $cpf; ocorre depois da validação, pois readonly só permite uma escrita.
  5. valor() retorna a string encapsulada, mantendo o acesso controlado.
  6. equals() compara dois CPFs pelo valor, não por identidade de objeto.
  7. No bloco try, criamos dois CPFs iguais, comparamos e depois tentamos criar um inválido.
  8. O catch captura a exceção e imprime a mensagem.

Por que funciona

Value Objects representam um valor e devem ser imutáveis: dois CPFs com a mesma string são equivalentes, independentemente de serem o mesmo objeto. A visibilidade private esconde o dado bruto e força o uso de valor(), enquanto readonly impede mutações posteriores. A validação no construtor garante que nenhuma instância inválida exista.

Erros comuns

  • Atribuir antes de validar: $this->valor = $cpf; if (...) throw ... — a propriedade readonly já foi escrita e o objeto inválido pode escapar.
  • Usar == em vez de === em equals: pode dar falso positivo com strings numéricas.
  • Esquecer o use InvalidArgumentException ou o \ inicial: em namespace, a classe não é encontrada.
  • Marcar valor como public readonly e achar que ainda há encapsulamento — quem tem o objeto lê direto, sem passar pelo método.

Outra forma de resolver

Uma alternativa é usar static factory com fromString:

public static function fromString(string $cpf): self
{
    return new self($cpf);
}

Útil quando você quer um nome mais expressivo que new Cpf(...) e centralizar a validação num único ponto.

Saída esperada

12345678901
iguais
Erro: CPF inválido