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
Resumo do artigo
| Conceito | O que aprendemos |
|---|---|
| Precedência | Classe própria > Trait > Classe pai |
insteadof |
Resolve conflitos escolhendo qual trait vence |
as |
Cria alias para método conflitante ou altera visibilidade |
abstract em trait |
Exige que a classe hospedeira implemente o método |
| Propriedades em traits | Possível desde PHP 8.2 com tipagem completa |
| Trait + Interface | Interface define contrato, trait fornece implementação — padrão recomendado |
| Traits dentro de traits | Composição de comportamentos menores em traits maiores |
Exercício da semana
-
Crie um trait
Validavelcom método abstratoregrasDeValidacao(): array(retorna array de regras) e método concretovalidar(array $dados): array(retorna erros). Implemente emFormularioCadastroeFormularioPagamento, cada um com suas próprias regras. -
Construa dois traits
LogConsoleeLogArquivo, ambos com métodolog(string $msg): void. Crie uma classeAplicacaoque usa ambos, resolve o conflito viainsteadofe mantém o segundo método acessível via alias. Demonstre chamando os dois. -
Combine trait com interface: interface
CacheavelInterfacecomchaveCache(): stringetempoExpiracao(): int. TraitCacheavelTraitimplementaarmazenarNoCache()ebuscarDoCache()usando os valores da interface. Implemente em duas classes diferentes. -
Crie um trait
HasUuidque gera um UUID v4 na inicialização e expõeuuid(): string. Use o trait emPedido,ProdutoeEventoDominio. Garanta que o UUID é imutável após a criação. -
Desafio: implemente o padrão completo do Laravel para
SoftDeletes: interfaceSoftDeletavelInterfacecomdeletar(),restaurar(),estaDeletado(),deletadoEm(); traitSoftDeletesTraitcom a implementação; e umQueryBuildersimplificado que, ao montar uma query para objetos que implementamSoftDeletavelInterface, adiciona automaticamenteWHERE deletado_em IS NULL.
Referências
- 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