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.
- 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
insteadofeas. - 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
)
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". "Paraexportar, use o doExportaCsvem vez doExportaJson." Isso resolve a colisão.- ExportaJson::exportar as exportarJson;
as= "como". "Traga também oexportardoExportaJson, com o nomeexportarJson." É 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 é:
- A própria classe ganha de tudo.
- Depois, o trait.
- 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ção | Melhor 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 jeito | Interface |
| 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) |
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
<?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.
<?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.
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
| Escrita | Significado |
|---|---|
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 trait | Exige que a classe tenha x() |
class_uses($obj) | Lista os traits usados pela classe do objeto |
__TRAIT__ | Nome do trait (dentro dele) |
| Precedência | classe > 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:
3. Apelido
Complete para que o salvar() do trait B continue disponível como 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 (instanceofdá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:
insteadofescolhe quem fica com o nome,asdá 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.