
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.
<?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.
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.
O form do admin salva mas abre em branco na edição: veja o getData() indexado pelo id da entidade e…
O meta está no banco mas some do /wp-json/wp/v2/posts. Veja o register_post_meta certo, com single, type, schema de array e…
Escreva 31 de agosto de 2026 em PHP 8.3 sem aquele array com os doze meses, usando IntlDateFormatter com pattern…
Listagem de CPT com meta_query que demora segundos: veja o EXPLAIN da query do WP_Query e o índice composto em…
Post com Perdeu o agendamento e backup que não roda há dias: inspecione a fila do WP-Cron, apague a trava…
Como aposentar o plugin AMP sem transformar centenas de URLs /amp/ indexadas em 404: o rewrite 301 no nginx, a…