No artigo anterior aprendemos a criar classes com propriedades, métodos e encapsulamento. Agora damos o próximo passo: como relacionar classes entre si para reutilizar código sem duplicação. O PHP oferece três mecanismos fundamentais — herança com extends, contratos com interface e reutilização horizontal com trait.
Cada um resolve um problema diferente. Usar o mecanismo errado gera código rígido, difícil de testar e de estender. Entender quando usar cada um é uma das habilidades centrais do design OOP.
Os três mecanismos — visão geral
| extends · Herança | interface · Contrato | trait · Mixin |
|---|---|---|
| ✓ Herda propriedades e métodos | ✓ Define contrato de métodos | ✓ Reutiliza código entre classes |
| ✓ Pode sobrescrever comportamento | ✓ Múltiplas por classe | ✓ Múltiplos por classe |
| ✓ Define tipo — "é um" | ✓ Define tipo — "age como" | ✓ Qualquer visibilidade |
| ✗ Apenas uma classe pai | ✗ Sem implementação de código | ✗ Não define tipo |
| ✗ Acopla hierarquia fortemente | ✗ Apenas métodos públicos | ✗ Não pode ser instanciado |
Herança com extends
Herança permite que uma classe filha herde todas as propriedades e métodos public e protected da classe pai. A filha pode adicionar comportamentos novos ou sobrescrever os existentes. O operador parent:: acessa a implementação original do pai:
<?php
declare(strict_types=1);
class Funcionario
{
public function __construct(
protected readonly string $nome, // protected = acessível nas filhas
protected float $salarioBase,
) {}
public function calcularSalario(): float
{
return $this->salarioBase;
}
public function apresentar(): string
{
// Chama calcularSalario() que pode ser sobrescrito pelas filhas
$s = number_format($this->calcularSalario(), 2, ',', '.');
return "{$this->nome} — R$ {$s}";
}
}
class Gerente extends Funcionario
{
public function __construct(
string $nome,
float $salarioBase,
private float $bonus,
) {
parent::__construct($nome, $salarioBase); // chama o construtor do pai
}
// Sobrescreve — adiciona o bônus ao cálculo base
public function calcularSalario(): float
{
return parent::calcularSalario() + $this->bonus;
}
}
class Estagiario extends Funcionario
{
public function calcularSalario(): float
{
return parent::calcularSalario() * 0.6; // 60% do salário base
}
}
$ana = new Gerente("Ana", 8000.0, 2000.0);
$bruno = new Estagiario("Bruno", 2000.0);
echo $ana->apresentar(); // Ana — R$ 10.000,00
echo $bruno->apresentar(); // Bruno — R$ 1.200,00
// instanceof percorre toda a cadeia de herança
var_dump($ana instanceof Gerente); // true
var_dump($ana instanceof Funcionario); // true — herança inclui o pai
var_dump($ana instanceof Estagiario); // false
Classes abstratas
Uma classe abstrata não pode ser instanciada diretamente — existe apenas como base para extensão. Métodos abstratos declaram a assinatura sem corpo, obrigando cada subclasse a fornecer a sua própria implementação. Isso garante o cumprimento do contrato em tempo de definição, não em tempo de execução:
<?php
declare(strict_types=1);
abstract class Forma
{
public function __construct(
protected readonly string $cor = "branco",
) {}
// abstract — assinatura sem corpo — toda subclasse DEVE implementar
abstract public function area(): float;
abstract public function perimetro(): float;
// Método concreto — compartilhado por todas as subclasses
// Chama area() e perimetro() que cada subclasse fornece
public function descricao(): string
{
return sprintf(
"%s %s — área: %.2f, perímetro: %.2f",
$this->cor,
get_class($this),
$this->area(),
$this->perimetro()
);
}
}
class Circulo extends Forma
{
public function __construct(
private readonly float $raio,
string $cor = "branco",
) {
parent::__construct($cor);
}
public function area(): float { return M_PI * $this->raio ** 2; }
public function perimetro(): float { return 2 * M_PI * $this->raio; }
}
class Retangulo extends Forma
{
public function __construct(
private readonly float $largura,
private readonly float $altura,
string $cor = "branco",
) {
parent::__construct($cor);
}
public function area(): float { return $this->largura * $this->altura; }
public function perimetro(): float { return 2 * ($this->largura + $this->altura); }
}
// new Forma(); // Fatal error: Cannot instantiate abstract class
$formas = [new Circulo(5.0, "azul"), new Retangulo(4.0, 6.0, "vermelho")];
// Polimorfismo — mesmo código funciona para qualquer Forma concreta
foreach ($formas as $forma) {
echo $forma->descricao() . "\n";
}
// azul Circulo — área: 78.54, perímetro: 31.42
// vermelho Retangulo — área: 24.00, perímetro: 20.00
💡 Classe abstrata vs Interface — quando usar cada uma
Use classe abstrata quando as subclasses compartilham código concreto (propriedades, métodos com implementação) além do contrato — comodescricao()acima. Use interface quando você só precisa garantir que certos métodos existem, sem compartilhar nenhuma implementação — especialmente entre classes não relacionadas. Se você está duplicando código entre classes, considere uma abstrata. Se você quer garantir compatibilidade de API entre classes distintas, use interface.
Interfaces — contratos puros
Uma interface define um contrato: qualquer classe que a implemente com implements deve ter todos os métodos declarados, com as mesmas assinaturas. Uma classe pode implementar múltiplas interfaces, e interfaces podem estender outras interfaces:
<?php
declare(strict_types=1);
interface Exportavel
{
public function exportarJson(): string;
public function exportarCsv(): string;
}
interface Auditavel
{
public function getCriadoEm(): DateTimeImmutable;
public function getAtualizadoEm(): DateTimeImmutable;
}
// Interface pode estender outra — herda o contrato completo
interface Persistivel extends Auditavel
{
public function getId(): int;
}
// Produto implementa duas interfaces ao mesmo tempo
class Produto implements Exportavel, Persistivel
{
private DateTimeImmutable $criadoEm;
private DateTimeImmutable $atualizadoEm;
public function __construct(
private int $id,
private readonly string $nome,
private float $preco,
) {
$this->criadoEm = new DateTimeImmutable();
$this->atualizadoEm = new DateTimeImmutable();
}
public function exportarJson(): string
{
return json_encode(["id" => $this->id, "nome" => $this->nome, "preco" => $this->preco]);
}
public function exportarCsv(): string
{
return implode(",", [$this->id, $this->nome, $this->preco]);
}
public function getId(): int { return $this->id; }
public function getCriadoEm(): DateTimeImmutable { return $this->criadoEm; }
public function getAtualizadoEm(): DateTimeImmutable { return $this->atualizadoEm; }
}
// Aceita QUALQUER classe que implemente Exportavel — polimorfismo via interface
function exportar(Exportavel $entidade, string $formato): string
{
return match($formato) {
"json" => $entidade->exportarJson(),
"csv" => $entidade->exportarCsv(),
default => throw new ValueError("Formato desconhecido: {$formato}"),
};
}
$p = new Produto(1, "Teclado", 350.0);
echo exportar($p, "json"); // {"id":1,"nome":"Teclado","preco":350}
echo exportar($p, "csv"); // 1,Teclado,350
// instanceof funciona com interfaces também
var_dump($p instanceof Exportavel); // true
var_dump($p instanceof Auditavel); // true — herdado por Persistivel
Traits — reutilização horizontal
Traits resolvem o problema de código que precisa ser compartilhado entre classes não relacionadas por herança. Artigo e Comentario são classes completamente diferentes — mas ambas precisam de timestamps e soft delete. Duplicar esse código seria errado; criar uma classe pai artificial seria forçado. Traits resolvem exatamente isso:
<?php
declare(strict_types=1);
trait Timestampable
{
private DateTimeImmutable $criadoEm;
private DateTimeImmutable $atualizadoEm;
public function inicializarTimestamps(): void
{
$this->criadoEm = new DateTimeImmutable();
$this->atualizadoEm = new DateTimeImmutable();
}
public function tocar(): void
{
$this->atualizadoEm = new DateTimeImmutable();
}
public function getCriadoEm(): DateTimeImmutable { return $this->criadoEm; }
public function getAtualizadoEm(): DateTimeImmutable { return $this->atualizadoEm; }
}
trait SoftDeletable
{
private ?DateTimeImmutable $deletadoEm = null;
public function deletar(): void { $this->deletadoEm = new DateTimeImmutable(); }
public function restaurar(): void { $this->deletadoEm = null; }
public function estaDeletado(): bool { return $this->deletadoEm !== null; }
}
class Artigo
{
use Timestampable, SoftDeletable; // inclui os dois traits
public function __construct(public readonly string $titulo)
{
$this->inicializarTimestamps();
}
}
// Sem nenhuma relação de herança com Artigo — mas mesmo comportamento
class Comentario
{
use Timestampable, SoftDeletable;
public function __construct(public readonly string $texto)
{
$this->inicializarTimestamps();
}
}
$artigo = new Artigo("PHP Moderno");
$artigo->deletar();
var_dump($artigo->estaDeletado()); // bool(true)
$artigo->restaurar();
var_dump($artigo->estaDeletado()); // bool(false)
Resolvendo conflitos entre traits
Se dois traits definem um método com o mesmo nome, o PHP lança um erro fatal ao incluir ambos. A solução usa duas palavras-chave: insteadof escolhe qual trait prevalece, e as renomeia o descartado para que ainda possa ser acessado:
<?php
declare(strict_types=1);
trait LogSimples
{
public function log(string $msg): void
{
echo "[SIMPLES] {$msg}\n";
}
}
trait LogDetalhado
{
public function log(string $msg): void
{
echo "[DETALHADO] " . date("H:i:s") . " {$msg}\n";
}
}
class Servico
{
use LogSimples, LogDetalhado {
// LogDetalhado::log prevalece quando chamamos $this->log()
LogDetalhado::log insteadof LogSimples;
// LogSimples::log ainda acessível com alias logSimples()
LogSimples::log as logSimples;
}
}
$s = new Servico();
$s->log("Operação concluída");
// [DETALHADO] 14:35:20 Operação concluída
$s->logSimples("Operação concluída");
// [SIMPLES] Operação concluída
// "as" também muda visibilidade de um método de trait
class ServicoRestrito
{
use LogSimples {
log as private logInterno; // privado nesta classe
}
public function executar(string $acao): void
{
$this->logInterno("Executando: {$acao}");
}
}
Polimorfismo — o valor real das hierarquias
Polimorfismo é a capacidade de tratar objetos de tipos diferentes de forma uniforme, desde que compartilhem um tipo comum. O código que usa os objetos não precisa saber com qual implementação específica está lidando — apenas com o contrato:
<?php
declare(strict_types=1);
interface Notificacao
{
public function enviar(string $dest, string $msg): bool;
public function getNome(): string;
}
class NotificacaoEmail implements Notificacao
{
public function enviar(string $dest, string $msg): bool
{
echo "📧 Email para {$dest}: {$msg}\n";
return true;
}
public function getNome(): string { return "Email"; }
}
class NotificacaoSMS implements Notificacao
{
public function enviar(string $dest, string $msg): bool
{
echo "📱 SMS para {$dest}: {$msg}\n";
return true;
}
public function getNome(): string { return "SMS"; }
}
class NotificacaoPush implements Notificacao
{
public function enviar(string $dest, string $msg): bool
{
echo "🔔 Push para {$dest}: {$msg}\n";
return true;
}
public function getNome(): string { return "Push"; }
}
// Só conhece Notificacao — não as implementações concretas
class ServicoNotificacao
{
/** @var Notificacao[] */
private array $canais = [];
// Retorna $this para permitir encadeamento fluente
public function adicionarCanal(Notificacao $canal): static
{
$this->canais[] = $canal;
return $this;
}
public function notificarTodos(string $dest, string $msg): void
{
foreach ($this->canais as $canal) {
if (!$canal->enviar($dest, $msg)) {
error_log("Falha ao enviar via {$canal->getNome()}");
}
}
}
}
// Fluent interface — encadeamento de chamadas
$servico = (new ServicoNotificacao())
->adicionarCanal(new NotificacaoEmail())
->adicionarCanal(new NotificacaoSMS())
->adicionarCanal(new NotificacaoPush());
$servico->notificarTodos("ana@email.com", "Pedido enviado!");
// 📧 Email para ana@email.com: Pedido enviado!
// 📱 SMS para ana@email.com: Pedido enviado!
// 🔔 Push para ana@email.com: Pedido enviado!
✅ Programe para interfaces, não para implementações
QuandoServicoNotificacaorecebeNotificacaoem vez deNotificacaoEmail, você pode adicionarNotificacaoWhatsAppamanhã sem tocar na classe orquestradora. Isso é o Princípio Open/Closed dos SOLID: aberto para extensão, fechado para modificação.
Classes e métodos final
A palavra-chave final impede que uma classe seja estendida, ou que um método seja sobrescrito. Use em classes que representam valores imutáveis, objetos de valor (Value Objects), ou implementações onde a extensão quebraria invariantes:
<?php
declare(strict_types=1);
// final class — nenhuma classe pode estendê-la
// Ideal para Value Objects: Uuid, Email, Dinheiro, CPF, etc.
final class Uuid
{
private function __construct(
private readonly string $valor,
) {}
public static function gerar(): static
{
$bytes = random_bytes(16);
$bytes[6] = chr(ord($bytes[6]) & 0x0f | 0x40);
$bytes[8] = chr(ord($bytes[8]) & 0x3f | 0x80);
return new static(vsprintf("%s%s-%s-%s-%s-%s%s%s", str_split(bin2hex($bytes), 4)));
}
public static function de(string $valor): static
{
if (!preg_match('/^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i', $valor)) {
throw new InvalidArgumentException("UUID inválido: {$valor}");
}
return new static(strtolower($valor));
}
public function __toString(): string { return $this->valor; }
public function equals(self $outro): bool { return $this->valor === $outro->valor; }
}
$id1 = Uuid::gerar();
$id2 = Uuid::gerar();
echo $id1; // ex: a1b2c3d4-e5f6-4abc-...
var_dump($id1->equals($id2)); // bool(false)
var_dump($id1->equals(Uuid::de((string)$id1))); // bool(true)
// class FakeUuid extends Uuid {} // Fatal: Cannot extend final class
// final em método individual — a classe pode ser estendida, o método não
class Base
{
final public function comportamentoFixo(): string
{
return "Este comportamento nunca muda em subclasses.";
}
public function comportamentoSobrescritivel(): string
{
return "Pode ser sobrescrito.";
}
}
class Filha extends Base
{
// public function comportamentoFixo() {} // Fatal: Cannot override final method
public function comportamentoSobrescritivel(): string { return "Sobrescrito."; }
}
Hierarquia de todo o artigo
«abstract» Forma — area() e perimetro() abstratos; descricao() concreto
├── Circulo — implementa area() e perimetro()
└── Retangulo — implementa area() e perimetro()
Funcionario — calcularSalario() concreto, sobrescritível
├── Gerente — override + bonus
└── Estagiario — override * 0.6
«interface» Exportavel — exportarJson(), exportarCsv()
«interface» Auditavel — getCriadoEm(), getAtualizadoEm()
«interface» Persistivel extends Auditavel — getId()
└── Produto implements Exportavel, Persistivel
«interface» Notificacao — enviar(), getNome()
├── NotificacaoEmail NotificacaoSMS NotificacaoPush
«trait» Timestampable — inicializarTimestamps(), tocar(), getters
«trait» SoftDeletable — deletar(), restaurar(), estaDeletado()
«trait» LogSimples / LogDetalhado — conflito resolvido com insteadof + as
├── Artigo use Timestampable, SoftDeletable
└── Comentario use Timestampable, SoftDeletable
final class Uuid — Value Object — não pode ser estendida
Guia de decisão
| Situação | extends | interface | trait |
|---|---|---|---|
| Classes compartilham código e estado (propriedades) | ✓ Ideal | ✗ | ~ Possível |
| Garantir que classes diferentes tenham os mesmos métodos | ✗ | ✓ Ideal | ✗ |
| Reutilizar código entre classes sem relação de herança | ✗ | ✗ | ✓ Ideal |
| Tipar parâmetros e retornos de funções | ✓ | ✓ Preferível | ✗ |
| Herança múltipla de comportamento | ✗ Impossível | ✓ N interfaces | ✓ N traits |
| Relação semântica é "é um" | ✓ Ideal | ✗ | ✗ |
| Relação semântica é "age como" | ✗ | ✓ Ideal | ✗ |
Boas práticas
Favoreça composição sobre herança. Herança cria acoplamento forte — uma mudança na classe pai pode quebrar todas as filhas. Antes de usar extends, pergunte: "a relação é realmente um 'é um'?" Se for "tem um" ou "usa um", passe o objeto como dependência no construtor.
Interfaces pequenas e focadas. Prefira várias interfaces pequenas — Exportavel, Auditavel — a uma grande com muitos métodos. Isso é o Princípio da Segregação de Interface (ISP dos SOLID). Classes que implementam interfaces menores se comprometem apenas com o que realmente fazem.
Traits para comportamento transversal, não para substituir design. Traits são poderosos para comportamentos que "cruzam" hierarquias — logging, timestamps, caching. Usá-los para mascarar ausência de design adequado ou duplicação de lógica de negócio é um sinal de alerta.
Use final por padrão em Value Objects. Classes como Uuid, Email, CPF, Dinheiro representam valores imutáveis cujas invariantes não devem ser relaxadas por subclasses. Marque-as final explicitamente.
Resumo do artigo
| Conceito | O que aprendemos |
|---|---|
extends |
Herda propriedades e métodos — relação "é um" |
parent:: |
Acessa construtor ou métodos da classe pai |
abstract class |
Não instanciável — contrato com implementação parcial |
método abstract |
Força subclasses a implementar — sem corpo na classe pai |
interface |
Contrato puro — apenas assinaturas, sem implementação |
implements |
Classe se compromete com todos os métodos da interface |
| Múltiplas interfaces | Uma classe pode implementar N interfaces |
trait |
Bloco de código reutilizável entre classes não relacionadas |
insteadof |
Resolve conflito de método entre dois traits |
as (trait) |
Renomeia ou muda visibilidade de método de trait |
final |
Impede extensão da classe ou sobrescrita do método |
instanceof |
Verifica herança e implementação de interface |
| Polimorfismo | Tratar objetos diferentes uniformemente via tipo comum |
🏠 Exercício da semana
- Crie uma hierarquia de veículos com classe abstrata
Veiculotendo método abstratocalcularConsumo(float $km): floate método concretodescricao(): string. ImplementeCarro,MotoeCaminhaocom consumos diferentes. Crie um array com os três e exiba a descrição de cada um via polimorfismo. - Defina a interface
Comparavelcom métodocomparar(mixed $outro): int(retorna -1, 0 ou 1). Implemente-a emTemperaturae emPreco. Escreva uma funçãoordenar(Comparavel ...$items): arrayque ordena qualquer coleção de objetosComparavel. - Crie um trait
Logavelcom métodolog(string $nivel, string $msg): voidque registra no formato[NIVEL] 2026-03-08 14:30 — mensagem. Use-o em duas classes sem relação de herança:ProcessadorPagamentoeImportadorCsv. - Implemente o padrão Strategy: interface
CalculadorFretecomcalcular(float $peso, float $distancia): float. ImplementeFreteCorreios,FreteExpresseFreteGratis. Crie umPedidoque aceita qualquerCalculadorFreteno construtor e exibe o valor do frete calculado. - Desafio: modele um sistema de pagamentos com interface
MetodoPagamentoe implementaçõesCartaoCredito,PixeBoleto. Use o traitLogaveldo exercício 3 em cada implementação para registrar tentativas. A classeCheckoutaceita qualquerMetodoPagamentono construtor e lançaPagamentoRecusadoExceptionquando o método retornafalse. Simule também um conflito entre dois traits e resolva-o cominsteadofeas.
Referências e leituras para aprofundar
- Herança — Manual oficial do PHP — Documentação completa de herança, sobrescrita de métodos,
parent::, classes abstratas e a palavra-chavefinal. - Interfaces — Manual oficial do PHP — Documentação de interfaces, constantes de interface, herança de interfaces e uso com
instanceof. - Traits — Manual oficial do PHP — Documentação completa de traits, incluindo resolução de conflitos com
insteadofeas, prioridade de métodos e traits com propriedades abstratas. - Padrão Strategy — Refactoring Guru — Uso clássico de interfaces para trocar algoritmos em tempo de execução. Corresponde ao exercício de frete deste artigo.
- Padrão Template Method — Refactoring Guru — Padrão baseado em classes abstratas — define o esqueleto de um algoritmo na classe pai e deixa as subclasses preencherem as etapas. Complemento natural deste artigo.
- GAMMA et al. — Design Patterns (GoF). Addison-Wesley, 1994 — Catálogo original dos 23 padrões de projeto OOP. Strategy, Template Method e Decorator são construídos diretamente sobre os mecanismos deste artigo.
- MARTIN, R.C. — Clean Code: princípios OCP, LSP e ISP. 2008 — Open/Closed, Liskov Substitution e Interface Segregation — a teoria por trás das escolhas de design deste artigo.