Hierarquia de exceções e exceções customizadas

1. Introdução à hierarquia de exceções no PHP

O PHP possui um sistema robusto de tratamento de erros e exceções, centrado na interface Throwable, introduzida no PHP 7. Essa interface é a base de toda a hierarquia de exceções e erros da linguagem.

1.1 A classe base Throwable e suas interfaces

Throwable define os métodos essenciais que qualquer exceção ou erro deve implementar: getMessage(), getCode(), getFile(), getLine(), getTrace(), getTraceAsString() e getPrevious(). Tanto Exception quanto Error implementam essa interface.

1.2 Divisão principal: Exception vs Error

A hierarquia se divide em dois grandes ramos:

  • Exception: representa condições excepcionais que podem ser recuperadas. É a classe base para exceções tradicionais.
  • Error: representa erros internos do PHP que geralmente não podem ser tratados em tempo de execução, como TypeError, ParseError, DivisionByZeroError.
try {
    // Código que pode lançar exceções ou erros
} catch (\Throwable $t) {
    echo "Capturado: " . $t->getMessage();
}

1.3 A hierarquia nativa de exceções (SPL exceptions)

O PHP oferece um conjunto de exceções SPL (Standard PHP Library) organizadas em categorias lógicas e de runtime.

2. Explorando as exceções SPL nativas

2.1 Exceções lógicas: LogicException, InvalidArgumentException, OutOfRangeException

Essas exceções indicam problemas na lógica do programa, geralmente detectáveis antes da execução.

function dividir($a, $b) {
    if (!is_numeric($a) || !is_numeric($b)) {
        throw new \InvalidArgumentException("Ambos os parâmetros devem ser numéricos.");
    }
    if ($b == 0) {
        throw new \DivisionByZeroError("Divisão por zero não é permitida.");
    }
    return $a / $b;
}

2.2 Exceções de tempo de execução: RuntimeException, UnexpectedValueException, OverflowException

Ocorrem durante a execução do programa e podem ser imprevisíveis.

function lerArquivo($caminho) {
    if (!file_exists($caminho)) {
        throw new \RuntimeException("Arquivo não encontrado: $caminho");
    }
    $conteudo = file_get_contents($caminho);
    if ($conteudo === false) {
        throw new \UnexpectedValueException("Falha ao ler o conteúdo do arquivo.");
    }
    return $conteudo;
}

2.3 Exceções de domínio: DomainException, RangeException, LengthException

Relacionadas a domínios específicos, como valores fora de faixa ou comprimentos inválidos.

function criarArray($tamanho) {
    if ($tamanho < 0) {
        throw new \DomainException("Tamanho não pode ser negativo.");
    }
    if ($tamanho > 1000) {
        throw new \RangeException("Tamanho máximo excedido (1000).");
    }
    return array_fill(0, $tamanho, null);
}

3. Criando exceções customizadas

3.1 Estrutura básica de uma classe de exceção customizada

class MinhaExcecao extends \Exception {
    public function __construct($mensagem = "", $codigo = 0, \Throwable $previous = null) {
        parent::__construct($mensagem, $codigo, $previous);
    }
}

3.2 Estendendo Exception vs estendendo RuntimeException

A escolha depende da categoria da exceção:

  • Estenda Exception para erros lógicos ou de domínio.
  • Estenda RuntimeException para erros que ocorrem em tempo de execução.
class ValidacaoException extends \InvalidArgumentException {}
class BancoDadosException extends \RuntimeException {}

3.3 Adicionando propriedades e métodos extras à exceção

class EmailInvalidoException extends \InvalidArgumentException {
    private string $email;

