PHP, Traits, Laravel & Beyond

Segundo a documentação do próprio PHPOs 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 queUm 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 exemplosUm 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 HandleImagestrait 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 bootNa 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<?phpnamespace App;use IlluminateDatabaseEloquentModel;use IlluminateDatabaseEloquentBuilder;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 abaixotrait 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<?phpnamespace App;use IlluminateDatabaseEloquentModel;use IlluminateDatabaseEloquentBuilder;use AppTraitsFilterBlockedclass 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 construtorpublic 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 abaixoMétodo initializeTraits da classe abstrata IlluminatedatabaselobmasterEloquentModelPrimeiro, 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 abaixotrait 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 ModelsDa 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 IlluminateDatabaseEloquentModel 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 AppUser;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 IlluminateDatabaseEloquentModel .

É 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çõesSeria 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.

BeyondEste 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… :).

. More details

Leave a Reply