PHP, Traits, Laravel & Beyond

Leonardo Cavalcante
sysvale
Published in
7 min readApr 3, 2019

É com muito prazer e paráfrase que gostaria de anunciar o tímido retorno de quem nunca esteve aqui… Há algum tempo não escrevo nada, e chegou a hora de compartilhar algumas experiências.

Meu intuito é mostrar alguns padrões interessantes surgidos da combinação dos Traits do PHP, junto com classes do Laravel. Os padrões mostrados são em sua totalidade relacionados ao uso dos Traits em classes de Models, o que não exclui outras possibilidades interessantes.

Este post é o primeiro de uma série de outros que discutirão soluções que surgem a partir das idéias apresentadas neste primeiro. Colocá-las em um post único tornaria a leitura um pouco longa e possivelmente enfadonha. Eles são listados a seguir (mais serão adicionados, na medida em que forem sendo escritos):

Mas vamos deixar de conversa fiada, vamos ao que interessa!

Definindo Traits

O que são Traits? Segundo a documentação do próprio PHP

Os Traits são um mecanismo para reuso de código em linguagens de herança única, como o PHP. Um Trait tem o intuito de reduzir algumas limitações da herança única permitindo ao desenvolvedor reutilizar conjuntos de métodos livremente, em uma variedade de classes independentes, vivendo em diferentes hierarquias de classes. A semântica da combinação de Traits e classes é definida de uma maneira que reduz a complexidade, e evita os problemas típicos associados a herança múltipla e Mixins.

Apesar de ser uma definição bem completa, a documentação vai um pouco além, ao explicar que

Um Trait é semelhante a uma classe, porém com o objetivo de agrupar funcionalidades de uma maneira sucinta e consistente. Não se pode instanciar um Trait por si só. Ele é uma adição à herança tradicional e permite a composição horizontal de comportamento. Ou seja, a aplicação de membros de classes sem a necessidade de heranças.

Até onde a analogia permite, se você já trabalhou (ou trabalha) com Javascript, verá que há semelhanças entre os Traits e o spread operator.

Fomentando discussões a partir de exemplos

Um exemplo fala mais que nenhum exemplo. Se tivermos dois métodos, em um determinado Controller Foo, responsáveis pelo download e upload de imagens no nosso backend, por exemplo

//...
class Foo extends Controller
{
// ...
public function upload(Request $request)
{
// Lógica de upload
}
public function download(Request $request)
{
// Lógica de download
}
// ...
}

E quisermos reutilizá-los em algum outro Controller, digamos, Bar, poderíamos declarar o Trait HandleImages

trait HandleImages {
public function upload(Request $request)
{
// Lógica de upload
}
public function download(Request $request)
{
// Lógica de download
}
}

E, então, poderíamos refatorar o Controller Foo em

// ...
class Foo extends Controller
{
use HandleImages;
// ...
}

E finalmente utilizar o Trait em Bar

// ... 
class Bar extends Controller
{
use HandleImages;
// ...
}

Outro ponto interessante é que poderíamos reutilizar o Trait então definido em qualquer outra classe (claro, desde que seu uso fosse razoável).

Vale ressaltar que, por se tornarem, efetivamente, métodos dos controllers, os métodos upload e download do Trait têm acesso a qualquer um de seus membros ou métodos através do $this , o que também é verdade para membros e métodos estáticos e constantes, através do self ou static .

Além disso os Traits não se restringem aos métodos, sendo também possível declarar propriedades, ou até mesmo métodos abstratos.

Traits e botas, Models e boot

Na documentação do Laravel, no que diz respeito a Models, há um trecho interessante que fala sobre escopos globais. Em resumo, é possível aplicar uma condição ou restrição a toda query realizada a partir de um determinado Model. Desta forma, se tivermos uma lista de usuários, representada pelo Model User, e, por padrão, quisermos listar somente os usuário não-bloqueados, indicados por um atributo blocked com valor false , teríamos o seguinte cenário

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class User extends Model
{
protected static function boot()
{
parent::boot();

static::addGlobalScope('non-blocked', function (Builder $builder) {
$builder->where('blocked', false);
});
}
}

Neste contexto, pode vir a mente o seguinte questionamento: “E se tivermos um outro Model que queira implementar o mesmo escopo global?”

Uma alternativa é declarar uma classe de escopo e então reutilizá-la, como descrito na documentação do Laravel. Outra alternativa é misturar o boot com os Traits discutidos anteriormente, e tornar o processo de adição do escopo global mais transparente.

Embora o segundo método seja um pouco questionável, é algo utilizado pelo próprio framework. Por exemplo, você pode ter se deparado com o Trait SoftDeletes.

Seguindo esse caminho, teríamos o Trait FilterBlocked, mostrado abaixo

