PHP Traits

Na lojinha da Ana, o pedido e o produto precisam guardar um histórico ("criado", "pago", "preço alterado"). São coisas bem diferentes: um pedido não "é um tipo de" produto, nem o contrário, então herança não serve. Copiar e colar os mesmos métodos nas duas classes funciona, mas qualquer correção precisa ser feita duas vezes. O trait (em inglês, "traço", "característica") é um pacote de métodos e propriedades que você encaixa em quantas classes quiser, com uma linha: use.

Você vai aprender
  • O que é um trait e como criar um com trait.
  • Como encaixar um trait numa classe com use.
  • Traits com propriedades, métodos abstratos e estáticos.
  • Vários traits na mesma classe e como resolver conflitos com insteadof e as.
  • Quem ganha quando a classe, o trait e a classe mãe têm o mesmo método.
  • Por que um trait não é um tipo, e como juntá-lo a uma interface.
  • Quando usar trait, herança ou outra solução.

Pré-requisitos: classes e objetos, herança, classes abstratas e interfaces. Os exemplos rodam na máquina fictícia da Ana, na pasta ~/poo.


A ideia antes do código: o adesivo e a forma

Pense numa cartela de adesivos. O adesivo "à prova d'água" pode ser colado num celular, num relógio, numa mochila. Os três não têm nada em comum (não são "parentes"), mas todos ganham a mesma característica. O trait é esse adesivo: um conjunto de habilidades que você cola em classes diferentes.

Compare com a herança: a herança é como a família. Uma classe só tem uma mãe no PHP (extends aceita uma classe só), e a filha "é um tipo" da mãe. O trait não cria parentesco nenhum: ele só copia código para dentro da classe.


1. O primeiro trait

<?php
trait TemHistorico
{
    private array $historico = [];

    public function registrar(string $mensagem): void
    {
        $this->historico[] = $mensagem;
    }

    public function historico(): array
    {
        return $this->historico;
    }
}

class Pedido
{
    use TemHistorico;

    public function __construct(public int $numero) {}
}

class Produto
{
    use TemHistorico;

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

$pedido = new Pedido(1001);
$pedido->registrar("Pedido criado");
$pedido->registrar("Pagamento recebido");

$caneca = new Produto("Caneca");
$caneca->registrar("Preço alterado para R$ 25,90");

print_r($pedido->historico());
print_r($caneca->historico());
trait TemHistorico { … }
Cria o trait. Por dentro, parece uma classe: propriedades e métodos. Por costume, o nome descreve uma habilidade (TemHistorico, PodeSerExportado).
private array $historico = [];
Uma propriedade. Cada objeto que usa o trait ganha a sua própria lista.
$this->historico[] = $mensagem;
Dentro do trait, $this é o objeto da classe que usou o trait (o pedido, o produto).
use TemHistorico;
Dentro da classe, use "cola" o trait: é como se você tivesse escrito a propriedade e os dois métodos ali dentro.
$pedido->registrar(…)
Quem usa a classe nem sabe que o método veio de um trait.
$ php historico.php
Array
(
    [0] => Pedido criado
    [1] => Pagamento recebido
)
Array
(
    [0] => Preço alterado para R$ 25,90       « a lista do produto é separada
)
Dois use diferentes

A palavra use aparece em dois lugares no PHP, com sentidos diferentes. Dentro de uma classe, use TemHistorico; cola um trait. No começo do arquivo, fora de classes, use Loja\Produto; importa um nome de outro namespace (veja Namespaces). E nas funções anônimas, function () use ($x) leva uma variável para dentro. Três usos, mesma palavra.

Um trait não vive sozinho

O trait é um pedaço de código para ser colado; ele não é uma classe. Não dá para criar um objeto dele:

<?php
trait TemHistorico
{
    public function registrar(string $m): void {}
}
$t = new TemHistorico();
$ php new-trait.php

Fatal error: Uncaught Error: Cannot instantiate trait TemHistorico in /home/ana/poo/new-trait.php:6
Stack trace:
#0 {main}
  thrown in /home/ana/poo/new-trait.php on line 6

"Não é possível instanciar o trait". Sempre use o trait dentro de uma classe.

Missão: o trait TemDesconto já está pronto, mas ninguém usa. Rode e veja o erro Call to undefined method. Depois cole o trait nas duas classes (Produto e Servico) com use TemDesconto;. A caneca de R$ 25 com 10% deve sair por 22.5 e o frete de R$ 20 por 18.

<?php
trait TemDesconto
{
    public function comDesconto(float $porcentagem): float
    {
        return $this->preco - $this->preco * $porcentagem / 100;
    }
}

class Produto
{
    public function __construct(public string $nome, public float $preco) {}
}

class Servico
{
    public function __construct(public string $nome, public float $preco) {}
}

$caneca = new Produto("Caneca", 25);
$frete = new Servico("Frete", 20);
echo "$caneca->nome: ", $caneca->comDesconto(10), "\n";
echo "$frete->nome: ", $frete->comDesconto(10), "\n";
<?php
trait TemDesconto
{
    public function comDesconto(float $porcentagem): float
    {
        return $this->preco - $this->preco * $porcentagem / 100;
    }
}

class Produto
{
    use TemDesconto;

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

class Servico
{
    use TemDesconto;

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

$caneca = new Produto("Caneca", 25);
$frete = new Servico("Frete", 20);
echo "$caneca->nome: ", $caneca->comDesconto(10), "\n";
echo "$frete->nome: ", $frete->comDesconto(10), "\n";

Repare que o trait usa $this->preco, mas não declara essa propriedade: ele conta que a classe terá uma. Funciona, mas é um combinado invisível. Logo abaixo você vai ver um jeito de deixar esse combinado explícito.


2. O que cabe num trait

Métodos abstratos: o trait exige algo da classe

Um trait pode declarar um método abstract (veja Classes Abstratas): ele diz "quem me usar precisa ter este método". Assim o combinado deixa de ser invisível:

<?php
trait TemNome
{
    abstract public function nome(): string;

