Traits Avançados

Traits Avançados

Trait resolve o que herança não alcança, e traz regras próprias: quem vence quando dois traits definem o mesmo método, o que insteadof e as realmente fazem, como exigir que a classe forneça algo com método abstrato, e por que combinar trait e interface muda a natureza da garantia.
PHP

21 min de leitura

Traits são o mecanismo do PHP para reutilização horizontal de código — uma forma de compor comportamentos em classes sem usar herança. Você já viu traits básicos, mas o uso profissional envolve resolver conflitos entre traits, definir requisitos que a classe hospedeira deve satisfazer, compor traits com interfaces para criar contratos verificáveis, e entender as regras de precedência que determinam qual implementação vence quando dois traits definem o mesmo método.

Traits aparecem intensamente em frameworks: o SoftDeletes do Laravel é um trait, o HasTimestamps é um trait, o Authenticatable é um trait. O Symfony usa traits para adicionar comportamentos a entidades Doctrine. Entender as regras avançadas é o que permite criar e depurar esses comportamentos com confiança.

Revisão: o que um trait resolve

Herança resolve o problema de especialização — um Gato é um Animal. Traits resolvem o problema de comportamento compartilhado entre classes não relacionadas: um Pedido, um Produto e um Usuario podem todos ter timestamps, sem serem subclasses de nenhuma classe comum.

<?php
declare(strict_types=1);

// Sem trait — duplicação em toda classe que precisa de timestamps
class Pedido
{
    private \DateTimeImmutable $criadoEm;
    private \DateTimeImmutable $atualizadoEm;

    public function criadoEm(): \DateTimeImmutable    { return $this->criadoEm; }
    public function atualizadoEm(): \DateTimeImmutable { return $this->atualizadoEm; }
    // ... mesmo código se repete em Produto, Usuario, Categoria etc.
}

// Com trait — comportamento definido uma vez, usado em qualquer classe
trait HasTimestamps
{
    private \DateTimeImmutable $criadoEm;
    private \DateTimeImmutable $atualizadoEm;

    public function inicializarTimestamps(): void
    {
        $agora = new \DateTimeImmutable();
        $this->criadoEm     = $agora;
        $this->atualizadoEm = $agora;
    }

    public function tocarTimestamp(): void
    {
        $this->atualizadoEm = new \DateTimeImmutable();
    }

    public function criadoEm(): \DateTimeImmutable    { return $this->criadoEm; }
    public function atualizadoEm(): \DateTimeImmutable { return $this->atualizadoEm; }
}

// Classes completamente não relacionadas compartilham o mesmo comportamento
class Produto { use HasTimestamps; }
class Usuario { use HasTimestamps; }
class Categoria { use HasTimestamps; }

$produto = new Produto();
$produto->inicializarTimestamps();
echo $produto->criadoEm()->format('Y-m-d H:i:s') . "\n";

Regras de precedência

Quando um trait e a classe hospedeira definem o mesmo método, PHP segue uma ordem estrita de precedência: método da classe própria > método do trait > método herdado da classe pai. Isso permite que a classe hospedeira sempre sobrescreva o comportamento do trait, enquanto o trait sobrescreve o comportamento herdado.

<?php
declare(strict_types=1);

trait Saudacao
{
    public function cumprimentar(): string
    {
        return "Olá do trait!";
    }
}

class Base
{
    public function cumprimentar(): string
    {
        return "Olá da classe pai!";
    }
}

// Trait vence sobre a classe pai — mas a classe própria vence sobre o trait
class FilhaComTrait extends Base
{
    use Saudacao;
    // cumprimentar() vem do trait — trait tem precedência sobre Base
}

class FilhaComOverride extends Base
{
    use Saudacao;

    // Método próprio vence sobre o trait
    public function cumprimentar(): string
    {
        // Pode chamar o método do trait via alias se necessário
        return "Olá da classe filha! (trait diz: " . parent::cumprimentar() . ")";
    }
}

