PHP Herança

A lojinha da Ana começou a vender e-books. Um e-book é um produto como qualquer outro (tem nome, preço, descrição), mas com uma diferença: não paga frete. Copiar a classe Produto inteira só para mudar o frete seria repetir código. Com herança, a Ana cria uma classe ProdutoDigital que aproveita tudo de Produto e muda só o que é diferente.

Você vai aprender
  • Criar uma classe filha com extends e o que ela herda.
  • Sobrescrever um método e reaproveitar o original com parent::.
  • Chamar o construtor da mãe com parent::__construct().
  • A diferença entre protected e private na prática.
  • Proibir mudanças com final.
  • Proteger-se de erros de digitação com o atributo #[\Override] do PHP 8.3.

A ideia: "é um"

Herança descreve uma relação de "é um": um e-book é um produto; um produto frágil é um produto físico. A classe nova (a filha, ou subclasse) recebe tudo da classe original (a mãe, ou superclasse, classe pai) e pode:

  • acrescentar coisas novas (o e-book tem tamanho em MB);
  • substituir o que é diferente (o frete do e-book é zero).

É como uma receita de família: a filha recebe o caderno de receitas da mãe, usa a maioria como está, troca o açúcar por mel numa delas e acrescenta receitas novas.


extends: a filha recebe tudo

<?php
class Produto
{
    public function __construct(
        public string $nome,
        public float $preco,
    ) {
    }

    public function frete(): float
    {
        return 15.0;
    }

    public function descricao(): string
    {
        return "{$this->nome} - R$ " . number_format($this->preco, 2, ",", ".");
    }
}

class ProdutoDigital extends Produto
{
}

$ebook = new ProdutoDigital("E-book de receitas", 19.90);
echo $ebook->descricao() . "\n";
echo "Frete: " . $ebook->frete() . "\n";
var_dump($ebook instanceof ProdutoDigital, $ebook instanceof Produto);
$ php basico.php
E-book de receitas - R$ 19,90     « descricao() veio da mãe
Frete: 15                         « frete() também (ainda não mudamos)
bool(true)                        « o e-book é um ProdutoDigital…
bool(true)                        « …e também é um Produto
class ProdutoDigital
A classe nova, a filha.
extends Produto
extends = "estende", "amplia". A filha recebe todas as propriedades, métodos e constantes de Produto, inclusive o construtor.
{ }
Mesmo vazia, a classe filha já funciona: new ProdutoDigital("…", 19.90) usou o construtor da mãe.

O instanceof (de Classes e Objetos) confirma a relação "é um": um objeto da filha é também um objeto da mãe. Por isso, uma função que pede Produto $p aceita um ProdutoDigital sem reclamar.


Sobrescrever métodos e usar parent::

Para mudar um comportamento, a filha declara um método com o mesmo nome do da mãe. Isso se chama sobrescrever (override). Para aproveitar o método original dentro do novo, use parent:: ("a mãe"):

<?php
class Produto
{
    public function __construct(
        public string $nome,
        public float $preco,
    ) {
    }

    public function frete(): float
    {
        return 15.0;
    }

    public function descricao(): string
    {
        return "{$this->nome} - R$ " . number_format($this->preco, 2, ",", ".");
    }
}

class ProdutoDigital extends Produto
{
    public function __construct(
        string $nome,
        float $preco,
        public float $tamanhoMb,
    ) {
        parent::__construct($nome, $preco);
    }

    public function frete(): float
    {
        return 0.0;
    }

    public function descricao(): string
    {
        return parent::descricao() . " (download, {$this->tamanhoMb} MB)";
    }
}

$caneca = new Produto("Caneca", 25.90);
$ebook = new ProdutoDigital("E-book de receitas", 19.90, 4.5);

foreach ([$caneca, $ebook] as $produto) {
    echo $produto->descricao() . " | frete: " . $produto->frete() . "\n";
}
$ php sobrescrever.php
Caneca - R$ 25,90 | frete: 15
E-book de receitas - R$ 19,90 (download, 4.5 MB) | frete: 0
public function frete(): float { return 0.0; }
Substitui o frete() da mãe. Para objetos ProdutoDigital, vale este; para os Produto comuns, continua valendo o da mãe.
parent::descricao()
Chama a versão da mãe e acrescenta um pedaço. Sem o parent::, $this->descricao() chamaria o próprio método da filha, que chamaria ele mesmo de novo, para sempre.
parent::__construct($nome, $preco)
O construtor da filha recebe os dados da mãe e os repassa para o construtor dela. Depois, a promoção cuida do tamanhoMb, que é só da filha.
string $nome, float $preco (sem public)
Na filha, esses dois são parâmetros comuns: as propriedades já são declaradas (e promovidas) na mãe.

