Exceções customizadas permitem que você crie tipos de erro específicos para sua aplicação, tornando o tratamento de erros mais semântico e organizado. Em PHP, todas as exceções devem estender a classe base Exception (ou uma de suas subclasses como RuntimeException). Ao criar exceções personalizadas, você pode adicionar propriedades e métodos que ajudam a depurar e tratar erros de forma mais precisa.

Nesta aula, veremos como estender a classe Exception, criar exceções de domínio que refletem regras de negócio, encadear exceções para preservar a causa original e discutir quando vale a pena criar uma exceção customizada versus usar as nativas.

Estendendo Exception

Para criar uma exceção customizada, basta definir uma classe que estenda Exception ou qualquer outra classe de exceção. Você pode sobrescrever o construtor para aceitar parâmetros adicionais, como um código de erro personalizado ou dados extras. Além disso, é comum adicionar métodos getter para acessar essas informações.

O construtor da classe Exception espera uma mensagem (string), um código (int) e uma exceção anterior (Throwable). Ao sobrescrever, você pode definir valores padrão ou exigir parâmetros específicos. Exemplo:

<?php
class MinhaExcecao extends Exception {
    private $dadosExtras;

    public function __construct($mensagem = "", $codigo = 0, Throwable $anterior = null, $dadosExtras = []) {
        parent::__construct($mensagem, $codigo, $anterior);
        $this->dadosExtras = $dadosExtras;
    }

    public function getDadosExtras() {
        return $this->dadosExtras;
    }
}

try {
    throw new MinhaExcecao("Erro personalizado", 42, null, ["chave" => "valor"]);
} catch (MinhaExcecao $e) {
    echo $e->getMessage() . "\n";
    print_r($e->getDadosExtras());
}
?>

No exemplo, a exceção MinhaExcecao aceita um array de dados extras. O construtor chama o construtor pai e armazena os dados. No bloco catch, podemos acessar os dados extras facilmente. Isso é útil para transportar informações contextuais sobre o erro.

Exceções de domínio

Exceções de domínio representam erros específicos do negócio da aplicação. Por exemplo, em um sistema bancário, você pode ter SaldoInsuficienteException ou ContaInativaException. Elas ajudam a separar erros técnicos de erros de negócio e permitem que o código cliente trate cada situação de forma adequada.

Crie exceções de domínio estendendo Exception ou RuntimeException (se o erro for de execução). Normalmente, você não precisa adicionar muitos métodos, apenas definir o nome da classe de forma clara. Exemplo:

<?php
class SaldoInsuficienteException extends \Exception {}

class ContaBancaria {
    private $saldo;

    public function __construct($saldoInicial) {
        $this->saldo = $saldoInicial;
    }

    public function sacar($valor) {
        if ($valor > $this->saldo) {
            throw new SaldoInsuficienteException("Saldo insuficiente para saque de R$ $valor");
        }
        $this->saldo -= $valor;
    }
}

try {
    $conta = new ContaBancaria(100);
    $conta->sacar(150);
} catch (SaldoInsuficienteException $e) {
    echo "Erro de negócio: " . $e->getMessage();
}
?>

Note que a classe de exceção é vazia, mas seu nome já comunica o problema. Ao capturar especificamente SaldoInsuficienteException, o código sabe que é um erro de regra de negócio, não um erro de banco de dados ou de sistema.

Encadeamento

O encadeamento de exceções (exception chaining) permite que você mantenha a causa original de um erro quando relança uma exceção. Isso é útil quando você captura uma exceção de baixo nível (como PDOException) e quer lançar uma exceção de domínio, mas preservando a exceção original para depuração.

Em PHP, o terceiro parâmetro do construtor de Exception aceita a exceção anterior. Você pode passá-la diretamente. Exemplo:

<?php
class BancoDeDadosException extends \Exception {}

try {
    // Simula uma exceção PDO
    throw new PDOException("Falha na conexão");
} catch (PDOException $e) {
    throw new BancoDeDadosException("Erro ao acessar banco", 0, $e);
}
?>

Quando você capturar BancoDeDadosException, pode chamar $e->getPrevious() para obter a exceção original (PDOException) e acessar seus detalhes. Isso é fundamental para logs e depuração, pois a mensagem da exceção de domínio pode ser mais amigável, enquanto a causa real fica disponível.

Você também pode encadear exceções em cascata. Cada exceção pode ter uma anterior, formando uma cadeia. O método getPrevious() retorna a exceção imediatamente anterior.

Quando criar

Criar uma exceção customizada é vantajoso quando você precisa capturar um erro específico e tratá-lo de forma diferente de outros erros. Por exemplo, se você tem várias operações que podem falhar por diferentes motivos, usar exceções distintas no catch permite respostas adequadas (como exibir uma mensagem diferente para o usuário ou tentar uma ação alternativa).

Outro cenário é quando você quer adicionar informações extras ao erro, como dados de contexto. Exceções nativas como InvalidArgumentException podem ser suficientes para muitos casos; não crie uma nova classe se não agregar valor. Uma boa prática é criar exceções que representem conceitos do domínio (ex.: ProdutoNaoEncontradoException) ou categorias técnicas (ex.: ConexaoFalhouException). Evite criar exceções genéricas como MeuErroException sem um propósito claro.

