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.