Repare no foreach: o mesmo código ($produto->frete()) deu 15 para a caneca e 0 para o e-book. Cada objeto respondeu do seu jeito à mesma chamada. Essa é a ideia de polimorfismo, um dos pilares da POO, que você vai aprofundar em Interfaces.

Se a filha tem construtor, chame o da mãe

Quando a filha declara o próprio __construct, o da mãe não roda sozinho. Esquecer o parent::__construct(…) deixa as propriedades da mãe sem valor (veja o erro em Quando dá errado).

Missão: produtos físicos pagam R$ 2,00 a mais de frete por quilo. Crie a classe ProdutoFisico, filha de Produto, com a propriedade promovida public float $pesoKg no construtor e um frete() que devolve parent::frete() + 2 * $this->pesoKg. A saída deve ser Frete da caneca: 16 e Frete do vaso: 21.

<?php
class Produto
{
    public function frete(): float
    {
        return 15.0;
    }
}

// crie a classe ProdutoFisico aqui


$caneca = new ProdutoFisico(0.5);
$vaso = new ProdutoFisico(3);
echo "Frete da caneca: " . $caneca->frete() . "\n";
echo "Frete do vaso: " . $vaso->frete() . "\n";
<?php
class Produto
{
    public function frete(): float
    {
        return 15.0;
    }
}

class ProdutoFisico extends Produto
{
    public function __construct(public float $pesoKg)
    {
    }

    public function frete(): float
    {
        return parent::frete() + 2 * $this->pesoKg;
    }
}

$caneca = new ProdutoFisico(0.5);
$vaso = new ProdutoFisico(3);
echo "Frete da caneca: " . $caneca->frete() . "\n";
echo "Frete do vaso: " . $vaso->frete() . "\n";

Herança em vários níveis

Uma filha pode ter filhas. Cada nível acrescenta o seu pedaço, e parent:: sempre chama o nível logo acima:

<?php
class Produto
{
    public function frete(): float { return 15.0; }
}
class ProdutoFisico extends Produto
{
    public function __construct(public float $pesoKg) {}

    public function frete(): float
    {
        return parent::frete() + 2.0 * $this->pesoKg;
    }
}
class ProdutoFragil extends ProdutoFisico
{
    public function frete(): float
    {
        return parent::frete() + 10.0;
    }
}

$vaso = new ProdutoFragil(3);
echo "Frete do vaso: " . $vaso->frete() . "\n";
var_dump($vaso instanceof Produto);
echo get_parent_class($vaso) . "\n";
print_r(class_parents($vaso));
$ php cadeia.php
Frete do vaso: 31        « 15 (Produto) + 2 × 3 kg (ProdutoFisico) + 10 (ProdutoFragil)
bool(true)               « um ProdutoFragil é um Produto, dois níveis acima
ProdutoFisico            « get_parent_class: a mãe direta
Array
(
    [ProdutoFisico] => ProdutoFisico
    [Produto] => Produto
)

No PHP, cada classe tem uma mãe só (herança simples): class Curso extends Produto, Servico é erro de sintaxe. Para uma classe "ser" várias coisas ao mesmo tempo, o PHP usa interfaces; para reaproveitar código de vários lugares, traits. E não exagere nos níveis: mais de dois ou três costuma deixar o código difícil de seguir.


protected × private na herança

Em Visibilidade você viu que protected é "para a família". Aqui está a diferença na prática:

<?php
class Produto
{
    protected int $estoque = 10;
    private float $custo = 8.0;

    public function __construct(public string $nome)
    {
    }
}

class ProdutoFisico extends Produto
{
    public function situacao(): string
    {
        return "{$this->nome}: {$this->estoque} em estoque";
    }

    public function lucro(): float
    {
        return 25.90 - $this->custo;
    }
}