    public function apresentar(): string
    {
        return "Eu sou " . $this->nome();
    }
}
class Produto
{
    use TemNome;
}
$ php abstrato.php

Fatal error: Class Produto contains 1 abstract method and must therefore be declared abstract or implement the remaining method (Produto::nome) in /home/ana/poo/abstrato.php on line 11
Stack trace:
#0 {main}

"A classe Produto contém 1 método abstrato e precisa ser declarada abstrata ou implementar o método que falta (Produto::nome)". O PHP conferiu o combinado. Basta a classe ter public function nome(): string { return $this->nome; }.

Métodos e propriedades estáticos, constantes

Um trait pode ter membros static e, desde o PHP 8.2, constantes. Detalhe importante: cada classe que usa o trait ganha a sua cópia da propriedade estática.

<?php
trait Contavel
{
    private static int $criados = 0;
    public const VERSAO = 2;

    public static function criados(): int
    {
        return self::$criados;
    }

    public function contar(): void
    {
        self::$criados++;
    }
}
class Pedido { use Contavel; }
class Produto { use Contavel; }
$a = new Pedido(); $a->contar();
$b = new Pedido(); $b->contar();
$c = new Produto(); $c->contar();
echo Pedido::criados(), " ", Produto::criados(), " ", Pedido::VERSAO, "\n";
$ php contavel.php
2 1 2          « Pedido contou 2, Produto contou 1: contadores separados

Isso confirma a ideia do "copiar e colar": é como se cada classe tivesse escrito a sua própria $criados.

self e static dentro do trait

Dentro de um trait, self e __CLASS__ são a classe que usou o trait, não o trait. Para saber o nome do trait, use __TRAIT__:

<?php
trait Saudacao
{
    public function ola(): string
    {
        return "Olá do trait, eu sou " . self::class . " / " . static::class . " / " . __CLASS__ . " / " . __TRAIT__;
    }
}
class Loja { use Saudacao; }
class Filial extends Loja {}
echo (new Loja())->ola(), "\n";
echo (new Filial())->ola(), "\n";
$ php quem.php
Olá do trait, eu sou Loja / Loja / Loja / Saudacao
Olá do trait, eu sou Loja / Filial / Loja / Saudacao     « static:: é a classe do objeto

3. Vários traits e conflitos

Uma classe pode usar quantos traits quiser, separados por vírgula: use TemHistorico, TemDesconto;. O problema aparece quando dois traits têm um método com o mesmo nome. O relatório da Ana sabe exportar em CSV e em JSON, e os dois traits chamam o método de exportar():

<?php
trait ExportaCsv
{
    public function exportar(): string
    {
        return "nome;preco";
    }
}

trait ExportaJson
{
    public function exportar(): string
    {
        return '{"nome":"","preco":0}';
    }
}

class Relatorio
{
    use ExportaCsv, ExportaJson;
}

echo (new Relatorio())->exportar(), "\n";
$ php conflito.php

Fatal error: Trait method ExportaJson::exportar has not been applied as Relatorio::exportar, because of collision with ExportaCsv::exportar in /home/ana/poo/conflito.php on line 18
Stack trace:
#0 {main}

"O método ExportaJson::exportar não foi aplicado como Relatorio::exportar por causa de uma colisão com ExportaCsv::exportar". O PHP não escolhe sozinho: ele para e pede que você decida. Repare que o erro acontece antes de qualquer linha rodar (aponta a linha 18, a da classe).

Resolver com insteadof e as

A decisão vai num bloco { } depois do use:

<?php
trait ExportaCsv
{
    public function exportar(): string
    {
        return "nome;preco";
    }
}

trait ExportaJson
{
    public function exportar(): string
    {
        return '{"nome":"","preco":0}';
    }
}

class Relatorio
{
    use ExportaCsv, ExportaJson {
        ExportaCsv::exportar insteadof ExportaJson;
        ExportaJson::exportar as exportarJson;
    }
}

$r = new Relatorio();
echo $r->exportar(), "\n";
echo $r->exportarJson(), "\n";
use ExportaCsv, ExportaJson { … }
Usa os dois traits e abre um bloco de regras.
ExportaCsv::exportar insteadof ExportaJson;
insteadof = "em vez de". "Para exportar, use o do ExportaCsv em vez do ExportaJson." Isso resolve a colisão.
ExportaJson::exportar as exportarJson;
as = "como". "Traga também o exportar do ExportaJson, com o nome exportarJson." É um apelido: assim a versão JSON não se perde.
$ php resolvido.php
nome;preco                 « exportar(): o do ExportaCsv
{"nome":"","preco":0}      « exportarJson(): o do ExportaJson, pelo apelido
as sozinho não resolve conflito

O as acrescenta um nome novo, mas o nome antigo continua existindo e continua em conflito. Por isso o par é sempre: insteadof para escolher quem fica com o nome, e as para dar outro nome ao que perdeu.

as para mudar a visibilidade

O as também muda a visibilidade de um método do trait, só nesta classe. Pode até mudar a visibilidade e dar um apelido de uma vez:

<?php
trait TemHistorico
{
    public function registrar(string $m): void
    {
        echo "registrado: $m\n";
    }
}
class Pedido
{
    use TemHistorico {
        registrar as protected;
        registrar as public anotar;
    }
}
$p = new Pedido();
$p->anotar("ok");
$p->registrar("x");
$ php visibilidade-trait.php
registrado: ok

Fatal error: Uncaught Error: Call to protected method Pedido::registrar() from global scope in /home/ana/poo/visibilidade-trait.php:18
Stack trace:
#0 {main}
  thrown in /home/ana/poo/visibilidade-trait.php on line 18

registrar as protected; deixou o registrar protegido nesta classe; registrar as public anotar; criou o apelido público anotar. Chamar registrar de fora agora é recusado.


4. Quem ganha: classe, trait ou classe mãe?

Se o mesmo método existe em mais de um lugar, a regra de precedência é:

  1. A própria classe ganha de tudo.
  2. Depois, o trait.
  3. Por último, o que veio da classe mãe (herança). O trait substitui o método herdado.
<?php
class Base
{
    public function quemSou(): string
    {
        return "Base";
    }
}

trait Falante
{
    public function quemSou(): string
    {
        return "Trait (antes era " . parent::quemSou() . ")";
    }

    public function oi(): string
    {
        return "oi do trait";
    }
}

class Filha extends Base
{
    use Falante;

    public function oi(): string
    {
        return "oi da classe";
    }
}

$f = new Filha();
echo $f->quemSou(), "\n";
echo $f->oi(), "\n";
$ php precedencia.php
Trait (antes era Base)     « o trait substituiu o método da mãe (e ainda chamou o dela com parent::)
oi da classe               « o método da própria classe ganhou do trait

Isso é útil: a classe pode usar um trait e trocar só um dos métodos dele, escrevendo a sua versão.


5. Trait não é tipo: junte com uma interface

Como o trait só copia código, o objeto não "é" do trait. O instanceof responde false e um parâmetro com o tipo do trait recusa o objeto:

<?php
trait TemHistorico
{
    public function registrar(string $m): void {}
}
class Pedido { use TemHistorico; }
function salvar(TemHistorico $obj): void {}
var_dump(new Pedido() instanceof TemHistorico);
var_dump(class_uses(new Pedido()));
salvar(new Pedido());
$ php tipo.php
bool(false)                « o Pedido não "é" um TemHistorico
array(1) {
  ["TemHistorico"]=>
  string(12) "TemHistorico"     « class_uses lista os traits usados
}

Fatal error: Uncaught TypeError: salvar(): Argument #1 ($obj) must be of type TemHistorico, Pedido given, called in /home/ana/poo/tipo.php on line 10 and defined in /home/ana/poo/tipo.php:7
Stack trace:
#0 /home/ana/poo/tipo.php(10): salvar(Object(Pedido))
#1 {main}
  thrown in /home/ana/poo/tipo.php on line 7

A solução clássica é a dupla interface + trait: a interface é o tipo ("todo Registravel tem estes métodos"), e o trait é uma implementação pronta desses métodos, para não reescrevê-los em cada classe.

<?php
interface Registravel
{
    public function registrar(string $mensagem): void;
    public function historico(): array;
}

trait TemHistorico
{
    private array $historico = [];