Além disso, considere a hierarquia de exceções. Você pode ter uma exceção base para seu módulo (ex.: MeuModuloException) e depois subclasses para erros específicos. Isso permite capturar todas as exceções do módulo com um único catch ou tratar cada uma separadamente.

Boas práticas

  • Não crie exceções para fluxo de controle normal; use-as apenas para situações excepcionais.
  • Nomeie as exceções de forma clara e consistente, geralmente terminando com "Exception".
  • Documente quando uma exceção é lançada (use phpDoc @throws).
  • Prefira exceções específicas a genéricas; isso facilita o tratamento.
  • Encadeie exceções sempre que relançar, para não perder a causa original.

Referências

Exercícios

  1. Crie uma exceção customizada chamada IdadeInvalidaException que aceite um parâmetro adicional $idade no construtor. Escreva um código que lance essa exceção se a idade for menor que 0 ou maior que 150.

    ✓ Resposta:
    <?php
    class IdadeInvalidaException extends \Exception {
        private $idade;
    
        public function __construct($mensagem = "", $codigo = 0, Throwable $anterior = null, $idade = null) {
            parent::__construct($mensagem, $codigo, $anterior);
            $this->idade = $idade;
        }
    
        public function getIdade() {
            return $this->idade;
        }
    }
    
    function validarIdade($idade) {
        if ($idade < 0 || $idade > 150) {
            throw new IdadeInvalidaException("Idade inválida: $idade", 0, null, $idade);
        }
        return true;
    }
    
    try {
        validarIdade(-5);
    } catch (IdadeInvalidaException $e) {
        echo "Erro: " . $e->getMessage() . " (idade fornecida: " . $e->getIdade() . ")";
    }
    ?>
  2. Estenda a classe RuntimeException para criar uma exceção chamada ArquivoNaoEncontradoException. Use-a em uma função que tenta abrir um arquivo e lança a exceção se o arquivo não existir.

    ✓ Resposta:
    <?php
    class ArquivoNaoEncontradoException extends \RuntimeException {
        public function __construct($arquivo, $codigo = 0, Throwable $anterior = null) {
            $mensagem = "Arquivo não encontrado: $arquivo";
            parent::__construct($mensagem, $codigo, $anterior);
        }
    }
    
    function abrirArquivo($caminho) {
        if (!file_exists($caminho)) {
            throw new ArquivoNaoEncontradoException($caminho);
        }
        return fopen($caminho, 'r');
    }
    
    try {
        $arquivo = abrirArquivo('inexistente.txt');
    } catch (ArquivoNaoEncontradoException $e) {
        echo $e->getMessage();
    }
    ?>
  3. Crie duas exceções de domínio: ProdutoEsgotadoException e PagamentoRecusadoException. Escreva um trecho de código que simule uma compra e lance uma delas dependendo da condição. Capture ambas com um único catch que trate exceções de domínio.

    ✓ Resposta:
    <?php
    class ProdutoEsgotadoException extends \Exception {}
    class PagamentoRecusadoException extends \Exception {}
    
    function comprar($produto, $pagamentoAprovado) {
        if ($produto === 'item_raro') {
            throw new ProdutoEsgotadoException("Produto esgotado: $produto");
        }
        if (!$pagamentoAprovado) {
            throw new PagamentoRecusadoException("Pagamento recusado");
        }
        echo "Compra realizada com sucesso!";
    }
    
    try {
        comprar('item_raro', true);
    } catch (ProdutoEsgotadoException | PagamentoRecusadoException $e) {
        echo "Erro na compra: " . $e->getMessage();
    }
    ?>
  4. Encadeie exceções: crie uma função que lance uma exceção de PDOException simulada, capture-a e relance uma exceção customizada DatabaseException com a causa original. Em seguida, capture a DatabaseException e exiba a mensagem e a causa.

    ✓ Resposta:
    <?php
    class DatabaseException extends \Exception {}
    
    function conectar() {
        // Simula uma exceção PDO
        throw new PDOException("Falha na conexão com o banco");
    }
    
    try {
        conectar();
    } catch (PDOException $e) {
        throw new DatabaseException("Erro de banco de dados", 0, $e);
    }
    ?>
    
    // Em outro ponto do código:
    try {
        // ... código que chama conectar()
    } catch (DatabaseException $e) {
        echo "Mensagem: " . $e->getMessage() . "\n";
        $anterior = $e->getPrevious();
        if ($anterior) {
            echo "Causa: " . $anterior->getMessage();
        }
    }
  5. Crie uma hierarquia de exceções para um sistema de biblioteca: uma exceção base BibliotecaException e duas subclasses: LivroIndisponivelException e UsuarioNaoEncontradoException. Escreva um código que lance LivroIndisponivelException e capture usando a classe base.

    ✓ Resposta:
    <?php
    class BibliotecaException extends \Exception {}
    class LivroIndisponivelException extends BibliotecaException {}
    class UsuarioNaoEncontradoException extends BibliotecaException {}
    
    function emprestarLivro($livroId, $usuarioId) {
        // Simulação: livro não disponível
        throw new LivroIndisponivelException("Livro $livroId não está disponível");
    }
    
    try {
        emprestarLivro(123, 456);
    } catch (BibliotecaException $e) {
        echo "Erro na biblioteca: " . $e->getMessage();
    }
    ?>