Traits são um mecanismo de reuso de código em PHP que permite compartilhar métodos entre classes que não pertencem à mesma hierarquia de herança. Eles resolvem o problema de herança única do PHP, possibilitando o reuso horizontal de código sem os problemas de acoplamento da herança múltipla. Nesta aula, vamos aprender como declarar, usar e gerenciar conflitos entre traits.

Diferentemente de classes abstratas ou interfaces, traits fornecem implementações concretas de métodos que podem ser 'inseridos' em classes. Eles são especialmente úteis para comportamentos transversais (cross-cutting concerns) como logs, cache, serialização, etc.

Reuso horizontal de código

Em PHP, uma classe pode herdar de apenas uma classe pai (herança única). Isso limita o reuso de código quando precisamos compartilhar funcionalidades entre classes não relacionadas. Traits oferecem uma alternativa: você pode agrupar um conjunto de métodos em um trait e então 'usá-lo' em várias classes, independentemente da hierarquia.

Exemplo: suponha que temos classes Carro, Bicicleta e Barco. Todas precisam de um método ligar() e desligar(). Em vez de duplicar o código ou forçar uma herança artificial, podemos criar um trait Motor com esses métodos e usá-lo em cada classe.

trait Motor {
    public function ligar() {
        echo "Motor ligado.\n";
    }

    public function desligar() {
        echo "Motor desligado.\n";
    }
}

class Carro {
    use Motor;
}

class Bicicleta {
    use Motor;
}

class Barco {
    use Motor;
}

$carro = new Carro();
$carro->ligar(); // Motor ligado.
$bicicleta = new Bicicleta();
$bicicleta->desligar(); // Motor desligado.

Isso promove reuso sem acoplamento rígido. Cada classe mantém sua própria hierarquia, mas ganha os métodos do trait.

use em classes

A palavra-chave use dentro de uma classe importa um ou mais traits. A sintaxe é use NomeDoTrait;. Você pode usar múltiplos traits separando-os por vírgula.

trait A {
    public function metodoA() { echo "A\n"; }
}

trait B {
    public function metodoB() { echo "B\n"; }
}

class MinhaClasse {
    use A, B;
}

$obj = new MinhaClasse();
$obj->metodoA(); // A
$obj->metodoB(); // B

Traits também podem usar outros traits (composição de traits). Basta usar a mesma sintaxe dentro do trait:

trait C {
    use A, B;
    public function metodoC() { echo "C\n"; }
}

class OutraClasse {
    use C;
}

$obj2 = new OutraClasse();
$obj2->metodoA(); // A
$obj2->metodoC(); // C

Conflitos

Quando dois traits fornecem métodos com o mesmo nome, ocorre um conflito fatal. Para resolver, usamos o operador insteadof para escolher qual método usar, e as para criar um alias.

trait A {
    public function conflito() { echo "A\n"; }
}

trait B {
    public function conflito() { echo "B\n"; }
}

class MinhaClasse {
    use A, B {
        B::conflito insteadof A; // usa o método de B no lugar de A
        A::conflito as conflitoA; // cria um alias para o método de A
    }
}

$obj = new MinhaClasse();
$obj->conflito();   // B
$obj->conflitoA();  // A

Também é possível alterar a visibilidade de um método importado usando as com modificador de acesso:

trait A {
    public function metodoPublico() { echo "público\n"; }
}

class MinhaClasse {
    use A {
        metodoPublico as private; // torna o método privado
    }
}

$obj = new MinhaClasse();
// $obj->metodoPublico(); // Erro: método privado

Quando usar

Traits são ideais para comportamentos compartilhados entre classes não relacionadas, como logging, cache, validação, formatação, etc. Evite traits quando o comportamento é intrinsicamente ligado à hierarquia da classe (use herança ou interfaces). Também não abuse de traits para criar 'herança múltipla' artificial, pois pode tornar o código difícil de entender.

Boas práticas:

  • Mantenha traits pequenos e focados em uma única responsabilidade.
  • Documente os métodos do trait e quaisquer dependências (ex: métodos que o trait espera que a classe tenha).
  • Prefira interfaces para contratos e traits para implementações reutilizáveis.
  • Evite traits que definem propriedades, pois podem causar conflitos de nomes.
  • Use traits para código que não muda frequentemente; para comportamentos dinâmicos, considere estratégias como composição.

Referências