    public function registrar(string $mensagem): void
    {
        $this->historico[] = $mensagem;
    }

    public function historico(): array
    {
        return $this->historico;
    }
}

class Pedido implements Registravel
{
    use TemHistorico;
}

function mostrar(Registravel $coisa): void
{
    echo implode(" > ", $coisa->historico()), "\n";
}

$p = new Pedido();
$p->registrar("criado");
$p->registrar("pago");
mostrar($p);
$ php interface-trait.php
criado > pago

O Pedido diz que é Registravel (implements) e cumpre a promessa com o trait (use). A função mostrar pede o tipo da interface, nunca o do trait.


Quando usar trait (e quando não)

SituaçãoMelhor escolha
A classe "é um tipo de" outra (Gerente é um Funcionario)Herança (extends)
Várias classes devem prometer os mesmos métodos, cada uma do seu jeitoInterface
Classes sem parentesco precisam do mesmo código (histórico, desconto, data de criação)Trait (de preferência junto com uma interface)
Uma classe precisa de um "ajudante" com estado e lógica próprios (enviar e-mail, gravar no banco)Outro objeto passado no construtor (composição)
Cuidado com o excesso

Traits são ótimos para pequenos pacotes de comportamento. Uma classe com cinco traits, cada um mexendo em propriedades que "contam" com as dos outros, vira um quebra-cabeça: para entender a classe, você precisa abrir seis arquivos. Se um trait começa a precisar de muita coisa da classe, provavelmente ele deveria ser um objeto separado.


Quando dá errado

Trait não encontrado
<?php
class Pedido
{
    use TemHistorico;
}
$ php sem-trait.php

Fatal error: Uncaught Error: Trait "TemHistorico" not found in /home/ana/poo/sem-trait.php:2
Stack trace:
#0 {main}
  thrown in /home/ana/poo/sem-trait.php on line 2

O que significa: "o trait TemHistorico não foi encontrado". O nome está errado ou o arquivo do trait não foi carregado.

Como consertar: confira o nome (maiúsculas contam para a leitura humana, mesmo que o PHP aceite diferenças) e carregue o arquivo com require ou com autoload. Traits são carregados pelo autoload do mesmo jeito que classes.

A mesma propriedade, com valores diferentes
<?php
trait Contador
{
    public int $total = 0;
}
class Carrinho
{
    use Contador;
    public int $total = 10;
}
$ php propriedade.php

Fatal error: Carrinho and Contador define the same property ($total) in the composition of Carrinho. However, the definition differs and is considered incompatible. Class was composed in /home/ana/poo/propriedade.php on line 6
Stack trace:
#0 {main}

O que significa: "Carrinho e Contador definem a mesma propriedade ($total)… a definição é diferente e incompatível". Para métodos, a classe pode substituir o do trait; para propriedades, não: o PHP só aceita se forem idênticas (mesma visibilidade, tipo e valor inicial).

Como consertar: declare a propriedade num lugar só (no trait ou na classe) ou use outro nome.

Colisão de métodos

Dois traits com o mesmo método na mesma classe dão Trait method … has not been applied as …, because of collision with …, como você viu na seção 3. Resolva com insteadof (quem fica com o nome) e, se quiser manter os dois, as (apelido para o outro).


Referência rápida

EscritaSignificado
trait Nome { … }Declara um trait (propriedades, métodos, abstratos, estáticos, constantes)
use Nome; (dentro da classe)Cola o trait na classe
use A, B;Usa vários traits
A::m insteadof B;Em caso de conflito, o m de A fica com o nome
B::m as outroNome;Apelido para um método do trait
m as protected;Muda a visibilidade nesta classe
m as private apelido;Apelido com outra visibilidade
abstract public function x(); no traitExige que a classe tenha x()
class_uses($obj)Lista os traits usados pela classe do objeto
__TRAIT__Nome do trait (dentro dele)
Precedênciaclasse > trait > classe mãe

Missão final

Missão: a atendente da lojinha sabe cumprimentar de dois jeitos, mas os dois traits usam o mesmo nome de método, e o PHP reclama da colisão. Resolva: o cumprimentar deve ser o do Formal (insteadof), e o do Informal deve continuar disponível com o nome cumprimentarAmigo (as).

<?php
trait Formal
{
    public function cumprimentar(string $nome): string
    {
        return "Bom dia, senhora $nome.";
    }
}

trait Informal
{
    public function cumprimentar(string $nome): string
    {
        return "E aí, $nome!";
    }
}

class Atendente
{
    use Formal, Informal;
}

$at = new Atendente();
echo $at->cumprimentar("Ana"), "\n";
echo $at->cumprimentarAmigo("Ana"), "\n";
<?php
trait Formal
{
    public function cumprimentar(string $nome): string
    {
        return "Bom dia, senhora $nome.";
    }
}

trait Informal
{
    public function cumprimentar(string $nome): string
    {
        return "E aí, $nome!";
    }
}

class Atendente
{
    use Formal, Informal {
        Formal::cumprimentar insteadof Informal;
        Informal::cumprimentar as cumprimentarAmigo;
    }
}

$at = new Atendente();
echo $at->cumprimentar("Ana"), "\n";
echo $at->cumprimentarAmigo("Ana"), "\n";

Exercícios

1. Colar o trait

Complete a linha, dentro da classe Cliente, que usa o trait TemHistorico:

2. Escolher no conflito

Os traits A e B têm o método salvar(). Complete para que fique o de A:

A::salvar B;

3. Apelido

Complete para que o salvar() do trait B continue disponível como salvarCopia():

B::salvar salvarCopia;

4. A classe Filha herda oi() da mãe, usa um trait que tem oi() e também escreve o seu próprio oi(). Qual roda em $filha->oi()?

5. A classe Pedido usa o trait TemHistorico. O que $pedido instanceof TemHistorico devolve?

6. Qual é a principal diferença entre usar um trait e usar herança?

7. Duas classes (Pedido e Produto) usam um trait com private static int $criados. Depois de criar 2 pedidos e 1 produto, contando cada um, quanto vale Pedido::criados()?


Resumo

  • Um trait é um pacote de propriedades e métodos que você cola em classes com use Nome;, como se tivesse escrito o código ali.
  • Serve para reaproveitar código entre classes sem parentesco. Uma classe pode usar vários traits (a herança só permite uma mãe).
  • Trait não vira objeto (Cannot instantiate trait) nem tipo (instanceof dá false); junte-o a uma interface para ter um tipo.
  • Pode ter métodos abstratos (o trait exige algo da classe), estáticos e constantes; cada classe ganha a sua cópia das propriedades estáticas.
  • Dois traits com o mesmo método: insteadof escolhe quem fica com o nome, as dá apelido (e pode mudar a visibilidade).
  • Precedência: a classe ganha do trait, que ganha da classe mãe.
  • Use com moderação: muitos traits interdependentes deixam a classe difícil de entender.