echo (new FilhaComTrait())->cumprimentar()    . "\n";
// Olá do trait!

echo (new FilhaComOverride())->cumprimentar() . "\n";
// Olá da classe filha! (trait diz: Olá da classe pai!)

Resolução de conflitos entre traits

Quando dois traits definem o mesmo método, o PHP lança um erro fatal — você é forçado a resolver o conflito explicitamente. Existem dois mecanismos: insteadof (escolhe qual trait vence) e as (cria um alias para o método que perdeu, tornando ambos acessíveis).

<?php
declare(strict_types=1);

trait LoggerA
{
    public function log(string $msg): void
    {
        echo "[LoggerA] {$msg}\n";
    }

    public function debug(string $msg): void
    {
        echo "[LoggerA:debug] {$msg}\n";
    }
}

trait LoggerB
{
    public function log(string $msg): void
    {
        echo "[LoggerB] {$msg}\n";
    }

    public function debug(string $msg): void
    {
        echo "[LoggerB:debug] {$msg}\n";
    }
}

class Servico
{
    use LoggerA, LoggerB {
        // insteadof — LoggerA::log vence sobre LoggerB::log
        LoggerA::log     insteadof LoggerB;
        // LoggerB::log fica acessível pelo alias logB()
        LoggerB::log     as logB;

        // Para debug: LoggerB vence, mas LoggerA fica via alias debugA()
        LoggerB::debug   insteadof LoggerA;
        LoggerA::debug   as debugA;
    }
}

$servico = new Servico();
$servico->log("mensagem principal");   // [LoggerA] mensagem principal
$servico->logB("via alias");           // [LoggerB] via alias
$servico->debug("depuração");          // [LoggerB:debug] depuração
$servico->debugA("depuração alt");     // [LoggerA:debug] depuração alt

O alias com as também pode alterar a visibilidade do método sem criar um novo nome:

<?php
declare(strict_types=1);

trait Segredo
{
    public function revelar(): string
    {
        return "segredo interno";
    }
}

class Cofre
{
    use Segredo {
        // Torna revelar() privado nesta classe — só Cofre acessa
        revelar as private;
    }

    public function abrir(string $senha): string
    {
        if ($senha !== '1234') {
            throw new \RuntimeException("Senha incorreta.");
        }
        // Chama o método do trait internamente
        return $this->revelar();
    }
}

$cofre = new Cofre();
echo $cofre->abrir('1234') . "\n"; // segredo interno
// $cofre->revelar() — erro: método privado

Requisitos de trait — abstract e propriedades

Um trait pode declarar métodos abstratos para exigir que a classe hospedeira os implemente. Isso cria um contrato implícito: o trait usa o comportamento que a classe deve fornecer. Traits também podem declarar propriedades — mas se a classe hospedeira declarar a mesma propriedade com tipo ou valor incompatível, o PHP lança um erro.

<?php
declare(strict_types=1);

// Trait com requisito abstrato — exige que a classe forneça getId()
trait Auditavel
{
    // Requisito: a classe hospedeira DEVE implementar este método
    abstract public function getId(): int;
    abstract public function getNome(): string;

    public function registrarAcao(string $acao): void
    {
        // Usa métodos que a classe hospedeira é obrigada a fornecer
        $linha = sprintf(
            "[AUDIT] %s | ID: %d | Nome: %s | Em: %s",
            $acao,
            $this->getId(),
            $this->getNome(),
            date('Y-m-d H:i:s')
        );
        echo $linha . "\n";
    }
}

// Trait com propriedade tipada — PHP 8.2+
trait ComSaldo
{
    // Propriedade com valor padrão
    private float $saldo = 0.0;

    public function saldo(): float { return $this->saldo; }

    public function depositar(float $valor): void
    {
        if ($valor <= 0) throw new \InvalidArgumentException("Valor deve ser positivo.");
        $this->saldo += $valor;
    }