$caneca = new ProdutoFisico("Caneca");
echo $caneca->situacao() . "\n";
echo $caneca->lucro() . "\n";
$ php protegido.php
Caneca: 10 em estoque     « protected: a filha enxerga

Warning: Undefined property: ProdutoFisico::$custo in /home/ana/loja/protegido.php on line 21
25.9                      « private: para a filha, $custo nem existe (a conta usou null)

O private da mãe é invisível para a filha: o PHP diz que a propriedade é "indefinida" e segue com null, produzindo um resultado errado. Regra prática: use private por padrão e passe para protected só o que as filhas realmente precisam. Ou, melhor ainda, ofereça um método protected que a filha possa chamar.


Regras ao sobrescrever

Como um objeto da filha pode ser usado em qualquer lugar onde se espera a mãe, o PHP exige que o método da filha seja compatível com o da mãe:

RegraExemplo erradoMensagem real
A visibilidade não pode ficar mais fechadaMãe: public function frete(); filha: private function frete()Access level to ProdutoDigital::frete() must be public (as in class Produto)
Os parâmetros obrigatórios não podem aumentarMãe: frete(); filha: frete(float $pesoKg)Declaration of ProdutoFisico::frete(float $pesoKg): float must be compatible with Produto::frete(): float
O tipo de retorno tem que "caber" no da mãeMãe: : float; filha: : string(a mesma mensagem: must be compatible)

Um parâmetro novo com valor padrão é permitido (frete(float $pesoKg = 0)), porque quem chama frete() sem argumentos continua funcionando.


final: "daqui não passa"

Às vezes a Ana quer proibir mudanças. A palavra final faz isso:

  • final public function total(): nenhuma filha pode sobrescrever este método.
  • final class Dinheiro: ninguém pode criar filhas desta classe.
<?php
class Pedido
{
    final public function total(): float
    {
        return 100.0;
    }
}

class PedidoEspertinho extends Pedido
{
    public function total(): float
    {
        return 0.0;
    }
}
$ php final-metodo.php

Fatal error: Cannot override final method Pedido::total() in /home/ana/loja/final-metodo.php on line 12
Stack trace:
#0 {main}
<?php
final class Dinheiro
{
}

class MeuDinheiro extends Dinheiro
{
}
$ php final-classe.php

Fatal error: Class MeuDinheiro cannot extend final class Dinheiro in /home/ana/loja/final-classe.php on line 6
Stack trace:
#0 {main}

Os dois erros aparecem antes de o programa rodar. Use final quando uma regra não pode ser trocada (o cálculo do total de um pedido) ou quando a classe não foi pensada para ter filhas. Muitos projetos modernos declaram as classes final por padrão e só "abrem" quando há motivo. (Constantes de classe também aceitam final: final public const ….)


#[\Override]: "isto sobrescreve, confira!" PHP 8.3+

Existe um erro silencioso perigoso na herança: errar o nome do método que você queria sobrescrever. Olhe:

<?php
class Produto
{
    public function frete(): float
    {
        return 15.0;
    }
}

class ProdutoDigital extends Produto
{
    public function fretes(): float
    {
        return 0.0;
    }
}

echo (new ProdutoDigital())->frete() . "\n";
$ php sem-override-erro.php
15                « o e-book pagou frete! fretes() é um método NOVO, não substituiu frete()

Nenhum erro, nenhum aviso: fretes() (com s) virou um método a mais, e o frete() da mãe continuou valendo. O atributo #[\Override] resolve isso: ele diz ao PHP "este método tem que sobrescrever um da mãe; se não sobrescrever, me avise":

<?php
class Produto
{
    public function frete(): float
    {
        return 15.0;
    }
}

class ProdutoDigital extends Produto
{
    #[\Override]
    public function fretes(): float
    {
        return 0.0;
    }
}

echo (new ProdutoDigital())->frete() . "\n";
$ php override-erro.php

Fatal error: ProdutoDigital::fretes() has #[\Override] attribute, but no matching parent method exists in /home/ana/loja/override-erro.php on line 13
Stack trace:
#0 {main}

"ProdutoDigital::fretes() tem o atributo #[\Override], mas não existe método correspondente na mãe". O erro aparece antes de o programa rodar, apontando a linha. Com o nome certo, tudo funciona:

<?php
class Produto
{
    public function frete(): float
    {
        return 15.0;
    }
}

