Criar exceção customizada em PHP: quando estender Exception compensa

Se o seu controller tem um catch (Exception $e) que trata “pedido 4821 não existe” e “o gateway devolveu 504” com a mesma tela de “erro ao processar”, quem chama o código não tem como separar as duas coisas. E separar com strpos($e->getMessage(), 'não encontrado') é pior ainda.

Uma classe por motivo de falha resolve isso. Não por elegância: por causa do catch.

A classe carrega o dado, não só o texto

<?php
namespace AppPedidoException;

class PedidoNaoEncontradoException extends RuntimeException
{
    public function __construct(
        private readonly int $pedidoId,
        ?Throwable $previous = null
    ) {
        parent::__construct("Pedido {$pedidoId} não encontrado.", 0, $previous);
    }

    public function getPedidoId(): int
    {
        return $this->pedidoId;
    }
}

Estendi RuntimeException de propósito. A convenção do PHP é essa: LogicException é bug de programador, coisa que se corrige escrevendo o código direito; RuntimeException é o que só aparece com dado real em produção. Pedido que sumiu é a segunda. A promoção de propriedade com readonly no construtor vale do PHP 8.1 pra cima.

O $previous existe para não perder o rastro

public function porId(int $id): array
{
    try {
        $stmt = $this->pdo->prepare('SELECT * FROM pedido WHERE id = :id');
        $stmt->execute(['id' => $id]);
        $linha = $stmt->fetch(PDO::FETCH_ASSOC);
    } catch (PDOException $e) {
        // banco fora do ar não é "pedido inexistente"
        throw new PedidoIndisponivelException($id, $e);
    }

    if ($linha === false) {
        throw new PedidoNaoEncontradoException($id);
    }

    return $linha;
}
// na borda da aplicação
try {
    $pedido = $this->repo->porId($id);
} catch (PedidoNaoEncontradoException $e) {
    return new JsonResponse(['erro' => 'pedido inexistente'], 404);
} catch (PedidoIndisponivelException $e) {
    $this->logger->error($e->getMessage(), ['causa' => $e->getPrevious()]);
    return new JsonResponse(['erro' => 'tente de novo em instantes'], 503);
}

PedidoIndisponivelException estende RuntimeException do mesmo jeito e recebe o $previous. Esse terceiro parâmetro é o que mais gente esquece: sem ele o stack trace do PDO some do log e sobra uma mensagem bonitinha sem a query que estourou. Com ele, getPrevious() devolve a exceção original inteira.

Um freio: não crie quarenta classes. Uma para cada decisão que o chamador precisa tomar. Se dois erros caem no mesmo catch e fazem a mesma coisa, eram a mesma classe desde o começo.

Dúvidas? Faça um comentário logo abaixo ou envie uma mensagem clicando aqui.

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *