PHP

Traits Avançados Já leu

11 min de leitura

Traits Avançados
Traits são o mecanismo do PHP para reutilização horizontal de código — uma forma de compor comportamentos em classes sem usar

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

  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.

  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.

  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.

  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.

  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.


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

Mais em PHP

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

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

Configurando o Ambiente de Desenvolvimento
Configurando o Ambiente de Desenvolvimento

Antes de escrever qualquer programa, voc&ecirc; precisa de um ambiente onde o...

Funções
Funções

At&eacute; agora escrevemos c&oacute;digo sequencial &mdash; uma instru&ccedi...