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.

Você vai aprender
  • Declarar uma classe com abstract class.
  • Escrever métodos abstratos: só a assinatura, sem corpo.
  • Por que new numa 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árioNa classe abstrata
O formulário em branco não vale como cadastroNão dá para fazer new numa classe abstrata
Cabeçalho e regras já impressosMétodos normais, com código, que as filhas herdam
Campos *obrigatórios em brancoMétodos abstratos: a mãe diz o nome; cada filha escreve o código
Cada formulário preenchidoCada 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á um taxa() que devolve float".
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 o taxa() da filha.
public function __construct(…)
Classes abstratas podem ter construtor e propriedades. As filhas herdam (o Cartao tem o seu e chama parent::__construct).
class Pix extends Pagamento
Uma filha concreta (não abstrata). Ela é obrigada a escrever todos os métodos abstratos da mãe: nome() e taxa().

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çãoUse
Várias classes "são um" mesmo tipo de coisa, compartilham código e cada uma muda alguns detalhesClasse 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 comumInterface
A classe precisa "ser" duas coisas ao mesmo tempoInterfaces (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

Cannot instantiate abstract class

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.

Class … contains 1 abstract method and must therefore be declared abstract or implement the remaining method

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.

Abstract function … cannot contain body
<?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;.

… declares abstract method … and must therefore be declared abstract
<?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.

A filha mudou o tipo de retorno
<?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

EscritaO 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:

Pagamento { }

2. O método abstrato

Complete a declaração do método abstrato taxa, público, que devolve float:

taxa(): float;

3. O que falta

Como termina a linha de um método abstrato (depois do tipo de retorno)?

abstract public function nome(): string

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: new nela 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.