Artigo 40 — Boas Práticas e Código Limpo em PHP
Módulo 6 · Semana 40 · Nível: Intermediário–Avançado
Introdução
Boas práticas não são regras arbitrárias inventadas por puristas. São soluções destiladas de décadas de experiência coletiva com código que ficou difícil de manter, difícil de testar, e difícil de entender. Neste artigo reunimos os princípios mais impactantes para escrever PHP profissional: código que se lê como prosa, que resiste ao tempo e que você não se envergonha de mostrar.
Declare tipos em tudo
O sistema de tipos do PHP 8 é expressivo e poderoso. Use-o:
<?php
declare(strict_types=1); // Sempre — ativa verificação estrita de tipos
// Ruim: sem tipos
function calcularDesconto($preco, $percentual) {
return $preco * (1 - $percentual / 100);
}
// Bom: tipos completos
function calcularDesconto(float $preco, float $percentual): float
{
if ($percentual < 0 || $percentual > 100) {
throw new \InvalidArgumentException("Percentual inválido: {$percentual}");
}
return $preco * (1 - $percentual / 100);
}
// Ainda melhor: Value Objects eliminam a necessidade de validar sempre
function calcularDesconto(Dinheiro $preco, Percentual $percentual): Dinheiro
{
return $preco->aplicarDesconto($percentual);
}
// Union types — quando necessário
function buscarOuCriar(int|string $id): Usuario { /* ... */ }
// Nullable — quando null é um valor legítimo
function buscarPorEmail(string $email): ?Usuario { /* ... */ }
// Never — função que nunca retorna (sempre lança exceção ou encerra)
function falhar(string $mensagem): never
{
throw new \RuntimeException($mensagem);
}
Nomes que explicam a intenção
<?php
// Ruim: o que é $d? O que faz $proc?
$d = new DateTime();
function proc($u, $p): bool { /* ... */ }
// Bom: nomes revelam intenção
$dataDeNascimento = new DateTimeImmutable('1990-05-15');
function verificarSenha(string $senhaDigitada, string $hashArmazenado): bool { /* ... */ }
// Ruim: abreviações crípticas
$usr = $uRepo->findById($uid);
$ord->addItm($prd, $qty, $prc);
// Bom: verbosidade que clarifica
$usuario = $usuarioRepositorio->buscarPorId($usuarioId);
$pedido->adicionarItem($produto, $quantidade, $preco);
// Booleans: prefixos que comunicam
$estaAtivo = true;
$podeEditarPerfil = false;
$temEstoqueDisponivel = $produto->estoque > 0;
// Funções booleans: verbos is/has/can/deve
function isEmailValido(string $email): bool { /* ... */ }
function temPermissao(Usuario $usuario, string $recurso): bool { /* ... */ }
function podeRealizarCompra(Usuario $usuario, Pedido $pedido): bool { /* ... */ }
Funções pequenas com uma responsabilidade
<?php
// Ruim: uma função fazendo tudo
function processarCadastro(array $dados): array
{
// validação
if (empty($dados['nome'])) return ['erro' => 'Nome obrigatório'];
if (!filter_var($dados['email'], FILTER_VALIDATE_EMAIL)) return ['erro' => 'Email inválido'];
// hash de senha
$hash = password_hash($dados['senha'], PASSWORD_ARGON2ID);
// inserção no banco
global $pdo;
$pdo->prepare('INSERT INTO usuarios ...');
// envio de email
mail($dados['email'], 'Bem-vindo!', '...');
return ['sucesso' => true];
}
// Bom: responsabilidades separadas
function validarDadosDeCadastro(array $dados): array // retorna erros
{
$erros = [];
if (empty(trim($dados['nome'] ?? ''))) $erros['nome'] = 'Nome obrigatório.';
if (!filter_var($dados['email'] ?? '', FILTER_VALIDATE_EMAIL)) $erros['email'] = 'Email inválido.';
if (strlen($dados['senha'] ?? '') < 8) $erros['senha'] = 'Senha deve ter 8+ caracteres.';
return $erros;
}
class CadastroService
{
public function __construct(
private readonly UsuarioRepositorioInterface $repositorio,
private readonly EmailService $emailService,
) {}
public function cadastrar(DadosDeCadastro $dados): Usuario
{
$usuario = $this->repositorio->salvar(
new Usuario(
nome: $dados->nome,
email: $dados->email,
senhaHash: password_hash($dados->senha, PASSWORD_ARGON2ID),
)
);
$this->emailService->enviarBoasVindas($usuario);
return $usuario;
}
}
Early return — evite else desnecessário
<?php
// Ruim: aninhamento excessivo
function processarPagamento(Pedido $pedido, Usuario $usuario): string
{
if ($pedido->estaAtivo()) {
if ($usuario->temSaldoSuficiente($pedido->total())) {
if (!$pedido->jaFoiPago()) {
// processa...
return 'Pagamento realizado.';
} else {
return 'Pedido já foi pago.';
}
} else {
return 'Saldo insuficiente.';
}
} else {
return 'Pedido inativo.';
}
}
// Bom: early return elimina o aninhamento
function processarPagamento(Pedido $pedido, Usuario $usuario): string
{
if (!$pedido->estaAtivo()) {
return 'Pedido inativo.';
}
if ($pedido->jaFoiPago()) {
return 'Pedido já foi pago.';
}
if (!$usuario->temSaldoSuficiente($pedido->total())) {
return 'Saldo insuficiente.';
}
// Chegou aqui: todas as condições satisfeitas
// processa...
return 'Pagamento realizado.';
}
Imutabilidade onde possível
<?php
// Value Objects são imutáveis por definição
final class Email
{
public function __construct(
private readonly string $valor // readonly: não pode ser alterado após construção
) {
if (!filter_var($valor, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException("Email inválido: {$valor}");
}
}
// "Modificação" retorna nova instância
public function comDominio(string $novoDominio): self
{
[$local] = explode('@', $this->valor);
return new self("{$local}@{$novoDominio}");
}
public function valor(): string { return $this->valor; }
}
// PHP 8.1+ readonly classes
final readonly class Coordenada
{
public function __construct(
public float $latitude,
public float $longitude,
) {
if ($this->latitude < -90 || $this->latitude > 90) {
throw new \InvalidArgumentException('Latitude inválida.');
}
}
}
Prefira exceções a códigos de erro
<?php
// Ruim: retorna false/null em caso de erro — o chamador pode ignorar
function dividir(float $a, float $b): float|false
{
if ($b === 0.0) return false;
return $a / $b;
}
$resultado = dividir(10, 0); // false — fácil de ignorar acidentalmente
// Bom: exceção que não pode ser ignorada silenciosamente
function dividir(float $a, float $b): float
{
if ($b === 0.0) {
throw new \DivisionByZeroError('Divisor não pode ser zero.');
}
return $a / $b;
}
// Hierarquia de exceções próprias — comunicam o problema com precisão
class DomainException extends \RuntimeException {}
class EstoqueInsuficienteException extends DomainException
{
public function __construct(
public readonly int $disponivel,
public readonly int $solicitado,
) {
parent::__construct(
"Estoque insuficiente: {$disponivel} disponível, {$solicitado} solicitado."
);
}
}
Resumo das boas práticas
| Prática | Por que importa |
|---|---|
declare(strict_types=1) |
Evita coerções silenciosas de tipo |
| Nomes descritivos | Código é lido muito mais do que escrito |
| Funções pequenas | Fáceis de testar, entender e reutilizar |
| Early return | Reduz aninhamento e aumenta clareza |
| Imutabilidade | Elimina bugs de estado compartilhado |
| Exceções em vez de null/false | Erros não podem ser ignorados silenciosamente |
| Injeção de dependência | Código testável e desacoplado |
Exercício da semana
-
Pegue um arquivo PHP antigo seu (ou de um repositório open-source) e aplique as práticas deste artigo: adicione tipos, renomeie variáveis, extraia funções, aplique early return. Documente as mudanças e compare a legibilidade.
-
Implemente uma hierarquia de exceções para um domínio de sua escolha:
DomainExceptioncomo base, e pelo menos três exceções específicas com dados contextuais nos construtores. -
Crie uma classe
Dinheirocompletamente imutável comreadonly,final, e sem setters. Implemente:somar(),subtrair(),multiplicar(),dividir(),aplicarDesconto(),equals(). Escreva testes para cada método. -
Desafio: refatore uma função longa (20+ linhas) extraindo subfunções nomeadas que expliquem cada etapa do processo. O objetivo é que o corpo da função original leia como uma sequência de frases em português:
$this->validarDados($dados); $usuario = $this->criarUsuario($dados); $this->notificar($usuario);
Referências
- Clean Code (livro) — Robert C. Martin: https://www.amazon.com.br/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882
- PHP The Right Way: https://phptherightway.com/
- PHPStan — análise estática: https://phpstan.org/
- Refactoring Guru — padrões e refatoração: https://refactoring.guru/
Módulo 6 completo — 3 artigos (38–40) cobrindo Composer, namespaces, PSR e boas práticas.
Progresso total: 40/52 artigos — só textos, conforme combinado.
Próximo: Módulo 7 — Laravel (artigos 41–46). São 6 artigos cobrindo instalação, rotas, Eloquent, Blade, autenticação e APIs com Laravel. Continuo?
you asked
Sim