No artigo Orientação a Objetos: Fundamentos 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.
A escolha entre os três costuma ficar mais clara com uma pergunta: o que você está dizendo sobre o objeto? extends afirma que ele é uma espécie de outra coisa e herda estado junto; implements afirma apenas que ele sabe fazer algo, sem dizer de onde veio; e use não afirma nada — apenas cola comportamento em quem precisa. Herança é a mais forte das três e por isso a mais cara de desfazer: quando a dúvida entre elas persistir, começar pela interface deixa as outras duas ainda abertas.
Fontes e leituras recomendadas
- 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.
Exercícios
Exercício 1
Crie uma hierarquia de veículos com classe abstrata Veiculo tendo método abstrato calcularConsumo(float $km): float e método concreto descricao(): string. Implemente Carro, Moto e Caminhao com consumos diferentes. Crie um array com os três e exiba a descrição de cada um via polimorfismo.
Ver resposta
✓ Resposta: O laço final é o ponto do exercício: ele chama descricao() sem saber a classe concreta. Adicionar um Onibus amanhã não muda uma linha dele — é isso que a classe abstrata compra.
<?php
declare(strict_types=1);
abstract class Veiculo
{
public function __construct(
protected readonly string $modelo,
protected readonly float $consumoMedio, // km por litro
) {}
// Sem corpo: cada veículo calcula do seu jeito. É o que obriga
// toda subclasse a decidir — e o que torna a classe abstrata.
abstract public function calcularConsumo(float $km): float;
abstract public function tipo(): string;
// Concreto: a lógica é a mesma para todos, herda pronta.
public function descricao(): string
{
return sprintf(
'%s %s — %.1f km/l, gasta %.2f L em 100 km',
$this->tipo(),
$this->modelo,
$this->consumoMedio,
$this->calcularConsumo(100),
);
}
}
final class Carro extends Veiculo
{
public function __construct(string $modelo, float $consumoMedio = 12.5) {
parent::__construct($modelo, $consumoMedio);
}
public function calcularConsumo(float $km): float
{
return $km / $this->consumoMedio;
}
public function tipo(): string { return 'Carro'; }
}
final class Moto extends Veiculo
{
public function __construct(string $modelo, float $consumoMedio = 38.0) {
parent::__construct($modelo, $consumoMedio);
}
public function calcularConsumo(float $km): float
{
return $km / $this->consumoMedio;
}
public function tipo(): string { return 'Moto'; }
}
final class Caminhao extends Veiculo
{
public function __construct(
string $modelo,
float $consumoMedio = 3.2,
private readonly float $toneladas = 0.0,
) {
parent::__construct($modelo, $consumoMedio);
}
// Carga pesa no consumo: 4% a mais por tonelada.
public function calcularConsumo(float $km): float
{
return ($km / $this->consumoMedio) * (1 + $this->toneladas * 0.04);
}
public function tipo(): string { return 'Caminhão'; }
}
// Polimorfismo: o array é de Veiculo, o corpo do laço não sabe qual é qual.
$frota = [
new Carro('Onix'),
new Moto('CB 500'),
new Caminhao('Constellation', toneladas: 8.0),
];
foreach ($frota as $veiculo) {
echo $veiculo->descricao(), PHP_EOL;
}
// Carro Onix — 12.5 km/l, gasta 8.00 L em 100 km
// Moto CB 500 — 38.0 km/l, gasta 2.63 L em 100 km
// Caminhão Constellation — 3.2 km/l, gasta 41.25 L em 100 km
Exercício 2
Defina a interface Comparavel com método comparar(mixed $outro): int (retorna -1, 0 ou 1). Implemente-a em Temperatura e em Preco. Escreva uma função ordenar(Comparavel ...$items): array que ordena qualquer coleção de objetos Comparavel.
Ver resposta
✓ Resposta: A função ordenar() não conhece nem Temperatura nem Preco — só o contrato. É a diferença entre programar para a interface e programar para a classe: qualquer tipo novo que implemente Comparavel já entra sem tocar nela.
<?php
declare(strict_types=1);
interface Comparavel
{
/** @return int -1 se menor, 0 se igual, 1 se maior */
public function comparar(mixed $outro): int;
}
final class Temperatura implements Comparavel
{
public function __construct(public readonly float $celsius) {}
public function comparar(mixed $outro): int
{
if (!$outro instanceof self) {
throw new InvalidArgumentException('Compare Temperatura com Temperatura.');
}
// O spaceship já devolve -1, 0 ou 1 — não precisa de if encadeado.
return $this->celsius <=> $outro->celsius;
}
public function __toString(): string
{
return number_format($this->celsius, 1, ',', '.') . ' °C';
}
}
final class Preco implements Comparavel
{
// Centavos, não float: 0.1 + 0.2 !== 0.3 em ponto flutuante, e dinheiro
// não perdoa esse tipo de erro.
public function __construct(public readonly int $centavos) {}
public static function deReais(float $reais): self
{
return new self((int) round($reais * 100));
}
public function comparar(mixed $outro): int
{
if (!$outro instanceof self) {
throw new InvalidArgumentException('Compare Preco com Preco.');
}
return $this->centavos <=> $outro->centavos;
}
public function __toString(): string
{
return 'R$ ' . number_format($this->centavos / 100, 2, ',', '.');
}
}
/**
* Ordena qualquer coleção de Comparavel.
*
* @return Comparavel[]
*/
function ordenar(Comparavel ...$items): array
{
// usort espera exatamente o contrato de comparar(): negativo, zero, positivo.
usort($items, static fn(Comparavel $a, Comparavel $b): int => $a->comparar($b));
return $items;
}
// Uso:
$temperaturas = ordenar(
new Temperatura(22.4),
new Temperatura(-3.0),
new Temperatura(35.8),
);
echo implode(' | ', array_map('strval', $temperaturas)), PHP_EOL;
// -3,0 °C | 22,4 °C | 35,8 °C
$precos = ordenar(
Preco::deReais(19.90),
Preco::deReais(4.50),
Preco::deReais(129.00),
);
echo implode(' | ', array_map('strval', $precos)), PHP_EOL;
// R$ 4,50 | R$ 19,90 | R$ 129,00
Exercício 3
Crie um trait Logavel com método log(string $nivel, string $msg): void que registra no formato [NIVEL] 2026-03-08 14:30 — mensagem. Use-o em duas classes sem relação de herança: ProcessadorPagamento e ImportadorCsv.
Ver resposta
✓ Resposta: É exatamente o caso em que herança não serve: ProcessadorPagamento e ImportadorCsv não têm ancestral comum nenhum, e inventar um só para compartilhar o log seria herança por conveniência. O trait entrega o comportamento sem mexer na hierarquia.
<?php
declare(strict_types=1);
trait Logavel
{
/** @var string[] */
private array $registros = [];
public function log(string $nivel, string $msg): void
{
$linha = sprintf(
'[%s] %s — %s',
strtoupper($nivel),
(new DateTimeImmutable())->format('Y-m-d H:i'),
$msg,
);
$this->registros[] = $linha;
// static::class resolve para a classe que USA o trait, não para o trait:
// o log sai identificado como ProcessadorPagamento ou ImportadorCsv.
error_log(static::class . ' ' . $linha);
}
/** @return string[] */
public function registros(): array
{
return $this->registros;
}
}
// Duas classes sem nenhuma relação de herança entre si.
final class ProcessadorPagamento
{
use Logavel;
public function processar(float $valor): bool
{
$this->log('info', "Iniciando cobrança de R$ {$valor}");
if ($valor <= 0) {
$this->log('error', 'Valor inválido — cobrança abortada');
return false;
}
$this->log('info', 'Pagamento aprovado');
return true;
}
}
final class ImportadorCsv
{
use Logavel;
public function importar(string $caminho): int
{
if (!is_readable($caminho)) {
$this->log('error', "Arquivo ilegível: {$caminho}");
return 0;
}
$linhas = 0;
$handle = fopen($caminho, 'rb');
while (fgetcsv($handle) !== false) {
$linhas++;
}
fclose($handle);
$this->log('info', "{$linhas} linhas importadas de {$caminho}");
return $linhas;
}
}
$pagamento = new ProcessadorPagamento();
$pagamento->processar(150.00);
$pagamento->processar(-5.00);
print_r($pagamento->registros());
// [INFO] 2026-03-08 14:30 — Iniciando cobrança de R$ 150
// [INFO] 2026-03-08 14:30 — Pagamento aprovado
// [INFO] 2026-03-08 14:30 — Iniciando cobrança de R$ -5
// [ERROR] 2026-03-08 14:30 — Valor inválido — cobrança abortada
Exercício 4
Implemente o padrão Strategy: interface CalculadorFrete com calcular(float $peso, float $distancia): float. Implemente FreteCorreios, FreteExpress e FreteGratis. Crie um Pedido que aceita qualquer CalculadorFrete no construtor e exibe o valor do frete calculado.
Ver resposta
✓ Resposta: Sem Strategy, isso seria um match gigante dentro de Pedido, e cada transportadora nova exigiria abrir a classe de novo. Com ele, transportadora nova é arquivo novo — Pedido fica fechado para modificação.
<?php
declare(strict_types=1);
interface CalculadorFrete
{
public function calcular(float $peso, float $distancia): float;
public function nome(): string;
}
final class FreteCorreios implements CalculadorFrete
{
public function calcular(float $peso, float $distancia): float
{
// Base + peso + faixa de distância.
return 12.50 + ($peso * 1.80) + ($distancia * 0.045);
}
public function nome(): string { return 'Correios (PAC)'; }
}
final class FreteExpress implements CalculadorFrete
{
public function calcular(float $peso, float $distancia): float
{
return (22.00 + ($peso * 3.20) + ($distancia * 0.11)) * 1.15;
}
public function nome(): string { return 'Express (24h)'; }
}
final class FreteGratis implements CalculadorFrete
{
// O caso "não cobra nada" é uma estratégia como as outras — não um `if`
// espalhado pelo código de quem calcula.
public function calcular(float $peso, float $distancia): float
{
return 0.0;
}
public function nome(): string { return 'Frete grátis'; }
}
final class Pedido
{
public function __construct(
private readonly string $descricao,
private readonly float $peso,
private readonly float $distancia,
private CalculadorFrete $frete,
) {}
// Trocar a estratégia em tempo de execução: pedido acima de R$ 300
// vira frete grátis sem o Pedido saber calcular nada.
public function usarFrete(CalculadorFrete $frete): void
{
$this->frete = $frete;
}
public function exibirFrete(): string
{
$valor = $this->frete->calcular($this->peso, $this->distancia);
return sprintf(
'%s via %s: R$ %s',
$this->descricao,
$this->frete->nome(),
number_format($valor, 2, ',', '.'),
);
}
}
$pedido = new Pedido('Kit teclado + mouse', peso: 1.8, distancia: 420, frete: new FreteCorreios());
echo $pedido->exibirFrete(), PHP_EOL;
// Kit teclado + mouse via Correios (PAC): R$ 34,64
$pedido->usarFrete(new FreteExpress());
echo $pedido->exibirFrete(), PHP_EOL;
// Kit teclado + mouse via Express (24h): R$ 84,20
$pedido->usarFrete(new FreteGratis());
echo $pedido->exibirFrete(), PHP_EOL;
// Kit teclado + mouse via Frete grátis: R$ 0,00
Exercício 5
Desafio: modele um sistema de pagamentos com interface MetodoPagamento e implementações CartaoCredito, Pix e Boleto. Use o trait Logavel do exercício 3 em cada implementação para registrar tentativas. A classe Checkout aceita qualquer MetodoPagamento no construtor e lança PagamentoRecusadoException quando o método retorna false. Simule também um conflito entre dois traits e resolva-o com insteadof e as.
Ver resposta
✓ Resposta: O par insteadof/as costuma ser mal entendido: insteadof escolhe qual implementação fica com o nome disputado, e as não renomeia — cria um apelido adicional. Sem o as, o método perdedor continuaria inacessível.
<?php
declare(strict_types=1);
final class PagamentoRecusadoException extends RuntimeException
{
public function __construct(
public readonly string $metodo,
public readonly float $valor,
string $motivo,
) {
parent::__construct("Pagamento por {$metodo} recusado: {$motivo}");
}
}
interface MetodoPagamento
{
public function pagar(float $valor): bool;
public function nome(): string;
}
// --- O conflito: dois traits com o MESMO método ---------------------------
trait Logavel
{
public function log(string $nivel, string $msg): void
{
printf("[%s] %s — %s%s", strtoupper($nivel),
(new DateTimeImmutable())->format('Y-m-d H:i'), $msg, PHP_EOL);
}
}
trait Auditavel
{
// Mesma assinatura de Logavel::log — usar os dois juntos é erro fatal
// ("Trait method log has not been applicable") até resolver o conflito.
public function log(string $nivel, string $msg): void
{
file_put_contents(
'auditoria.log',
sprintf("%s|%s|%s\n", $nivel, (new DateTimeImmutable())->format('c'), $msg),
FILE_APPEND,
);
}
}
final class CartaoCredito implements MetodoPagamento
{
use Logavel, Auditavel {
// Quem vence o nome `log`:
Logavel::log insteadof Auditavel;
// E o perdedor ganha um apelido, em vez de sumir:
Auditavel::log as auditar;
}
public function __construct(
private readonly string $numero,
private readonly float $limite,
) {}
public function pagar(float $valor): bool
{
$this->log('info', "Tentativa de R$ {$valor} no cartão final "
. substr($this->numero, -4));
$this->auditar('info', "cartao|{$valor}");
if ($valor > $this->limite) {
$this->log('error', 'Limite insuficiente');
return false;
}
return true;
}
public function nome(): string { return 'Cartão de crédito'; }
}
final class Pix implements MetodoPagamento
{
use Logavel;
public function __construct(private readonly string $chave) {}
public function pagar(float $valor): bool
{
$this->log('info', "Pix de R$ {$valor} para {$this->chave}");
return $this->chave !== '';
}
public function nome(): string { return 'Pix'; }
}
final class Boleto implements MetodoPagamento
{
use Logavel;
public function __construct(private readonly DateTimeImmutable $vencimento) {}
public function pagar(float $valor): bool
{
$this->log('info', "Boleto de R$ {$valor} gerado");
// Boleto vencido não se paga.
return $this->vencimento >= new DateTimeImmutable('today');
}
public function nome(): string { return 'Boleto'; }
}
final class Checkout
{
public function __construct(private readonly MetodoPagamento $metodo) {}
public function finalizar(float $valor): string
{
if (!$this->metodo->pagar($valor)) {
throw new PagamentoRecusadoException(
$this->metodo->nome(), $valor, 'recusado pelo provedor'
);
}
return sprintf('Pago R$ %s via %s',
number_format($valor, 2, ',', '.'), $this->metodo->nome());
}
}
// Uso:
foreach ([
new CartaoCredito('4111111111111234', limite: 500.00),
new Pix('webdesigner@exemplo.com'),
new Boleto(new DateTimeImmutable('yesterday')),
] as $metodo) {
try {
echo (new Checkout($metodo))->finalizar(320.00), PHP_EOL;
} catch (PagamentoRecusadoException $e) {
echo '✗ ', $e->getMessage(), PHP_EOL;
}
}