trait FilterBlocked {
protected static function boot()
{
parent::boot();

static::addGlobalScope('non-blocked', function (Builder $builder) {
$builder->where('blocked', false);
});
}
}

De modo que o utilizaríamos no Model User como mostrado abaixo

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use App\Traits\FilterBlockedclass User extends Model
{
use FilterBlocked;
}

Note que o Trait foi assumido como sendo declarado na pasta app/Traits .

Neste momento, você pode estar se questionando “Mas e se eu quiser adicionar mais funcionalidades ao método boot, por exemplo, um outro escopo global? Teria de incluí-las no Trait? Teria de defini-las em um método de nome diferente, e então chamá-las no método boot do Model?”

Felizmente são questionamentos que o framework já resolve. Na implementação da classe Model, nos deparamos com o seguinte código do construtor

public function __construct(array $attributes = []) {
$this->bootIfNotBooted();
$this->initializeTraits(); // preste atenção neste cara!
$this->syncOriginal();
$this->fill($attributes);
}

Analizando mais de perto o método initializeTraits , vemos algo como o mostrado abaixo

Método initializeTraits da classe abstrata Illuminate\database\blob\master\Eloquent\Model

Primeiro, estamos interessados no que acontece nas linhas 10 e 11.

Explicando um pouco o caminho entre as árvores, o método class_uses_recursive é um helper do Laravel, que basicamente itera recursivamente por todos os Traits usados por uma classe, o que significa que ele itera, também, por todos os Traits usados por classes pai.

No caso das nossas linhas batutas, é verificado, na linha 10, se um método boot<NomeDoTrait> existe na classe e não foi, ainda, “bootado”. Então, se for esse o caso, o método (estático) é chamado na linha 11.

No fim das contas isso significa que podemos declarar o nosso Trait FilterBlocked , por exemplo, como abaixo

trait FilterBlocked {
protected static function bootFilterBlocked()
{
static::addGlobalScope('non-blocked', function (Builder $builder) {
$builder->where('blocked', false);
});
}
}

Veja que a chamada ao parent::boot() foi removida, uma vez que é responsabilidade do método boot do Model.

Chegamos a algo interessante, podemos declarar Traits que são injetados no método boot de Models sem sobreescrevê-los, se você ainda não está convencido do pontencial dessa ideia, sugiro que continue a leitura!

É vento, é furacão: declarando event listenners através de métodos estáticos em Models

Da documentação do Laravel, sabemos que é possível declarar listenners para eventos de CRUD em Models. Tais eventos são sempre sobre antes ou depois de uma determinada operação de CRUD ter sido realizada em um Model. Todavia, eles dependem da criação de classes associadas. No momento, nossa busca está pretenciosamente direcionada a algo que possa ser utilizado de modo mais autocontido no Model, em específico algo que permita que listenners, por exemplo, sejam declarados no método boot.

Consultar a API do Laravel, em específico no que diz respeito a classe Illuminate\Database\Eloquent\Model mostra algo interessante. Essa classe possui os métodos estáticos creating , created , updating , updated e outros associados às operações de CRUD em Models, que aceitam callbacks que recebem como argumento o Model onde a operação está sendo ou foi realizada.

Fornecendo um exemplo interessante, o seguinte Trait faz com que sempre seja salvo o id do usuário que cria o Model no qual ele é aplicado.

//...use Auth;
use App\User;
trait TracksCreator {
protected static function bootTracksCreator()
{
static::creating(function (Model $model) {
$model->creator_id = Auth::user()->id;
});
}

public function creator()
{
$this->belongsTo(User::class);
}
}

De bônus o Trait também injeta no Model o método de relacionamento com o criador.

Por fim, note que o callback é declarado como recebendo uma instância Illuminate\Database\Eloquent\Model . É só lembrar que todo Model é uma instância dessa classe, de forma que isso funciona para qualquer Model.

Neste ponto, o intuito é que tenhamos bagagem o suficiente para partir para os estudos de caso e padrões interessantes que surgem quando se misturam Traits, Models e event listenners.

Algumas Considerações

Seria desonesto de minha parte se não colocasse aqui o post que encontrei quando me veio a mente procurar saber se era possível declarar métodos de boot independentes a partir de Traits. Pois bem, me deparei com este cara aqui. Há um post semelhante que, assim como discuti aqui, discute a implementação da classe abstrata Model do framework, li há algum tempo e não consegui encontrá-lo, repeti a ideia aqui porque achei interessante a atitude de mostrar o que está acontecendo, mesmo que ligeiramente, nos bastidores do framework.

Beyond

Este post discutiu algumas ideias e buscou fomentar a imaginação sobre a mistura de alguns conceitos do PHP e do Laravel. Os próximos posts darão a este a razão de ser e existir, fique atento! Mas só se quiser… :)

--

--