    public function sacar(float $valor): void
    {
        if ($valor > $this->saldo) {
            throw new \RuntimeException("Saldo insuficiente: {$this->saldo}");
        }
        $this->saldo -= $valor;
    }
}

// A classe hospedeira satisfaz os requisitos abstratos do trait Auditavel
class ContaBancaria
{
    use Auditavel, ComSaldo;

    public function __construct(
        private readonly int    $id,
        private readonly string $titular,
    ) {}

    // Satisfaz o requisito abstrato de Auditavel
    public function getId(): int     { return $this->id; }
    public function getNome(): string { return $this->titular; }
}

$conta = new ContaBancaria(7, "Ana Lima");
$conta->depositar(1000.0);
$conta->registrarAcao("Depósito de R$ 1000,00");
// [AUDIT] Depósito de R$ 1000,00 | ID: 7 | Nome: Ana Lima | Em: 2024-01-15 14:32:00

$conta->sacar(250.0);
$conta->registrarAcao("Saque de R$ 250,00");
echo "Saldo: R$ " . $conta->saldo() . "\n"; // Saldo: R$ 750

Traits com interfaces — contratos verificáveis

O problema com traits puros é que não há como garantir, via type hint, que um objeto usa determinado trait. A solução profissional é combinar trait com interface: a interface define o contrato verificável pelo type system, e o trait fornece a implementação padrão.

<?php
declare(strict_types=1);

// Interface — contrato verificável pelo sistema de tipos
interface SerializavelInterface
{
    public function paraArray(): array;
    public function paraJson(): string;
}

// Trait — implementação padrão do contrato
trait SerializavelTrait
{
    public function paraArray(): array
    {
        // Usa reflection para serializar propriedades públicas e protected
        $dados = [];
        $reflection = new \ReflectionObject($this);

        foreach ($reflection->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED) as $prop) {
            $prop->setAccessible(true);
            $valor = $prop->getValue($this);
            $dados[$prop->getName()] = $valor instanceof \DateTimeInterface
                ? $valor->format('Y-m-d H:i:s')
                : $valor;
        }
        return $dados;
    }

    public function paraJson(): string
    {
        return json_encode($this->paraArray(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
    }
}

// Interface verificável + Trait fornece implementação — padrão recomendado
class Produto implements SerializavelInterface
{
    use SerializavelTrait;  // fornece paraArray() e paraJson()

    public function __construct(
        public readonly int    $id,
        public readonly string $nome,
        public readonly float  $preco,
    ) {}
}

class Usuario implements SerializavelInterface
{
    use SerializavelTrait;  // mesmo trait, outra classe

    public function __construct(
        public readonly int    $id,
        public readonly string $email,
    ) {}
}

// Função tipada na INTERFACE — aceita Produto, Usuario, ou qualquer outro
// que implemente SerializavelInterface, com ou sem o trait
function exportar(SerializavelInterface $objeto): string
{
    return $objeto->paraJson();
}

echo exportar(new Produto(1, 'Teclado', 350.0));
// { "id": 1, "nome": "Teclado", "preco": 350 }

echo exportar(new Usuario(42, 'ana@email.com'));
// { "id": 42, "email": "ana@email.com" }

Este é o padrão usado pelo Laravel em toda a sua base: Authenticatable é uma interface, AuthenticatableTrait é o trait que a implementa, e os modelos usam implements Authenticatable + use AuthenticatableTrait.

Traits dentro de traits

Traits podem usar outros traits, criando composições de comportamentos reutilizáveis. Isso permite construir traits complexos a partir de blocos menores sem repetição.

<?php
declare(strict_types=1);

trait HasCreatedAt
{
    private \DateTimeImmutable $criadoEm;

    public function inicializarCriadoEm(): void
    {
        $this->criadoEm = new \DateTimeImmutable();
    }
    public function criadoEm(): \DateTimeImmutable { return $this->criadoEm; }
}

trait HasUpdatedAt
{
    private \DateTimeImmutable $atualizadoEm;

    public function inicializarAtualizadoEm(): void
    {
        $this->atualizadoEm = new \DateTimeImmutable();
    }
    public function tocar(): void
    {
        $this->atualizadoEm = new \DateTimeImmutable();
    }
    public function atualizadoEm(): \DateTimeImmutable { return $this->atualizadoEm; }
}

trait HasSoftDelete
{
    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; }
    public function deletadoEm(): ?\DateTimeImmutable { return $this->deletadoEm; }
}

