Exceções customizadas permitem que você crie classes de erro específicas para o seu domínio de aplicação, tornando o tratamento de erros mais expressivo e organizado. Ao estender a classe Exception do PHP, você pode adicionar propriedades e métodos personalizados, além de categorizar erros de forma semântica.

Nesta aula, vamos explorar como estender a classe Exception, criar exceções de domínio, encadear exceções para preservar a pilha de chamadas original e discutir quando é apropriado criar suas próprias exceções.

Estendendo Exception

A classe Exception do PHP é a base para todas as exceções. Para criar uma exceção customizada, basta definir uma nova classe que estenda Exception. Você pode sobrescrever o construtor e adicionar métodos específicos.

Exemplo de uma exceção simples para erros de validação:

<?php
class ValidationException extends Exception {
    private $errors;

    public function __construct($message = "", $code = 0, Throwable $previous = null, array $errors = []) {
        parent::__construct($message, $code, $previous);
        $this->errors = $errors;
    }

    public function getErrors(): array {
        return $this->errors;
    }
}
?>

Essa exceção armazena uma lista de erros de validação. Ao lançá-la, você pode passar os detalhes dos campos inválidos. Exemplo de uso:

throw new ValidationException("Dados inválidos", 400, null, ["nome" => "Campo obrigatório"]);

Exceções de domínio

Exceções de domínio representam erros específicos do negócio, como "SaldoInsuficienteException" em um sistema financeiro ou "ProdutoEsgotadoException" em um e-commerce. Elas ajudam a tornar o código mais semântico e facilitam o tratamento seletivo.

Exemplo de exceção de domínio:

<?php
class SaldoInsuficienteException extends \RuntimeException {
    private $saldoAtual;
    private $valorSolicitado;

    public function __construct($saldoAtual, $valorSolicitado, $message = "", $code = 0, Throwable $previous = null) {
        $this->saldoAtual = $saldoAtual;
        $this->valorSolicitado = $valorSolicitado;
        $message = $message ?: "Saldo insuficiente: R$ {$saldoAtual} para saque de R$ {$valorSolicitado}";
        parent::__construct($message, $code, $previous);
    }

    public function getSaldoAtual(): float {
        return $this->saldoAtual;
    }

    public function getValorSolicitado(): float {
        return $this->valorSolicitado;
    }
}
?>

Uso em uma classe Conta:

public function sacar(float $valor): void {
    if ($valor > $this->saldo) {
        throw new SaldoInsuficienteException($this->saldo, $valor);
    }
    $this->saldo -= $valor;
}

Encadeamento

O encadeamento de exceções permite que você lance uma nova exceção mantendo a referência à exceção original. Isso é útil ao capturar uma exceção de baixo nível e relançá-la com uma mensagem mais significativa, preservando a pilha de chamadas original.

No PHP, o encadeamento é feito passando a exceção anterior como terceiro parâmetro do construtor ($previous). Exemplo:

try {
    // código que pode lançar PDOException
} catch (PDOException $e) {
    throw new DatabaseException("Erro ao acessar o banco", 0, $e);
}

Você pode obter a exceção anterior usando o método getPrevious(). Isso é útil para depuração e logs.

Quando criar

Crie exceções customizadas quando:

  • Você precisa de informações adicionais sobre o erro (como dados do contexto).
  • Deseja tratar diferentes tipos de erro de forma seletiva com múltiplos blocos catch.
  • O erro é específico do domínio e merece uma classe com nome significativo.
  • Você quer evitar o uso de códigos de erro mágicos ou mensagens soltas.

Evite criar exceções para cada situação trivial. Se um erro pode ser representado por uma exceção nativa (como InvalidArgumentException), prefira usá-la. O excesso de classes de exceção pode tornar o código confuso.

Boas práticas

  • Nomeie as exceções de forma clara, usando sufixo "Exception".
  • Mantenha a hierarquia: estenda Exception ou RuntimeException conforme a necessidade (exceções verificadas vs. não verificadas).
  • Documente as exceções que seu método pode lançar usando PHPDoc (@throws).
  • Considere criar uma exceção base para seu projeto, como AppException, e derivar as demais.

Referências

Exercícios

  1. Crie uma exceção customizada chamada ArquivoNaoEncontradoException que aceite o nome do arquivo como parâmetro adicional. Implemente um método getNomeArquivo().
  2. ✓ Resposta:
    <?php
    class ArquivoNaoEncontradoException extends \Exception {
        private $nomeArquivo;
    
        public function __construct($nomeArquivo, $message = "", $code = 0, Throwable $previous = null) {
            $this->nomeArquivo = $nomeArquivo;
            $message = $message ?: "Arquivo não encontrado: {$nomeArquivo}";
            parent::__construct($message, $code, $previous);
        }
    
        public function getNomeArquivo(): string {
            return $this->nomeArquivo;
        }
    }
    ?>
  3. Escreva um trecho de código que lance uma exceção SaldoInsuficienteException (como no exemplo) e a capture, exibindo o saldo atual e o valor solicitado.
  4. ✓ Resposta:
    try {
        $conta = new Conta(100);
        $conta->sacar(200);
    } catch (SaldoInsuficienteException $e) {
        echo "Saldo atual: R$ " . $e->getSaldoAtual() . "<br>";
        echo "Valor solicitado: R$ " . $e->getValorSolicitado();
    }
    
  5. Implemente o encadeamento de exceções: capture uma PDOException e lance uma DatabaseException (crie essa classe) com a exceção original como anterior.
  6. ✓ Resposta:
    <?php
    class DatabaseException extends \Exception {}
    
    try {
        // código PDO
    } catch (PDOException $e) {
        throw new DatabaseException("Erro no banco de dados", 0, $e);
    }
    ?>
  7. Crie uma exceção base AppException e duas subclasses: ValidacaoException e PersistenciaException. Mostre como capturar a exceção base para tratar ambas.
  8. ✓ Resposta:
    <?php
    class AppException extends \Exception {}
    class ValidacaoException extends AppException {}
    class PersistenciaException extends AppException {}
    
    try {
        // código que pode lançar qualquer AppException
    } catch (AppException $e) {
        echo "Erro da aplicação: " . $e->getMessage();
    }
    ?>
  9. Explique em um parágrafo quando você deve criar uma exceção customizada em vez de usar uma exceção nativa.
  10. ✓ Resposta: Você deve criar uma exceção customizada quando o erro representa uma condição específica do domínio da aplicação e você precisa de informações adicionais (como dados de contexto) ou deseja capturar seletivamente esse tipo de erro. Se a exceção nativa já é suficiente (ex.: InvalidArgumentException para argumentos inválidos), prefira usá-la para evitar complexidade desnecessária.