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.
- Criar uma classe filha com
extendse 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
protectedeprivatena 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 deProduto, 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 objetosProdutoDigital, vale este; para osProdutocomuns, 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.
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:
| Regra | Exemplo errado | Mensagem real |
|---|---|---|
| A visibilidade não pode ficar mais fechada | Mã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 aumentar | Mã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ãe | Mã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 "oOverridedo 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ção | Use |
|---|---|
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 base | Classe 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
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);.
<?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.
<?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).
<?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.
<?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
| Escrita | O que faz |
|---|---|
class Filha extends Mae { } | Cria uma classe filha que herda tudo de Mae |
| Método com o mesmo nome na filha | Sobrescreve o da mãe |
parent::metodo() | Chama a versão da mãe |
parent::__construct(…) | Chama o construtor da mãe |
protected | Visível na classe e nas filhas |
final public function f() | Método que não pode ser sobrescrito |
final class X | Classe 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:
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:
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(…). protectedpassa para as filhas;privatenão.- Ao sobrescrever, mantenha a assinatura compatível e a visibilidade igual ou mais aberta.
finalproí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.