// Trait composto — usa os três traits menores
// Quem usa ModelTrait ganha tudo automaticamente
trait ModelTrait
{
    use HasCreatedAt, HasUpdatedAt, HasSoftDelete;

    public function inicializar(): void
    {
        $this->inicializarCriadoEm();
        $this->inicializarAtualizadoEm();
    }
}

class Artigo
{
    use ModelTrait;

    public function __construct(public readonly string $titulo) {}
}

$artigo = new Artigo("PHP Avançado");
$artigo->inicializar();

echo $artigo->criadoEm()->format('H:i:s') . "\n";
echo $artigo->estaDeletado() ? "deletado\n" : "ativo\n"; // ativo

$artigo->deletar();
echo $artigo->estaDeletado() ? "deletado\n" : "ativo\n"; // deletado

$artigo->restaurar();
echo $artigo->estaDeletado() ? "deletado\n" : "ativo\n"; // ativo

Trait é cópia de código feita pelo compilador, e essa frase explica quase todo o comportamento estranho dele: a precedência, os conflitos, o static::class que resolve para a classe que usa. Por ser cólagem, ele não cria tipo — e é por isso que trait sozinho não serve como contrato, enquanto trait mais interface serve muito bem. A dose também importa: uma classe montada a partir de seis traits é difícil de ler justamente porque o código dela não está nela.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Crie um trait Validavel com método abstrato regrasDeValidacao(): array (retorna array de regras) e método concreto validar(array $dados): array (retorna erros). Implemente em FormularioCadastro e FormularioPagamento, cada um com suas próprias regras.

Ver resposta

✓ Resposta: O método abstrato no trait é o que o torna um template: validar() conhece o algoritmo, e a classe fornece só a parte que varia. Se esquecer de implementar regrasDeValidacao(), o erro é fatal já no carregamento da classe — não em produção.

<?php

declare(strict_types=1);

trait Validavel
{
    // Método abstrato dentro do trait: quem usar é OBRIGADO a implementar.
    // É assim que o trait exige o pedaço variável sem conhecê-lo.
    abstract public function regrasDeValidacao(): array;

    /** @return array<string, string[]> campo => lista de erros */
    public function validar(array $dados): array
    {
        $erros = [];

        foreach ($this->regrasDeValidacao() as $campo => $regras) {
            $valor = $dados[$campo] ?? null;

            foreach (explode('|', $regras) as $regra) {
                [$nome, $param] = array_pad(explode(':', $regra, 2), 2, null);

                $erro = $this->aplicarRegra($nome, $campo, $valor, $param);

                if ($erro !== null) {
                    $erros[$campo][] = $erro;
                    // Uma falha por campo basta: sem isso, campo vazio
                    // acusaria "obrigatório" e "mínimo 8" ao mesmo tempo.
                    break;
                }
            }
        }

        return $erros;
    }

    private function aplicarRegra(string $regra, string $campo, mixed $valor, ?string $param): ?string
    {
        return match ($regra) {
            'obrigatorio' => (is_string($valor) ? trim($valor) === '' : $valor === null)
                ? "O campo {$campo} é obrigatório." : null,

            'email' => ($valor !== null && !filter_var($valor, FILTER_VALIDATE_EMAIL))
                ? "O campo {$campo} precisa ser um e-mail válido." : null,

            'min' => (is_string($valor) && mb_strlen($valor) < (int) $param)
                ? "O campo {$campo} precisa de ao menos {$param} caracteres." : null,

            'numerico' => ($valor !== null && !is_numeric($valor))
                ? "O campo {$campo} precisa ser numérico." : null,

            'positivo' => (is_numeric($valor) && (float) $valor <= 0)
                ? "O campo {$campo} precisa ser maior que zero." : null,

            default => null,
        };
    }
}