class ProdutoDigital extends Produto
{
    #[\Override]
    public function frete(): float
    {
        return 0.0;
    }
}

echo (new ProdutoDigital())->frete() . "\n";
$ php override.php
0
#[ … ]
A sintaxe de um atributo: uma "etiqueta" com informação extra para o PHP (veja Atributos).
\Override
O nome do atributo. A barra \ diz "o Override do próprio PHP" (assunto de Namespaces); sem namespace, #[Override] também funciona, mas #[\Override] funciona sempre.

O #[\Override] também protege o futuro: se um dia alguém renomear o frete() da mãe, a filha avisa na hora, em vez de passar a ser um método esquecido. Em PHP 8.2 ou anterior, o atributo é simplesmente ignorado (sem erro), então você pode usá-lo sem medo.


Quando usar herança (e quando não)

SituaçãoUse
A nova classe é um tipo da outra e muda pouca coisa (ProdutoDigital é um Produto)Herança (extends)
Uma classe tem outra (um Pedido tem Produtos, tem um Cliente)Uma propriedade com o objeto (isso se chama composição), não herança
Classes diferentes precisam prometer os mesmos métodos (pagar() no Pix e no boleto)Interface
Uma "mãe" que nunca deve ser criada sozinha, só servir de baseClasse abstrata
O mesmo pedaço de código em classes sem relação de "é um"Trait

O teste rápido: leia em voz alta "um X é um Y". "Um pedido é um cliente" soa errado, então Pedido extends Cliente está errado, mesmo que poupe código.


Quando dá errado

Esquecer o parent::__construct
<?php
class Produto
{
    public function __construct(
        public string $nome,
        public float $preco,
    ) {
    }
}

class ProdutoDigital extends Produto
{
    public function __construct(
        string $nome,
        float $preco,
        public float $tamanhoMb,
    ) {
    }
}

$ebook = new ProdutoDigital("E-book", 19.90, 4.5);
echo $ebook->nome;
$ php sem-parent-construct.php

Fatal error: Uncaught Error: Typed property Produto::$nome must not be accessed before initialization in /home/ana/loja/sem-parent-construct.php:22
Stack trace:
#0 {main}
  thrown in /home/ana/loja/sem-parent-construct.php on line 22

O que significa: a filha tem o próprio construtor, que recebeu $nome e $preco e não fez nada com eles. O construtor da mãe (que guardaria esses valores) nunca rodou, e Produto::$nome ficou sem valor.

Como consertar: primeira linha do construtor da filha: parent::__construct($nome, $preco);.

Declaration of … must be compatible with …
<?php
class Produto
{
    public function frete(): float
    {
        return 15.0;
    }
}

class ProdutoFisico extends Produto
{
    public function frete(float $pesoKg): float
    {
        return 15.0 + $pesoKg;
    }
}
$ php assinatura.php

Fatal error: Declaration of ProdutoFisico::frete(float $pesoKg): float must be compatible with Produto::frete(): float in /home/ana/loja/assinatura.php on line 12
Stack trace:
#0 {main}

O que significa: a filha mudou a "assinatura" do método (acrescentou um parâmetro obrigatório). Um código que chama $produto->frete() sem argumentos quebraria com um ProdutoFisico.

Como consertar: mantenha a assinatura da mãe. Se a filha precisa de um dado a mais, guarde-o numa propriedade (como o pesoKg da missão) ou dê um valor padrão ao parâmetro novo.

Access level to … must be public
<?php
class Produto
{
    public function frete(): float
    {
        return 15.0;
    }
}

class ProdutoDigital extends Produto
{
    private function frete(): float
    {
        return 0.0;
    }
}
$ php visibilidade-menor.php

Fatal error: Access level to ProdutoDigital::frete() must be public (as in class Produto) in /home/ana/loja/visibilidade-menor.php on line 12
Stack trace:
#0 {main}

O que significa: a filha tentou esconder um método que a mãe deixava público. Como consertar: mantenha public (a filha pode abrir mais, de protected para public, mas nunca fechar).

Duas mães
<?php
class Produto {}
class Servico {}
class Curso extends Produto, Servico {}
$ php duas-maes.php

Parse error: syntax error, unexpected token ",", expecting "{" in /home/ana/loja/duas-maes.php on line 4

