PHP Classes Abstratas
Na Herança, a classe mãe era um produto "de verdade", que podia ser criado sozinho. Às vezes, porém, a mãe é só uma ideia geral: "uma forma de pagamento". Ninguém paga com "uma forma de pagamento"; paga com Pix, boleto ou cartão. Uma classe abstrata é uma mãe que serve só de base: ela não pode ser instanciada, e pode obrigar as filhas a escrever certos métodos.
- Declarar uma classe com
abstract class. - Escrever métodos abstratos: só a assinatura, sem corpo.
- Por que
newnuma classe abstrata dá erro (e qual erro). - O que acontece quando uma filha esquece um método abstrato.
- Misturar métodos prontos e abstratos (o padrão "método modelo").
- Quando usar uma classe abstrata.
A ideia: o formulário com campos obrigatórios
Pense num formulário de cadastro de fornecedor. Ele já vem com o cabeçalho, o logotipo da loja e as regras impressas (a parte pronta). Mas alguns campos estão em branco, marcados com *obrigatório: cada fornecedor tem que preencher. E o formulário em branco, sozinho, não vale como cadastro.
| No formulário | Na classe abstrata |
|---|---|
| O formulário em branco não vale como cadastro | Não dá para fazer new numa classe abstrata |
| Cabeçalho e regras já impressos | Métodos normais, com código, que as filhas herdam |
| Campos *obrigatórios em branco | Métodos abstratos: a mãe diz o nome; cada filha escreve o código |
| Cada formulário preenchido | Cada classe filha "concreta" (Pix, Boleto, Cartão) |
O problema: uma mãe que não deveria existir sozinha
Com o que você já sabe, a Ana poderia escrever uma classe Pagamento comum e criar filhas. Mas que taxa tem um pagamento "genérico"?
<?php
class Pagamento
{
public function __construct(public float $valor)
{
}
public function taxa(): float
{
return 0.0; // ??? que taxa tem um "pagamento genérico"?
}
}
$p = new Pagamento(100);
echo "Taxa: " . $p->taxa() . "\n";
$ php problema.php Taxa: 0
Dois problemas: alguém pode criar um new Pagamento(100), que não significa nada; e uma filha que esquecer de escrever o próprio taxa() herda o 0.0 "de mentira", sem nenhum aviso. A classe abstrata resolve os dois.
abstract class e métodos abstratos
<?php
abstract class Pagamento
{
public function __construct(public float $valor)
{
}
abstract public function nome(): string;
abstract public function taxa(): float;
public function total(): float
{
return round($this->valor + $this->taxa(), 2);
}
public function resumo(): string
{
return $this->nome() . ": " . $this->reais($this->valor)
. " + taxa " . $this->reais($this->taxa())
. " = " . $this->reais($this->total());
}
private function reais(float $valor): string
{
return "R$ " . number_format($valor, 2, ",", ".");
}
}
class Pix extends Pagamento
{
public function nome(): string
{
return "Pix";
}
public function taxa(): float
{
return 0.0;
}
}
class Boleto extends Pagamento
{
public function nome(): string
{
return "Boleto";
}
public function taxa(): float
{
return 2.50;
}
}
class Cartao extends Pagamento
{
public function __construct(float $valor, public int $parcelas = 1)
{
parent::__construct($valor);
}
public function nome(): string
{
return "Cartão {$this->parcelas}x";
}
public function taxa(): float
{
$percentual = $this->parcelas === 1 ? 3.0 : 5.0;
return $this->valor * $percentual / 100;
}
}
$pagamentos = [new Pix(100), new Boleto(100), new Cartao(100), new Cartao(100, 3)];
foreach ($pagamentos as $pagamento) {
echo $pagamento->resumo() . "\n";
}
$ php pagamentos.php Pix: R$ 100,00 + taxa R$ 0,00 = R$ 100,00 Boleto: R$ 100,00 + taxa R$ 2,50 = R$ 102,50 Cartão 1x: R$ 100,00 + taxa R$ 3,00 = R$ 103,00 Cartão 3x: R$ 100,00 + taxa R$ 5,00 = R$ 105,00
- abstract class Pagamento
abstract= "abstrata". A classe vira só um molde de moldes: não pode ser instanciada.- abstract public function taxa(): float;
- Um método abstrato: tem nome, parâmetros e tipo de retorno, mas nenhum corpo (termina com
;, sem chaves). É uma promessa: "toda filha concreta terá umtaxa()que devolvefloat". - public function total(), resumo()
- Métodos normais, com código. As filhas herdam prontos. Repare que
total()chama$this->taxa(), mesmo sem saber qual taxa é: na hora de rodar, vale otaxa()da filha. - public function __construct(…)
- Classes abstratas podem ter construtor e propriedades. As filhas herdam (o
Cartaotem o seu e chamaparent::__construct). - class Pix extends Pagamento
- Uma filha concreta (não abstrata). Ela é obrigada a escrever todos os métodos abstratos da mãe:
nome()etaxa().
Esse jeito de organizar tem nome: método modelo (template method). A mãe escreve o "roteiro" (resumo() e total()) e deixa lacunas (nome() e taxa()) que cada filha preenche. O roteiro fica num lugar só; mudou o formato do resumo, muda para todos.
Não dá para instanciar
<?php
abstract class Pagamento
{
public function __construct(public float $valor)
{
}
abstract public function taxa(): float;
}
$p = new Pagamento(100);
$ php instanciar.php
Fatal error: Uncaught Error: Cannot instantiate abstract class Pagamento in /home/ana/loja/instanciar.php:11
Stack trace:
#0 {main}
thrown in /home/ana/loja/instanciar.php on line 11
"Não é possível instanciar a classe abstrata Pagamento". O formulário em branco não vale como cadastro: crie uma das filhas concretas (new Pix(100)).
A filha que esqueceu um método
<?php
abstract class Pagamento
{
public function __construct(public float $valor)
{
}
abstract public function nome(): string;
abstract public function taxa(): float;
}
class Boleto extends Pagamento
{
public function nome(): string
{
return "Boleto";
}
}
echo "Oi\n";
$ php faltou.php
Fatal error: Class Boleto contains 1 abstract method and must therefore be declared abstract or implement the remaining method (Pagamento::taxa) in /home/ana/loja/faltou.php on line 13
Stack trace:
#0 {main}
Repare que o Oi da linha 21 nem apareceu: o PHP confere a promessa ao ler a classe, antes de rodar qualquer coisa. A mensagem diz: "a classe Boleto contém 1 método abstrato e por isso precisa ser declarada abstrata ou implementar o método que falta (Pagamento::taxa)". Implementar é o nome técnico para "escrever o corpo de um método prometido". É exatamente a proteção que faltava na versão sem abstract: esquecer a taxa agora é impossível.
Missão: a Ana passou a aceitar transferência. Rode o código e leia o erro: a classe Transferencia ainda não cumpre a promessa. Escreva nela os dois métodos que faltam: nome() devolve "TED" e taxa() devolve 1.00. A saída deve ser TED: R$ 50,00 + taxa R$ 1,00 = R$ 51,00.
<?php
abstract class Pagamento
{
public function __construct(public float $valor)
{
}
abstract public function nome(): string;
abstract public function taxa(): float;
public function resumo(): string
{
$f = fn(float $v): string => "R$ " . number_format($v, 2, ",", ".");
return $this->nome() . ": " . $f($this->valor) . " + taxa " . $f($this->taxa())
. " = " . $f($this->valor + $this->taxa());
}
}
class Transferencia extends Pagamento
{
// escreva nome() e taxa() aqui
}
echo (new Transferencia(50))->resumo() . "\n";
<?php
abstract class Pagamento
{
public function __construct(public float $valor)
{
}
abstract public function nome(): string;
abstract public function taxa(): float;
public function resumo(): string
{
$f = fn(float $v): string => "R$ " . number_format($v, 2, ",", ".");
return $this->nome() . ": " . $f($this->valor) . " + taxa " . $f($this->taxa())
. " = " . $f($this->valor + $this->taxa());
}
}
class Transferencia extends Pagamento
{
public function nome(): string
{
return "TED";
}
public function taxa(): float
{
return 1.00;
}
}
echo (new Transferencia(50))->resumo() . "\n";
(O $f = fn(…) => … do resumo() é uma arrow function que formata dinheiro, só para encurtar o exemplo.)
A abstrata como tipo
Você não cria objetos de Pagamento, mas pode (e deve) usar Pagamento como tipo de parâmetro. Assim a função aceita qualquer forma de pagamento, inclusive as que ainda nem foram inventadas:
<?php
abstract class Pagamento
{
public function __construct(public float $valor)
{
}
abstract public function taxa(): float;
}
class Pix extends Pagamento
{
public function taxa(): float
{
return 0.0;
}
}
function finalizarCompra(Pagamento $pagamento): void
{
$total = $pagamento->valor + $pagamento->taxa();
echo get_class($pagamento) . ": total R$ " . number_format($total, 2, ",", ".") . "\n";
}
finalizarCompra(new Pix(79.30));
var_dump(new Pix(1) instanceof Pagamento);
finalizarCompra("Pix");
$ php tipo.php
Pix: total R$ 79,30
bool(true)
Fatal error: Uncaught TypeError: finalizarCompra(): Argument #1 ($pagamento) must be of type Pagamento, string given, called in /home/ana/loja/tipo.php on line 27 and defined in /home/ana/loja/tipo.php:19
Stack trace:
#0 /home/ana/loja/tipo.php(27): finalizarCompra('Pix')
#1 {main}
thrown in /home/ana/loja/tipo.php on line 19
A função finalizarCompra não sabe nem quer saber se é Pix ou cartão: ela sabe que todo Pagamento tem valor e taxa(), porque a classe abstrata garante. Um texto ("Pix") é recusado com TypeError.
Variações
Uma abstrata filha de outra abstrata
Uma classe abstrata pode estender outra e implementar só parte dos métodos. Aqui, PagamentoSemTaxa resolve a taxa para todos os pagamentos sem taxa, mas deixa o nome() em aberto:
<?php
abstract class Pagamento
{
abstract public function nome(): string;
abstract public function taxa(): float;
}
abstract class PagamentoSemTaxa extends Pagamento
{
public function taxa(): float
{
return 0.0;
}
}
class Pix extends PagamentoSemTaxa
{
public function nome(): string
{
return "Pix";
}
}
class Dinheiro extends PagamentoSemTaxa
{
public function nome(): string
{
return "Dinheiro na entrega";
}
}
foreach ([new Pix(), new Dinheiro()] as $p) {
echo $p->nome() . ": taxa " . $p->taxa() . "\n";
}
$ php abstrata-filha.php Pix: taxa 0 Dinheiro na entrega: taxa 0
Uma abstrata sem nenhum método abstrato
Também vale: abstract aí serve só para proibir o new numa classe que é apenas base de código comum:
<?php
abstract class Modelo
{
public function salvar(): string
{
return "Salvando " . static::class;
}
}
class Cliente extends Modelo
{
}
echo (new Cliente())->salvar() . "\n";
$ php abstrata-sem-metodos.php Salvando Cliente
(O static::class é o nome da classe que foi realmente usada, como você viu em static.)
Quando usar uma classe abstrata
| Situação | Use |
|---|---|
| Várias classes "são um" mesmo tipo de coisa, compartilham código e cada uma muda alguns detalhes | Classe abstrata com os detalhes como métodos abstratos |
A mãe faz sentido sozinha (um Produto comum existe) | Classe normal + herança |
| Só quer garantir que as classes tenham certos métodos, sem código comum | Interface |
| A classe precisa "ser" duas coisas ao mesmo tempo | Interfaces (uma classe só pode ter uma mãe, abstrata ou não) |
Uma comparação detalhada entre classe abstrata e interface está na próxima aula, Interfaces.
Quando dá errado
O que significa: new numa classe abstrata (exemplo acima). Como consertar: crie um objeto de uma filha concreta. Se não existe nenhuma, crie uma.
O que significa: a filha não escreveu um dos métodos abstratos da mãe (o nome dele aparece entre parênteses no fim da mensagem, como (Pagamento::taxa)). Como consertar: escreva o método na filha, com a mesma assinatura. Se a filha também deve ser só uma base, declare-a abstract.
<?php
abstract class Pagamento
{
abstract public function taxa(): float
{
return 0.0;
}
}
$ php corpo.php
Fatal error: Abstract function Pagamento::taxa() cannot contain body in /home/ana/loja/corpo.php on line 4
Stack trace:
#0 {main}
O que significa: um método abstrato não pode ter chaves com código: ele é só a promessa. Como consertar: se existe um código padrão que serve para a maioria, tire o abstract do método (as filhas podem sobrescrever). Se não existe, termine a linha com ;: abstract public function taxa(): float;.
<?php
class Pagamento
{
abstract public function taxa(): float;
}
$ php sem-abstract.php
Fatal error: Class Pagamento declares abstract method taxa() and must therefore be declared abstract in /home/ana/loja/sem-abstract.php on line 4
Stack trace:
#0 {main}
O que significa: só uma classe abstrata pode ter métodos abstratos. Como consertar: abstract class Pagamento.
<?php
abstract class Pagamento
{
abstract public function taxa(): float;
}
class Pix extends Pagamento
{
public function taxa(): string
{
return "zero";
}
}
$ php assinatura-errada.php
Fatal error: Declaration of Pix::taxa(): string must be compatible with Pagamento::taxa(): float in /home/ana/loja/assinatura-errada.php on line 9
Stack trace:
#0 {main}
O que significa: a promessa era devolver float, e a filha devolve string. As mesmas regras de compatibilidade da herança valem aqui. Como consertar: siga exatamente a assinatura do método abstrato.
Referência
| Escrita | O que faz |
|---|---|
abstract class X { } | Classe que não pode ser instanciada; só serve de mãe |
abstract public function f(): tipo; | Método sem corpo que toda filha concreta precisa implementar |
abstract protected function f(): tipo; | O mesmo, para métodos de uso interno (as filhas implementam, ninguém de fora chama) |
class Y extends X { … } | Filha concreta: implementa todos os abstratos |
abstract class Z extends X { … } | Filha abstrata: pode deixar abstratos para as netas |
function f(X $obj) | Aceita qualquer filha concreta de X |
Missão final
Missão: a lojinha avisa o cliente quando o pedido sai. Transforme Notificacao numa classe abstrata com o método abstrato abstract protected function canal(): string; (o enviar() já está pronto e usa canal()). Depois crie as filhas Email (canal "E-mail") e Sms (canal "SMS"). A saída deve ser [E-mail] para ana@exemplo.com: Pedido enviado! e [SMS] para 81 99999-0000: Pedido enviado!.
<?php
class Notificacao
{
public function __construct(public string $destino)
{
}
public function enviar(string $mensagem): string
{
return "[{$this->canal()}] para {$this->destino}: $mensagem";
}
}
// crie as classes Email e Sms aqui
$avisos = [new Email("ana@exemplo.com"), new Sms("81 99999-0000")];
foreach ($avisos as $aviso) {
echo $aviso->enviar("Pedido enviado!") . "\n";
}
<?php
abstract class Notificacao
{
public function __construct(public string $destino)
{
}
abstract protected function canal(): string;
public function enviar(string $mensagem): string
{
return "[{$this->canal()}] para {$this->destino}: $mensagem";
}
}
class Email extends Notificacao
{
protected function canal(): string
{
return "E-mail";
}
}
class Sms extends Notificacao
{
protected function canal(): string
{
return "SMS";
}
}
$avisos = [new Email("ana@exemplo.com"), new Sms("81 99999-0000")];
foreach ($avisos as $aviso) {
echo $aviso->enviar("Pedido enviado!") . "\n";
}
Exercícios
1. Declarar a abstrata
Complete para que Pagamento não possa ser instanciada:
2. O método abstrato
Complete a declaração do método abstrato taxa, público, que devolve float:
3. O que falta
Como termina a linha de um método abstrato (depois do tipo de retorno)?
4. O que acontece com new Pagamento(100), se Pagamento é abstrata?
5. Uma classe abstrata pode ter…
6. Uma filha concreta não escreveu um dos métodos abstratos da mãe. Quando o PHP avisa?
7. Uma função declara o parâmetro Pagamento $p (classe abstrata). O que ela aceita?
Resumo
abstract classé uma mãe que só serve de base:newnela dáError: Cannot instantiate abstract class.- Um método abstrato (
abstract public function f(): tipo;) é uma promessa sem corpo; toda filha concreta precisa implementá-lo, ou o PHP recusa a classe antes de rodar. - A abstrata pode ter propriedades, construtor e métodos prontos que usam os abstratos: o padrão método modelo.
- Use a abstrata como tipo de parâmetro para aceitar qualquer filha.
- Abstrata = código comum + lacunas obrigatórias. Só lacunas, sem código comum? Prefira uma interface.