Herança, Interfaces e Traits

Herança, Interfaces e Traits

Herdar, assinar um contrato ou compor: três mecanismos que resolvem problemas parecidos e não são intercambiáveis. Quando estender uma classe abstrata faz sentido, por que a interface descreve capacidade em vez de parentesco, e o que o trait oferece a classes que não compartilham ancestral.
PHP

24 min de leitura

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

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

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;
    }
}
Comentários

Mais em PHP

Interfaces Avançadas
Interfaces Avançadas

Interface é contrato, e contrato tem regras que vão além de listar métodos…

Arrays em Profundidade
Arrays em Profundidade

O array do PHP é uma estrutura só que faz o papel de lista, mapa e registro —…

Variáveis, Tipos de Dados e Operadores
Variáveis, Tipos de Dados e Operadores

Toda variável em PHP guarda um valor de algum tipo, e a linguagem converte…