PHP Criar suas Exceções
Na aula anterior, a lojinha da Ana lançava new Exception("Estoque insuficiente…"). Funciona, mas quem captura só tem a mensagem para saber o que aconteceu, e não dá para tratar "estoque acabou" de um jeito e "cartão recusado" de outro. A solução é criar tipos de exceção próprios, com nomes que dizem o problema. São classes de uma ou poucas linhas, e são a porta de entrada mais suave para a orientação a objetos.
- Criar uma exceção com
class EstoqueInsuficienteException extends Exception {}. - Capturar só o seu tipo, deixando os outros passarem.
- Dar um construtor à exceção, com a mensagem pronta e dados extras (
parent::__construct). - Montar uma hierarquia: uma exceção "mãe" da loja e várias "filhas".
- Estender as exceções prontas do PHP (
InvalidArgumentException…). - Quando vale a pena criar uma exceção, e quando não.
Você precisa ter visto Exceções (try e catch). Esta aula usa duas ideias de orientação a objetos, explicadas aqui só o suficiente: classe (um "molde" de objetos) e herança (extends, "estende": uma classe nova que aproveita tudo de outra). Elas são estudadas a fundo em Classes e Objetos e Herança, no Nível 3.
A ideia: etiquetas nos avisos
Imagine que todos os avisos da cozinha fossem papeizinhos brancos com um texto. Para saber se é "acabou o ovo" ou "o forno quebrou", você precisa ler cada um. Agora imagine papeizinhos coloridos: amarelo é falta de ingrediente, vermelho é equipamento quebrado. Você separa só de olhar, e cada cor vai para a pessoa certa. Criar uma exceção própria é criar uma nova "cor" de aviso.
A exceção mais simples
<?php
class EstoqueInsuficienteException extends Exception
{
}
function vender(string $produto, int $estoque, int $quantidade): int
{
if ($quantidade > $estoque) {
throw new EstoqueInsuficienteException("Só há $estoque de $produto");
}
return $estoque - $quantidade;
}
try {
vender('bolo de cenoura', 2, 5);
} catch (EstoqueInsuficienteException $e) {
echo "Avise o cliente: ", $e->getMessage(), "\n";
}
vender('torta', 1, 3);
- class
- "Classe": vamos criar um novo tipo.
- EstoqueInsuficienteException
- O nome. Por costume, em CamelCase (cada palavra com maiúscula) e terminando em
Exception. Diga o problema, não o lugar:EstoqueInsuficiente, e nãoErroNaFuncaoVender. - extends Exception
- "Estende
Exception": a classe nova herda tudo daException(mensagem, código, linha, rastro,getMessage()…). É isso que permite lançá-la. - { }
- O corpo vazio: não precisamos acrescentar nada. A única novidade é o nome, e isso já é muito.
$ cd excp
$ php simples.php
Avise o cliente: Só há 2 de bolo de cenoura
Fatal error: Uncaught EstoqueInsuficienteException: Só há 1 de torta in /home/ana/excp/simples.php:9
Stack trace:
#0 /home/ana/excp/simples.php(19): vender('torta', 1, 3)
#1 {main}
thrown in /home/ana/excp/simples.php on line 9
A primeira venda foi capturada pelo catch específico. A segunda, sem try, virou o erro fatal, e repare que a mensagem já diz o tipo: Uncaught EstoqueInsuficienteException. Quem ler o registro de erros entende o problema antes mesmo de ler a mensagem.
Missão: crie a classe EstoqueInsuficienteException (filha de Exception), lance-a na função no lugar da Exception genérica e troque o catch para capturar só ela.
<?php
// crie a classe aqui
function vender(string $produto, int $estoque, int $quantidade): int
{
if ($quantidade > $estoque) {
throw new Exception("Só há $estoque de $produto");
}
return $estoque - $quantidade;
}
try {
vender('bolo', 2, 5);
} catch (Exception $e) {
echo "Avise o cliente: ", $e->getMessage(), "\n";
}
<?php
class EstoqueInsuficienteException extends Exception
{
}
function vender(string $produto, int $estoque, int $quantidade): int
{
if ($quantidade > $estoque) {
throw new EstoqueInsuficienteException("Só há $estoque de $produto");
}
return $estoque - $quantidade;
}
try {
vender('bolo', 2, 5);
} catch (EstoqueInsuficienteException $e) {
echo "Avise o cliente: ", $e->getMessage(), "\n";
}
Um construtor: mensagem pronta e dados extras
Com a classe vazia, cada throw precisa montar a mensagem, e quem captura só recebe texto. Se o catch quiser saber quantos faltaram, teria que "ler" a mensagem. Melhor: a exceção guarda os dados e monta a mensagem sozinha. Para isso ela ganha um construtor (__construct), a função que roda quando você escreve new:
<?php
class EstoqueInsuficienteException extends Exception
{
public function __construct(
public readonly string $produto,
public readonly int $pedido,
public readonly int $disponivel,
) {
parent::__construct("Estoque insuficiente de $produto: pediu $pedido, há $disponivel");
}
}
try {
throw new EstoqueInsuficienteException('brigadeiro', 50, 30);
} catch (EstoqueInsuficienteException $e) {
echo $e->getMessage(), "\n";
echo "Falta(m) ", $e->pedido - $e->disponivel, " {$e->produto}(s)\n";
echo "Linha: ", $e->getLine(), "\n";
}
| Trecho | O que faz |
|---|---|
public function __construct(…) | O construtor: recebe os valores passados em new EstoqueInsuficienteException('brigadeiro', 50, 30). Veja Construtores. |
public readonly string $produto, | Recebe o parâmetro e já cria uma propriedade $produto com o valor (a "promoção de construtor", PHP 8.0+). public: dá para ler de fora ($e->produto). readonly (8.1+): ninguém pode mudar depois. |
parent::__construct("…") | Chama o construtor da classe mãe (Exception) com a mensagem montada. É ele que guarda a mensagem para o getMessage(). Não esqueça esta linha! |
$e->pedido - $e->disponivel | Quem captura usa os dados, sem precisar interpretar o texto. |
$ php construtor.php Estoque insuficiente de brigadeiro: pediu 50, há 30 Falta(m) 20 brigadeiro(s) Linha: 14 « getLine continua funcionando: herdado da Exception
Agora o throw ficou curto e sem chance de mensagem mal escrita: throw new EstoqueInsuficienteException($produto, $qtd, $estoque);.
Missão: a exceção já guarda $produto e $disponivel. Acrescente o dado $pedido (int) ao construtor, inclua-o na mensagem e, no catch, mostre Faltam 3 pudim(s) para o pedido da Bia calculando com as propriedades.
<?php
class EstoqueInsuficienteException extends Exception
{
public function __construct(
public readonly string $produto,
public readonly int $disponivel,
) {
parent::__construct("Só há $disponivel de $produto");
}
}
try {
throw new EstoqueInsuficienteException('pudim', 2);
} catch (EstoqueInsuficienteException $e) {
echo $e->getMessage(), "\n";
}
<?php
class EstoqueInsuficienteException extends Exception
{
public function __construct(
public readonly string $produto,
public readonly int $pedido,
public readonly int $disponivel,
) {
parent::__construct("Pediu $pedido de $produto, só há $disponivel");
}
}
try {
throw new EstoqueInsuficienteException('pudim', 5, 2);
} catch (EstoqueInsuficienteException $e) {
echo $e->getMessage(), "\n";
echo "Faltam ", $e->pedido - $e->disponivel, " {$e->produto}(s) para o pedido da Bia\n";
}
Uma família de exceções
A lojinha tem vários problemas possíveis: produto que não existe, estoque insuficiente, pagamento recusado. Cada um merece um tipo. E todos são "problemas da loja", então podem ter uma mãe comum:
<?php
class LojaException extends Exception {}
class EstoqueInsuficienteException extends LojaException {}
class ProdutoNaoEncontradoException extends LojaException {}
class PagamentoRecusadoException extends LojaException {}
$catalogo = ['bolo' => 2, 'torta' => 0];
function comprar(array $catalogo, string $produto, int $qtd, bool $cartaoOk): string
{
if (!array_key_exists($produto, $catalogo)) {
throw new ProdutoNaoEncontradoException("Não vendemos $produto");
}
if ($catalogo[$produto] < $qtd) {
throw new EstoqueInsuficienteException("Só temos {$catalogo[$produto]} $produto(s)");
}
if (!$cartaoOk) {
throw new PagamentoRecusadoException('Cartão recusado');
}
return "Compra de $qtd $produto(s) aprovada";
}
$tentativas = [['bolo', 1, true], ['pastel', 1, true], ['torta', 1, true], ['bolo', 2, false]];
foreach ($tentativas as [$p, $q, $ok]) {
try {
echo comprar($catalogo, $p, $q, $ok), "\n";
} catch (PagamentoRecusadoException $e) {
echo "Pagamento: ", $e->getMessage(), " (tente outro cartão)\n";
} catch (LojaException $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
}
var_dump(new EstoqueInsuficienteException() instanceof LojaException);
var_dump(new EstoqueInsuficienteException() instanceof Exception);
print_r(class_parents(new EstoqueInsuficienteException()));
A árvore fica assim:
Throwable
└── Exception
└── LojaException
├── EstoqueInsuficienteException
├── ProdutoNaoEncontradoException
└── PagamentoRecusadoException
$ php hierarquia.php
Compra de 1 bolo(s) aprovada
ProdutoNaoEncontradoException: Não vendemos pastel « caiu no catch da mãe
EstoqueInsuficienteException: Só temos 0 torta(s) « também
Pagamento: Cartão recusado (tente outro cartão) « tratamento especial
bool(true) « uma EstoqueInsuficienteException É uma LojaException
bool(true) « …e também É uma Exception
Array
(
[LojaException] => LojaException
[Exception] => Exception
)
Com a família, quem captura escolhe o nível de detalhe:
catch (PagamentoRecusadoException $e): só aquele problema, com tratamento especial.catch (LojaException $e): qualquer problema da loja, de uma vez. Se amanhã surgir umaCupomInvalidoException extends LojaException, essecatchjá a pega, sem mudar nada.- Problemas que não são da loja (um
TypeError, umJsonException) passam reto e não são confundidos com regras de negócio.
Estender as exceções do PHP
A sua exceção não precisa ser filha direta de Exception. Se ela é um caso particular de uma exceção que o PHP já tem (veja a tabela na aula anterior), estenda essa:
<?php
class QuantidadeInvalidaException extends InvalidArgumentException {}
function quantidade(string $texto): int
{
if (!ctype_digit($texto)) {
throw new QuantidadeInvalidaException("Quantidade inválida: '$texto'");
}
return (int) $texto;
}
try {
quantidade('dez');
} catch (InvalidArgumentException $e) {
echo get_class($e), " pega como InvalidArgumentException: ", $e->getMessage(), "\n";
}
$ php spl.php QuantidadeInvalidaException pega como InvalidArgumentException: Quantidade inválida: 'dez'
Assim, um código que já captura InvalidArgumentException (de uma biblioteca, por exemplo) continua funcionando com a sua exceção.
extends Error?
Não faça. A família Error é para erros internos do PHP (tipo errado, função inexistente). As suas exceções são da família Exception.
Organizar em arquivos
Num sistema de verdade, as exceções ficam num arquivo próprio (ou um arquivo por classe), carregado com require_once (aula include e require):
<?php
class LojaException extends Exception {}
class EstoqueInsuficienteException extends LojaException
{
public function __construct(
public readonly string $produto,
public readonly int $pedido,
public readonly int $disponivel,
) {
parent::__construct("Estoque insuficiente de $produto: pediu $pedido, há $disponivel");
}
}
<?php
require_once __DIR__ . '/excecoes.php';
try {
throw new EstoqueInsuficienteException('pudim', 3, 1);
} catch (LojaException $e) {
echo $e->getMessage(), "\n";
}
$ php usa-arquivo.php Estoque insuficiente de pudim: pediu 3, há 1
No Nível 3 você vai ver o padrão profissional: cada classe no seu arquivo (EstoqueInsuficienteException.php), em namespaces, carregadas pelo autoload sem nenhum require.
Quando criar uma exceção própria?
| Situação | Faça |
|---|---|
| Quem chama vai querer tratar esse problema de um jeito diferente dos outros | Crie uma exceção própria (PagamentoRecusadoException) |
| O problema tem dados úteis para quem trata (quanto faltou, qual produto) | Exceção própria com construtor e propriedades |
| Seu sistema (ou biblioteca) tem vários problemas "de negócio" | Uma mãe comum (LojaException) e filhas |
| Um argumento inválido qualquer, que ninguém vai tratar de forma especial | Use a pronta: InvalidArgumentException |
| "Não encontrei" é um resultado normal (buscar um cliente que pode não existir) | Nem exceção: devolva null |
| Uma exceção para cada linha do programa | Não: exceções demais viram ruído. Crie quando o tipo fizer diferença para quem captura. |
Quando dá errado
extends)
<?php
class EstoqueInsuficienteException
{
}
throw new EstoqueInsuficienteException();
$ php sem-extends.php
Fatal error: Uncaught Error: Cannot throw objects that do not implement Throwable in /home/ana/excp/sem-extends.php:5
Stack trace:
#0 {main}
thrown in /home/ana/excp/sem-extends.php on line 5
O que significa: o nome termina em Exception, mas a classe é comum; só objetos da família Throwable podem ser lançados.
Como consertar: class EstoqueInsuficienteException extends Exception.
$ php implements.php « class EstoqueInsuficienteException implements Throwable
Fatal error: Class EstoqueInsuficienteException cannot implement interface Throwable, extend Exception or Error instead in /home/ana/excp/implements.php on line 2
Stack trace:
#0 {main}
Como consertar: a própria mensagem diz: estenda Exception.
parent::__construct)
<?php
class EstoqueInsuficienteException extends Exception
{
public function __construct(public readonly string $produto)
{
}
}
try {
throw new EstoqueInsuficienteException('bolo');
} catch (EstoqueInsuficienteException $e) {
var_dump($e->getMessage());
}
$ php sem-parent.php string(0) ""
O que significa: o construtor próprio substituiu o da Exception, e ninguém entregou a mensagem a ela.
Como consertar: chame parent::__construct("…mensagem…"); dentro do seu construtor.
$ php argumentos.php « o construtor pede (produto, disponível) e alguém passou só um texto
Fatal error: Uncaught ArgumentCountError: Too few arguments to function EstoqueInsuficienteException::__construct(), 1 passed in /home/ana/excp/argumentos.php on line 9 and exactly 2 expected in /home/ana/excp/argumentos.php:4
Stack trace:
#0 /home/ana/excp/argumentos.php(9): EstoqueInsuficienteException->__construct('Estoque acabou')
#1 {main}
thrown in /home/ana/excp/argumentos.php on line 4
O que significa: depois que a exceção ganhou um construtor próprio, o new precisa passar os argumentos dele, e não mais uma mensagem.
Como consertar: new EstoqueInsuficienteException('bolo', 0), na ordem do construtor (ou com argumentos nomeados).
$ php final.php « tentou criar um método getMessage() na exceção
Fatal error: Cannot override final method Exception::getMessage() in /home/ana/excp/final.php on line 4
Stack trace:
#0 {main}
O que significa: os métodos get… da Exception são final ("finais"): não podem ser substituídos.
Como consertar: passe a mensagem pelo parent::__construct. Para informações extras, crie propriedades ou métodos com outros nomes.
Referência
| Sintaxe | O que faz |
|---|---|
class XException extends Exception {} | Cria uma exceção própria. |
class XException extends LojaException {} | Filha de outra exceção sua (hierarquia). |
class XException extends InvalidArgumentException {} | Caso particular de uma exceção do PHP. |
public function __construct(public readonly int $x) { … } | Construtor que guarda dados na exceção. |
parent::__construct($mensagem, $codigo, $anterior) | Entrega mensagem, código e causa à Exception. |
$e instanceof LojaException | Pergunta se a exceção é daquela família. |
get_class($e), class_parents($e) | Classe exata e "antepassados". |
Exercícios
1. A mãe certa
Complete para criar uma exceção própria:
2. Entregar a mensagem
Complete a linha dentro do construtor da exceção:
3. Pegar a família toda
Todas as exceções da loja estendem LojaException. Complete para capturar qualquer uma:
4. class PagamentoRecusadoException extends LojaException {} e class LojaException extends Exception {}. Qual catch não captura uma PagamentoRecusadoException?
5. Por que criar EstoqueInsuficienteException em vez de lançar new Exception("Estoque insuficiente")?
6. A exceção tem construtor próprio, mas getMessage() devolve "". O que falta?
Missão final: no arquivo excecoes.php, crie LojaException (filha de Exception) e duas filhas dela, CupomInexistenteException e CupomExpiradoException, cada uma com um construtor que recebe o código do cupom e monta a mensagem (O cupom XYZ não existe, O cupom NATAL25 expirou). No index.php, lance a exceção certa na função e complete o catch para mostrar Cupom recusado (Classe): mensagem para qualquer exceção da loja. O último teste provoca um TypeError, que já tem o seu próprio catch.
<?php
require_once __DIR__ . '/excecoes.php';
$cupons = ['PROMO10' => ['desconto' => 10, 'valido' => true],
'NATAL25' => ['desconto' => 25, 'valido' => false]];
function aplicar(array $cupons, string $codigo, float $total): float
{
// lance CupomInexistenteException ou CupomExpiradoException aqui
return $total * (100 - $cupons[$codigo]['desconto']) / 100;
}
foreach (['PROMO10', 'NATAL25', 'XYZ', null] as $codigo) {
try {
$final = aplicar($cupons, $codigo, 100);
echo "Desconto de {$cupons[$codigo]['desconto']}%: R$ " . number_format($final, 2, ',', '.') . "\n";
} catch (TypeError $e) {
echo "Erro inesperado (" . get_class($e) . ")\n";
}
}
<?php // crie as exceções aqui
<?php
require_once __DIR__ . '/excecoes.php';
$cupons = ['PROMO10' => ['desconto' => 10, 'valido' => true],
'NATAL25' => ['desconto' => 25, 'valido' => false]];
function aplicar(array $cupons, string $codigo, float $total): float
{
if (!isset($cupons[$codigo])) {
throw new CupomInexistenteException($codigo);
}
if (!$cupons[$codigo]['valido']) {
throw new CupomExpiradoException($codigo);
}
return $total * (100 - $cupons[$codigo]['desconto']) / 100;
}
foreach (['PROMO10', 'NATAL25', 'XYZ', null] as $codigo) {
try {
$final = aplicar($cupons, $codigo, 100);
echo "Desconto de {$cupons[$codigo]['desconto']}%: R$ " . number_format($final, 2, ',', '.') . "\n";
} catch (LojaException $e) {
echo "Cupom recusado (" . get_class($e) . "): " . $e->getMessage() . "\n";
} catch (TypeError $e) {
echo "Erro inesperado (" . get_class($e) . ")\n";
}
}
<?php
class LojaException extends Exception {}
class CupomInexistenteException extends LojaException
{
public function __construct(public readonly string $codigo)
{
parent::__construct("O cupom $codigo não existe");
}
}
class CupomExpiradoException extends LojaException
{
public function __construct(public readonly string $codigo)
{
parent::__construct("O cupom $codigo expirou");
}
}
Resumo
- Uma exceção própria é uma classe que estende
Exception:class EstoqueInsuficienteException extends Exception {}. - O nome diz o problema e termina em
Exception; quem captura pode tratá-la separadamente. - Um construtor guarda dados (
public readonly) e monta a mensagem, entregue comparent::__construct(…). - Uma mãe comum (
LojaException) permite capturar a família toda; filhas permitem tratar casos específicos. - Casos particulares de exceções do PHP podem estendê-las (
extends InvalidArgumentException); nunca estendaError. - Crie exceções quando o tipo fizer diferença para quem captura.
Próxima aula: Depurar, para encontrar o problema quando a mensagem de erro não basta.