Exercícios

  1. Crie um trait chamado Logavel com um método log($mensagem) que imprime a mensagem prefixada com a data atual. Em seguida, use-o em uma classe Usuario e chame o método.

    ✓ Resposta:
    trait Logavel {
        public function log($mensagem) {
            echo date('Y-m-d H:i:s') . ' - ' . $mensagem . "\n";
        }
    }
    
    class Usuario {
        use Logavel;
    }
    
    $user = new Usuario();
    $user->log('Usuário logado.');
  2. Declare dois traits: TraitA com método foo() e TraitB com método foo(). Crie uma classe que use ambos e resolva o conflito para que o método foo() chame a implementação de TraitB e crie um alias para o método de TraitA como fooA().

    ✓ Resposta:
    trait TraitA {
        public function foo() {
            echo "TraitA\n";
        }
    }
    
    trait TraitB {
        public function foo() {
            echo "TraitB\n";
        }
    }
    
    class MinhaClasse {
        use TraitA, TraitB {
            TraitB::foo insteadof TraitA;
            TraitA::foo as fooA;
        }
    }
    
    $obj = new MinhaClasse();
    $obj->foo();  // TraitB
    $obj->fooA(); // TraitA
  3. Crie um trait Singleton que implemente o padrão Singleton (instância única) com método estático getInstance(). Use-o em uma classe Config e demonstre que ambas as variáveis apontam para a mesma instância.

    ✓ Resposta:
    trait Singleton {
        private static $instance = null;
    
        public static function getInstance() {
            if (self::$instance === null) {
                self::$instance = new self();
            }
            return self::$instance;
        }
    }
    
    class Config {
        use Singleton;
        private $dados = [];
        public function set($chave, $valor) {
            $this->dados[$chave] = $valor;
        }
        public function get($chave) {
            return $this->dados[$chave] ?? null;
        }
    }
    
    $config1 = Config::getInstance();
    $config1->set('db_host', 'localhost');
    
    $config2 = Config::getInstance();
    var_dump($config1 === $config2); // true
    echo $config2->get('db_host'); // localhost
  4. Defina um trait Comparavel com um método abstrato equals($outro) que deve ser implementado pela classe que o usar. Crie uma classe Pessoa com propriedades nome e idade, implemente o método equals para comparar nome e idade. Teste com duas pessoas.

    ✓ Resposta:
    trait Comparavel {
        abstract public function equals($outro): bool;
    }
    
    class Pessoa {
        use Comparavel;
    
        private $nome;
        private $idade;
    
        public function __construct($nome, $idade) {
            $this->nome = $nome;
            $this->idade = $idade;
        }
    
        public function equals($outro): bool {
            if (!$outro instanceof Pessoa) return false;
            return $this->nome === $outro->nome && $this->idade === $outro->idade;
        }
    }
    
    $p1 = new Pessoa('João', 30);
    $p2 = new Pessoa('João', 30);
    $p3 = new Pessoa('Maria', 25);
    
    var_dump($p1->equals($p2)); // true
    var_dump($p1->equals($p3)); // false
  5. Crie um trait Timestampable que adiciona métodos getCreatedAt() e getUpdatedAt() e define propriedades privadas $createdAt e $updatedAt. Use-o em uma classe Post e inicialize as propriedades no construtor (dica: o trait pode definir um método initializeTimestamps() chamado no construtor).

    ✓ Resposta:
    trait Timestampable {
        private $createdAt;
        private $updatedAt;
    
        public function initializeTimestamps() {
            $this->createdAt = new DateTime();
            $this->updatedAt = new DateTime();
        }
    
        public function getCreatedAt(): DateTime {
            return $this->createdAt;
        }
    
        public function getUpdatedAt(): DateTime {
            return $this->updatedAt;
        }
    
        public function updateTimestamp() {
            $this->updatedAt = new DateTime();
        }
    }
    
    class Post {
        use Timestampable;
    
        private $titulo;
    
        public function __construct($titulo) {
            $this->titulo = $titulo;
            $this->initializeTimestamps();
        }
    
        public function setTitulo($titulo) {
            $this->titulo = $titulo;
            $this->updateTimestamp();
        }
    }
    
    $post = new Post('Meu post');
    echo $post->getCreatedAt()->format('Y-m-d H:i:s');
    $post->setTitulo('Novo título');
    echo $post->getUpdatedAt()->format('Y-m-d H:i:s');