final class FormularioCadastro
{
    use Validavel;

    public function regrasDeValidacao(): array
    {
        return [
            'nome'  => 'obrigatorio|min:3',
            'email' => 'obrigatorio|email',
            'senha' => 'obrigatorio|min:8',
        ];
    }
}

final class FormularioPagamento
{
    use Validavel;

    public function regrasDeValidacao(): array
    {
        return [
            'cartao'    => 'obrigatorio|min:13',
            'cvv'       => 'obrigatorio|numerico|min:3',
            'valor'     => 'obrigatorio|numerico|positivo',
        ];
    }
}

print_r((new FormularioCadastro())->validar([
    'nome' => 'Jo', 'email' => 'nao-eh-email', 'senha' => '123',
]));
// nome  => O campo nome precisa de ao menos 3 caracteres.
// email => O campo email precisa ser um e-mail válido.
// senha => O campo senha precisa de ao menos 8 caracteres.

print_r((new FormularioPagamento())->validar([
    'cartao' => '4111111111111111', 'cvv' => '12x', 'valor' => -5,
]));
// cvv   => O campo cvv precisa ser numérico.
// valor => O campo valor precisa ser maior que zero.

Exercício 2

Construa dois traits LogConsole e LogArquivo, ambos com método log(string $msg): void. Crie uma classe Aplicacao que usa ambos, resolve o conflito via insteadof e mantém o segundo método acessível via alias. Demonstre chamando os dois.

Ver resposta

✓ Resposta: O erro mais comum aqui é achar que as renomeia. Ele adiciona um nome: depois de LogArquivo::log as logArquivo, o método continua existindo como log também — só que insteadof já deu esse nome ao outro trait.

<?php

declare(strict_types=1);

trait LogConsole
{
    public function log(string $msg): void
    {
        printf("[console] %s — %s%s",
               (new DateTimeImmutable())->format('H:i:s'), $msg, PHP_EOL);
    }
}

trait LogArquivo
{
    public function log(string $msg): void
    {
        file_put_contents(
            __DIR__ . '/app.log',
            sprintf("[arquivo] %s — %s\n", (new DateTimeImmutable())->format('c'), $msg),
            FILE_APPEND | LOCK_EX,
        );
    }
}

final class Aplicacao
{
    // Usar os dois sem o bloco de resolução é erro FATAL:
    // "Trait method Aplicacao::log has not been applied as it collides..."
    use LogConsole, LogArquivo {
        // 1) Quem fica com o nome disputado:
        LogConsole::log insteadof LogArquivo;

        // 2) O perdedor ganha um nome adicional — `as` NÃO renomeia,
        //    cria um apelido. Sem isso, LogArquivo::log ficaria inacessível.
        LogArquivo::log as logArquivo;
    }

    public function iniciar(): void
    {
        $this->log('Aplicação iniciada');        // LogConsole
        $this->logArquivo('Aplicação iniciada'); // LogArquivo
    }

    public function falhar(string $motivo): void
    {
        // Erro grave vai para os dois destinos.
        $this->log("ERRO: {$motivo}");
        $this->logArquivo("ERRO: {$motivo}");
    }
}

$app = new Aplicacao();
$app->iniciar();
$app->falhar('conexão recusada');

// Saída no console:
// [console] 14:30:12 — Aplicação iniciada
// [console] 14:30:12 — ERRO: conexão recusada
//
// app.log:
// [arquivo] 2026-08-15T14:30:12-03:00 — Aplicação iniciada
// [arquivo] 2026-08-15T14:30:12-03:00 — ERRO: conexão recusada

// ---------- Bônus: `as` também muda visibilidade -------------------------
final class Servico
{
    use LogConsole {
        // O log vira detalhe interno: ninguém de fora chama $servico->log().
        log as protected registrar;
    }