O que significa: o PHP esperava a chave { logo depois do nome da mãe: só cabe uma. Como consertar: escolha uma mãe e use interfaces (que podem ser várias) ou traits para o resto.

A mãe não existe
<?php
class ProdutoDigital extends Produtos
{
}
$ php pai-nao-existe.php

Fatal error: Uncaught Error: Class "Produtos" not found in /home/ana/loja/pai-nao-existe.php:2
Stack trace:
#0 {main}
  thrown in /home/ana/loja/pai-nao-existe.php on line 2

O que significa: o nome da mãe está errado (Produtos), ou o arquivo dela não foi incluído antes. Como consertar: confira o nome e inclua o arquivo da mãe (com require_once ou autoload).


Referência

EscritaO que faz
class Filha extends Mae { }Cria uma classe filha que herda tudo de Mae
Método com o mesmo nome na filhaSobrescreve o da mãe
parent::metodo()Chama a versão da mãe
parent::__construct(…)Chama o construtor da mãe
protectedVisível na classe e nas filhas
final public function f()Método que não pode ser sobrescrito
final class XClasse que não pode ter filhas
#[\Override] PHP 8.3+Garante que o método sobrescreve um da mãe
get_parent_class($obj), class_parents($obj)A mãe direta / todos os ancestrais

Missão final

Missão: a Ana lançou uma assinatura mensal de café. Crie a classe Assinatura, filha de Produto, que sobrescreve dois métodos, ambos marcados com #[\Override]: frete() devolve 0.0, e descricao() devolve a descrição da mãe (parent::descricao()) seguida de /mês. A saída deve ter Clube do Café - R$ 39,90/mês | frete: 0.

<?php
class Produto
{
    public function __construct(
        public string $nome,
        public float $preco,
    ) {
    }

    public function frete(): float
    {
        return 15.0;
    }

    public function descricao(): string
    {
        return "{$this->nome} - R$ " . number_format($this->preco, 2, ",", ".");
    }
}

// crie a classe Assinatura aqui


$itens = [new Produto("Caneca", 25.90), new Assinatura("Clube do Café", 39.90)];
foreach ($itens as $item) {
    echo $item->descricao() . " | frete: " . $item->frete() . "\n";
}
<?php
class Produto
{
    public function __construct(
        public string $nome,
        public float $preco,
    ) {
    }

    public function frete(): float
    {
        return 15.0;
    }

    public function descricao(): string
    {
        return "{$this->nome} - R$ " . number_format($this->preco, 2, ",", ".");
    }
}

class Assinatura extends Produto
{
    #[\Override]
    public function frete(): float
    {
        return 0.0;
    }

    #[\Override]
    public function descricao(): string
    {
        return parent::descricao() . "/mês";
    }
}

$itens = [new Produto("Caneca", 25.90), new Assinatura("Clube do Café", 39.90)];
foreach ($itens as $item) {
    echo $item->descricao() . " | frete: " . $item->frete() . "\n";
}

Exercícios

1. Criar a filha

Complete para Ebook herdar de Produto:

class Ebook { }

2. Construtor da mãe

Complete a primeira linha do construtor da filha para repassar $nome e $preco à mãe:

;

3. Proibir filhas

Complete para que ninguém possa estender a classe Dinheiro:

class Dinheiro { }

4. A mãe tem private float $custo. O que acontece quando um método da filha lê $this->custo?

5. Para que serve o #[\Override]?

6. Qual relação não deve ser feita com herança?

7. $e = new ProdutoDigital(…), filha de Produto. Quanto vale $e instanceof Produto?


Resumo

  • class Filha extends Mae: a filha herda propriedades, métodos e constantes, e "é um" objeto da mãe também (instanceof).
  • Método com o mesmo nome na filha sobrescreve o da mãe; parent::metodo() chama a versão original.
  • Se a filha tem construtor, chame parent::__construct(…).
  • protected passa para as filhas; private não.
  • Ao sobrescrever, mantenha a assinatura compatível e a visibilidade igual ou mais aberta.
  • final proíbe sobrescrever (método) ou estender (classe).
  • #[\Override] (8.3+) transforma um erro de digitação silencioso num erro claro.
  • Herança é para "é um". Para "tem um", use composição; para várias "capacidades", interfaces.