    public function __construct(string $email, string $mensagem = "") {
        $this->email = $email;
        parent::__construct($mensagem ?: "Email inválido: $email");
    }

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

4. Boas práticas na definição de exceções customizadas

4.1 Nomenclatura semântica e coesa para classes de exceção

Use nomes que descrevam claramente o problema: ArquivoNaoEncontradoException, SaldoInsuficienteException, ConexaoPerdidaException.

4.2 Quando criar uma exceção específica vs usar exceções nativas

Crie exceções customizadas quando:
- Precisar de propriedades adicionais.
- O contexto exigir tratamento diferenciado.
- A exceção nativa não transmitir informação suficiente.

4.3 Organizando exceções em namespaces e diretórios

namespace App\Excecoes;

class UsuarioNaoEncontradoException extends \RuntimeException {}
class EmailDuplicadoException extends \DomainException {}

5. Capturando exceções de forma hierárquica

5.1 Ordem dos blocos catch e a hierarquia de classes

Sempre capture as exceções mais específicas primeiro.

try {
    // código que pode lançar exceções
} catch (EmailInvalidoException $e) {
    // trata email inválido
} catch (InvalidArgumentException $e) {
    // trata argumentos inválidos genéricos
} catch (\Exception $e) {
    // trata qualquer outra exceção
}

5.2 Capturando múltiplos tipos de exceção com união de tipos (PHP 8+)

try {
    // código
} catch (EmailInvalidoException | UsuarioNaoEncontradoException $e) {
    echo "Erro de validação: " . $e->getMessage();
}

5.3 Uso de finally em conjunto com exceções customizadas

function processarPedido(Pedido $pedido) {
    $conexao = abrirConexao();
    try {
        $conexao->iniciarTransacao();
        $pedido->validar();
        $conexao->salvar($pedido);
        $conexao->confirmarTransacao();
    } catch (ValidacaoException $e) {
        $conexao->reverterTransacao();
        throw new ProcessamentoException("Erro ao processar pedido", 0, $e);
    } finally {
        $conexao->fechar();
    }
}

6. Exceções customizadas com códigos de erro e mensagens

6.1 Definindo constantes de código de erro na classe de exceção

class ApiException extends \RuntimeException {
    public const ERRO_AUTENTICACAO = 1001;
    public const ERRO_VALIDACAO = 1002;
    public const ERRO_SERVIDOR = 1003;
}

6.2 Formatando mensagens dinâmicas com dados contextuais

class SaldoInsuficienteException extends \RuntimeException {
    public function __construct(
        private float $saldoAtual,
        private float $valorSolicitado
    ) {
        $mensagem = sprintf(
            "Saldo insuficiente. Atual: R$ %.2f, solicitado: R$ %.2f",
            $saldoAtual,
            $valorSolicitado
        );
        parent::__construct($mensagem, 2001);
    }
}

6.3 Métodos auxiliares para logging e rastreamento

class LoggableException extends \RuntimeException {
    public function toLog(): array {
        return [
            'mensagem' => $this->getMessage(),
            'codigo' => $this->getCode(),
            'arquivo' => $this->getFile(),
            'linha' => $this->getLine(),
            'trace' => $this->getTraceAsString(),
        ];
    }
}

7. Encadeamento de exceções e exceções aninhadas

7.1 Passando a exceção anterior no construtor ($previous)

try {
    // operação que falha
} catch (\PDOException $e) {
    throw new BancoDadosException("Falha na consulta", 0, $e);
}

7.2 Criando exceções que encapsulam outras exceções

class ProcessamentoException extends \RuntimeException {
    private array $erros;

    public function __construct(string $mensagem, array $erros = []) {
        $this->erros = $erros;
        parent::__construct($mensagem);
    }

    public function getErros(): array {
        return $this->erros;
    }
}

7.3 Rastreamento de causas raiz com getPrevious()

function obterCausaRaiz(\Throwable $e): \Throwable {
    while ($e->getPrevious() !== null) {
        $e = $e->getPrevious();
    }
    return $e;
}

8. Exemplos práticos de implementação

8.1 Sistema de validação com exceções customizadas por tipo de erro

class ValidacaoException extends \RuntimeException {
    private array $erros;

    public function __construct(array $erros) {
        $this->erros = $erros;
        parent::__construct("Erros de validação encontrados");
    }

    public function getErros(): array {
        return $this->erros;
    }
}

function validarUsuario(array $dados): void {
    $erros = [];
    if (empty($dados['nome'])) {
        $erros[] = "Nome é obrigatório";
    }
    if (!filter_var($dados['email'] ?? '', FILTER_VALIDATE_EMAIL)) {
        $erros[] = "Email inválido";
    }
    if (!empty($erros)) {
        throw new ValidacaoException($erros);
    }
}

8.2 Camada de repositório com exceções de domínio

class RepositorioException extends \RuntimeException {}
class EntidadeNaoEncontradaException extends RepositorioException {}

class UsuarioRepositorio {
    public function buscarPorId(int $id): Usuario {
        $stmt = $this->db->prepare("SELECT * FROM usuarios WHERE id = ?");
        $stmt->execute([$id]);
        $dados = $stmt->fetch();

        if (!$dados) {
            throw new EntidadeNaoEncontradaException(
                "Usuário com ID $id não encontrado"
            );
        }
        return new Usuario($dados);
    }
}

8.3 Tratamento centralizado de exceções customizadas com set_exception_handler

set_exception_handler(function (\Throwable $e) {
    $codigo = $e->getCode() ?: 500;
    http_response_code($codigo);

    if ($e instanceof ValidacaoException) {
        echo json_encode([
            'erro' => 'validação',
            'mensagens' => $e->getErros()
        ]);
    } elseif ($e instanceof EntidadeNaoEncontradaException) {
        echo json_encode([
            'erro' => 'não_encontrado',
            'mensagem' => $e->getMessage()
        ]);
    } else {
        error_log($e->getMessage() . "\n" . $e->getTraceAsString());
        echo json_encode([
            'erro' => 'interno',
            'mensagem' => 'Erro interno do servidor'
        ]);
    }
    exit;
});

Referências