    public function executar(): void
    {
        $this->registrar('executando');
    }
}

Exercício 3

Combine trait com interface: interface CacheavelInterface com chaveCache(): string e tempoExpiracao(): int. Trait CacheavelTrait implementa armazenarNoCache() e buscarDoCache() usando os valores da interface. Implemente em duas classes diferentes.

Ver resposta

✓ Resposta: A dupla interface + trait resolve o que nenhum dos dois faz sozinho: a interface permite tipar (function aquecer(CacheavelInterface $c)) e o trait evita reescrever o mesmo cache em cada classe. Trait sozinho não serve para type hint.

<?php

declare(strict_types=1);

interface CacheavelInterface
{
    public function chaveCache(): string;

    public function tempoExpiracao(): int;   // segundos
}

trait CacheavelTrait
{
    // O trait não implementa a interface — ele CONSOME os dois métodos que
    // a interface obriga a existir. A classe declara `implements`, o trait
    // entrega o comportamento. Separação limpa entre contrato e implementação.
    public function armazenarNoCache(mixed $valor): bool
    {
        $arquivo = $this->caminhoCache();

        $envelope = [
            'expira_em' => time() + $this->tempoExpiracao(),
            'valor'     => $valor,
        ];

        return file_put_contents(
            $arquivo,
            serialize($envelope),
            LOCK_EX,
        ) !== false;
    }

    public function buscarDoCache(): mixed
    {
        $arquivo = $this->caminhoCache();

        if (!is_file($arquivo)) {
            return null;
        }

        $envelope = unserialize((string) file_get_contents($arquivo), ['allowed_classes' => false]);

        if (!is_array($envelope) || time() > ($envelope['expira_em'] ?? 0)) {
            // Expirou: limpa e devolve null como se nunca tivesse existido.
            @unlink($arquivo);
            return null;
        }

        return $envelope['valor'];
    }

    public function invalidarCache(): void
    {
        $arquivo = $this->caminhoCache();

        if (is_file($arquivo)) {
            unlink($arquivo);
        }
    }

    private function caminhoCache(): string
    {
        // sha1 na chave: nome de arquivo seguro mesmo com "/" ou ":" na chave.
        return sys_get_temp_dir() . '/cache_' . sha1($this->chaveCache()) . '.php';
    }
}

final class RelatorioVendas implements CacheavelInterface
{
    use CacheavelTrait;

    public function __construct(
        private readonly int $ano,
        private readonly int $mes,
    ) {}

    public function chaveCache(): string
    {
        return sprintf('relatorio:vendas:%04d-%02d', $this->ano, $this->mes);
    }

    // Relatório de mês fechado não muda mais: cacheia por um dia.
    public function tempoExpiracao(): int
    {
        $ehMesAtual = $this->ano === (int) date('Y') && $this->mes === (int) date('n');

        return $ehMesAtual ? 300 : 86400;
    }

    public function gerar(): array
    {
        if (($cache = $this->buscarDoCache()) !== null) {
            return $cache;
        }

        $dados = ['total' => 128450.90, 'pedidos' => 312];   // consulta cara

        $this->armazenarNoCache($dados);

        return $dados;
    }
}

final class PerfilUsuario implements CacheavelInterface
{
    use CacheavelTrait;

    public function __construct(private readonly int $usuarioId) {}

    public function chaveCache(): string
    {
        return "usuario:{$this->usuarioId}:perfil";
    }

    public function tempoExpiracao(): int { return 600; }
}

$relatorio = new RelatorioVendas(2026, 7);
print_r($relatorio->gerar());   // 1ª vez: calcula e grava
print_r($relatorio->gerar());   // 2ª vez: vem do cache

Exercício 4

Crie um trait HasUuid que gera um UUID v4 na inicialização e expõe uuid(): string. Use o trait em Pedido, Produto e EventoDominio. Garanta que o UUID é imutável após a criação.

Ver resposta

