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
- PHP Manual — Traits: https://www.php.net/manual/pt_BR/language.oop5.traits.php
- PHP Manual — Traits com propriedades (PHP 8.2): https://www.php.net/manual/pt_BR/migration82.new-features.php
- Laravel Source — SoftDeletes Trait: https://github.com/laravel/framework/blob/master/src/Illuminate/Database/Eloquent/SoftDeletes.php
- Laravel Source — Authenticatable: https://github.com/laravel/framework/blob/master/src/Illuminate/Auth/Authenticatable.php
- PHP: The Right Way — Traits: https://phptherightway.com/#traits
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