PHP

Herança, Interfaces e Traits Já leu

16 min de leitura

Herança, Interfaces e Traits
No artigo anterior aprendemos a criar classes com propriedades, métodos e encapsulamento. Agora damos o próximo passo: como relacionar classes ent

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 — como descricao() 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
Quando ServicoNotificacao recebe Notificacao em vez de NotificacaoEmail, você pode adicionar NotificacaoWhatsApp amanhã 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

  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.
  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.
  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.
  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.
  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.

Referências e leituras para aprofundar

Comentários

Mais em PHP

Estruturas de Repetição
Estruturas de Repetição

Se as estruturas de controle ensinam o programa a tomar decis&otilde;es, as e...

Orientação a Objetos: Fundamentos
Orientação a Objetos: Fundamentos

Orienta&ccedil;&atilde;o a Objetos &eacute; o paradigma que organiza o c&oacu...

Interfaces Avançadas
Interfaces Avançadas

Interfaces s&atilde;o o principal mecanismo de abstra&ccedil;&atilde;o do PHP...