✓ Resposta: A escolha entre as duas variantes é real: readonly dá garantia forte, mas obriga toda classe a chamar o inicializador no construtor e não aceita atribuição preguiçosa. A versão com ??= é mais tolerante e igualmente imutável na prática, já que não existe setter.

<?php

declare(strict_types=1);

trait HasUuid
{
    // Sem `readonly` aqui, porque o valor é atribuído fora do construtor da
    // classe; a imutabilidade vem de a propriedade ser private e não haver
    // setter. Quem quiser readonly de verdade, veja a variante no fim.
    private ?string $uuid = null;

    public function uuid(): string
    {
        // Geração preguiçosa: o primeiro acesso cria, os seguintes reusam.
        // Assim o trait funciona mesmo em classe cujo construtor não o chama.
        return $this->uuid ??= self::gerarUuidV4();
    }

    public static function gerarUuidV4(): string
    {
        $bytes = random_bytes(16);

        // Versão 4 nos bits 12-15 do time_hi_and_version.
        $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
        // Variante RFC 4122 nos dois bits mais altos do clock_seq_hi.
        $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);

        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));
    }

    public function mesmaIdentidade(object $outro): bool
    {
        return method_exists($outro, 'uuid') && $this->uuid() === $outro->uuid();
    }
}

final class Pedido
{
    use HasUuid;

    public function __construct(
        public readonly string $cliente,
        public readonly float $total,
    ) {
        $this->uuid();   // fixa a identidade já na criação
    }
}

final class Produto
{
    use HasUuid;

    public function __construct(public readonly string $nome) {}
}

final class EventoDominio
{
    use HasUuid;

    public function __construct(
        public readonly string $nome,
        public readonly DateTimeImmutable $ocorridoEm = new DateTimeImmutable(),
    ) {}

    public function paraFila(): array
    {
        // O UUID é a chave de idempotência: reprocessar o mesmo evento duas
        // vezes é detectável porque o id não muda.
        return ['id' => $this->uuid(), 'nome' => $this->nome,
                'em' => $this->ocorridoEm->format('c')];
    }
}

$pedido = new Pedido('Ana', 349.90);

echo $pedido->uuid(), PHP_EOL;   // 3f2b1c8a-... (v4)
echo $pedido->uuid(), PHP_EOL;   // idêntico: não regenera

var_dump($pedido->uuid() === $pedido->uuid());          // true
var_dump($pedido->mesmaIdentidade(new Pedido('Ana', 349.90)));  // false

// ---------- Variante com readonly de verdade (PHP 8.1+) ------------------
trait HasUuidReadonly
{
    public readonly string $uuid;

    // A classe chama isto no construtor. readonly só aceita UMA escrita, e
    // ela tem de partir do escopo da classe — por isso não dá para usar a
    // inicialização preguiçosa aqui.
    private function inicializarUuid(): void
    {
        $this->uuid = HasUuid::gerarUuidV4();
    }
}

Exercício 5

Desafio: implemente o padrão completo do Laravel para SoftDeletes: interface SoftDeletavelInterface com deletar(), restaurar(), estaDeletado(), deletadoEm(); trait SoftDeletesTrait com a implementação; e um QueryBuilder simplificado que, ao montar uma query para objetos que implementam SoftDeletavelInterface, adiciona automaticamente WHERE deletado_em IS NULL.

Ver resposta

✓ Resposta: O is_subclass_of() com a interface é o que torna o comportamento automático — e é por isso que o Eloquent detecta o trait. Lembre do outro lado: índice em deletado_em e atenção a UNIQUE, porque a linha continua lá e um e-mail "apagado" ainda bloqueia o recadastro.

<?php

declare(strict_types=1);

interface SoftDeletavelInterface
{
    public function deletar(): void;

    public function restaurar(): void;

    public function estaDeletado(): bool;

    public function deletadoEm(): ?DateTimeImmutable;
}

trait SoftDeletesTrait
{
    private ?DateTimeImmutable $deletadoEm = null;

    public function deletar(): void
    {
        // Idempotente: deletar duas vezes não muda a data original.
        $this->deletadoEm ??= new DateTimeImmutable();
    }

    public function restaurar(): void
    {
        $this->deletadoEm = null;
    }

    public function estaDeletado(): bool
    {
        return $this->deletadoEm !== null;
    }

    public function deletadoEm(): ?DateTimeImmutable
    {
        return $this->deletadoEm;
    }

    /** Marca a coluna no banco, sem apagar a linha. */
    public function aplicarDelecao(PDO $pdo, string $tabela, int $id): bool
    {
        $stmt = $pdo->prepare(
            "UPDATE {$tabela} SET deletado_em = :quando WHERE id = :id AND deletado_em IS NULL"
        );

        $this->deletar();
        $stmt->execute([
            ':quando' => $this->deletadoEm->format('Y-m-d H:i:s'),
            ':id'     => $id,
        ]);

        return $stmt->rowCount() > 0;
    }
}

final class Post implements SoftDeletavelInterface
{
    use SoftDeletesTrait;

    public function __construct(
        public readonly int $id,
        public readonly string $titulo,
    ) {}
}

final class Comentario implements SoftDeletavelInterface
{
    use SoftDeletesTrait;

    public function __construct(public readonly int $id, public readonly string $texto) {}
}

// ---------- QueryBuilder que respeita a exclusão lógica -------------------
final class QueryBuilder
{
    private array $condicoes = [];
    private bool $incluirDeletados = false;

    public function __construct(
        private readonly string $tabela,
        private readonly string $classeEntidade,
    ) {}

    public function where(string $condicao): static
    {
        $this->condicoes[] = $condicao;
        return $this;
    }

    /** Equivale ao withTrashed() do Eloquent. */
    public function comDeletados(): static
    {
        $this->incluirDeletados = true;
        return $this;
    }

    /** Só a lixeira — o onlyTrashed(). */
    public function apenasDeletados(): static
    {
        $this->incluirDeletados = true;
        $this->condicoes[] = 'deletado_em IS NOT NULL';
        return $this;
    }

    public function build(): string
    {
        $condicoes = $this->condicoes;

        // O coração do exercício: se a entidade implementa a interface, o
        // filtro entra SOZINHO. Esquecer de escrever o WHERE deixa de ser
        // possível — e é justamente esse esquecimento que faz registro
        // "apagado" reaparecer numa listagem.
        if (!$this->incluirDeletados
            && is_subclass_of($this->classeEntidade, SoftDeletavelInterface::class)) {
            array_unshift($condicoes, 'deletado_em IS NULL');
        }

        $sql = "SELECT * FROM {$this->tabela}";

        if ($condicoes !== []) {
            $sql .= ' WHERE ' . implode(' AND ', $condicoes);
        }

        return $sql;
    }
}

echo (new QueryBuilder('posts', Post::class))
        ->where('autor_id = 7')
        ->build(), PHP_EOL;
// SELECT * FROM posts WHERE deletado_em IS NULL AND autor_id = 7

echo (new QueryBuilder('posts', Post::class))
        ->where('autor_id = 7')
        ->comDeletados()
        ->build(), PHP_EOL;
// SELECT * FROM posts WHERE autor_id = 7

echo (new QueryBuilder('posts', Post::class))->apenasDeletados()->build(), PHP_EOL;
// SELECT * FROM posts WHERE deletado_em IS NOT NULL

// Entidade sem a interface: nada é acrescentado.
final class LogAcesso {}
echo (new QueryBuilder('logs', LogAcesso::class))->build(), PHP_EOL;
// SELECT * FROM logs
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…

A História do PHP: de script pessoal a pilar da web
A História do PHP: de script pessoal a pilar da web

De um script para contar visitas a um currículo até a linguagem que move boa…

Estruturas de Controle
Estruturas de Controle

Decidir é o que separa um script de um programa. Como o PHP encadeia condições…