Ícone do site Taller

Laravel: filtrando queries utilizando scopes

Laravel_filtrando_queries_utilizando_scopes_Taller_Blog

É comum aparecer situações em que precisamos repetir trechos de códigos para fazer filtros em um projeto. Para resolver esse tipo de problema, o Laravel (mais precisamente, o Eloquent) possui um recurso para filtrar de forma “automágica” as consultas.

Já pararam pra pensar como uma collection do Laravel trata os soft deletes de forma automática? É utilizado um recurso chamado scope, que serve exatamente para isso, definir um filtro que remove todos os resultados com deleted_at preenchido.

Algo parecido com isso:

User::whereNull('deleted_at')->get();

 

Assim sempre vamos retornar apenas os usuários não excluídos, sendo possível chamar diretamente:

User::get();

 

Ok, mas, como podemos usar o scope para personalizar outros filtros? Por exemplo, no caso de produtos, vamos trabalhar nisso e ver o que conseguimos. 😁

Existem duas maneiras para isso: Local Scopes e Global Scopes.

Local Scopes

Local Scopes podem ser chamados dinamicamente pela sua aplicação. Não seria exatamente o caso de um deleted_at. Mas, ajuda quando o filtro é necessário apenas em alguns pontos do sistema. É perfeito para situações em que temos estados, como ativo/inativos…

Para adicionar um filtro com scope precisamos apenas criar um método na model iniciando o nome scope. Esse método receberá NO MÍNIMO o parâmetro Builder da query e também retornará um objeto da classe Builder. 

use \Illuminate\Database\Eloquent\Builder;
 
class Product extends Model
{
   public function scopeActive(Builder $query): Builder
   {
       return $query->where('active', true);
   }
}

 

Assim quando for necessário consultar apenas os produtos ativos, podemos usar a sintaxe abaixo:

$activeProducts = Product
   ::active()
   ->get();

 

(Repare que aqui o método foi chamado apenas pelo nome APÓS o scope utilizado na declaração do método.)

Bem mais prático e claro do que a versão sem scope:

$activeProducts = Product
   ::where('active', true)
   ->get();

 

Repare que falei logo acima que recebe NO MÍNIMO?

O scope pode receber N parâmetros após a $query, podemos fazer scopes para coletar por status de forma dinâmica, por exemplo:

use \Illuminate\Database\Eloquent\Builder;
 
class Product extends Model
{
   public function scopeActive(Builder $query): Builder
   {
       return $query->where('active', true);
   }
 
   public function scopeStatus(Builder $query, string $status): Builder
   {
       return $query->where('status', $status);
   }
}

 

O uso do status vira um parâmetro do scope criado:

$products = Product
   ::status('active')
   ->get();
// Ou
$products = Product
   ::status('inactive')
   ->get();

 

Nesse formato podemos ter quantos parâmetros forem necessários para a realidade do projeto, ali utilizei apenas o primeiro parâmetro como string. Reparar que o Builder do primeiro parâmetro é suprimido na utilização da consulta.

Para os casos em que SEMPRE (ou quase sempre) vamos utilizar um scope, existe um recurso chamado Global Scope, que vamos ver agora.

Global Scopes

Ao contrário do Local Scope, o Global Scope não precisa ser chamado, o uso fica implícito em cada consulta, exatamente como o deleted_at que já é nativo do Laravel.

Quando criamos Global Scope, esse scope recebe dois parâmetros: o primeiro é o nome do scope, e no segundo é uma função que recebe e retorna o $builder para que seja manipulada a consulta.

Como, por exemplo, para pegar todos os produtos do usuário logado no sistema:

use Illuminate\Database\Eloquent\Builder;
 
class Product extends Model
{
 
   protected static function booted()
   {
       static::addGlobalScope('UserProducts', function (Builder $builder) {
           $builder->where('user_id', Auth::id());
           return $builder;
       });
   }
}

 

Para fazer consultas com esse formato é bem simples… Simplesmente ignoramos que o scope existe e é só realizar a consulta normalmente!

Product::get();

 

Fácil, não?

O Primeiro parâmetro que ali foi denominado UserProducts pode ser qualquer nome, serve para manipular o uso, assim como é adicionado, podemos ignorar o scope em alguma consulta:

Product::withoutGlobalScope('UserProducts')->get();

 

Dessa forma, coletamos todos os produtos ignorando o scope, vindo produtos de todos os usuários.

Porém, da forma que foi adicionado ali, pode ser meio confuso, ainda mais se a função para manipular o scope for muito grande. Para isso podemos criar uma classe separada. Por exemplo:

Arquivo do scope:

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
 
class UserScope implements Scope
{
 
   public function apply(Builder $builder, Model $model)
   {
       $builder->where("user_id", Auth::id());
       return $builder;
   }
}

Model utilizando a classe do scope:

class Product extends Model
{
 
   protected static function booted()
   {
       static::addGlobalScope(new UserScope);
   }
}

 

Assim a classe da model fica mais organizada, limpa e os scopes podem ficar em uma estrutura separada, sendo possível até ser reutilizado. Repare que o nome do scope é genérico, qualquer model que trabalhe com o user_id pode consumir esse mesmo scope.

No caso de um scope em classe separada, podemos usar próprio nome para ignorar o seu uso:

Product::withoutGlobalScope(UserScope::class)->get();

 

Podemos inclusive, misturar os usos, com local e global scope, como no exemplo abaixo:

class Product extends Model
{
 
   protected static function booted()
   {
       static::addGlobalScope(new UserScope);
   }
 
   public function scopeActive(Builder $query): Builder
   {
       return $query->where('active', true);
   }
}

 

Assim podemos consultar todos os produtos ativos apenas do usuário logado:

Product::active()->get();

 

Bem melhor do que repetir os filtros abaixo em cada consulta, não é mesmo? 😁

Product::where("user_id", Auth::id())
   ->where("active", true)
   ->get();

 

Concluindo, os scopes do Laravel ajudam a remover repetições, deixam o código mais legível e ainda centralizam filtros comuns, ajudando na manutenção do código. Ficou com alguma dúvida? Deixa aqui nos comentários e até a próxima!

Aproveite que já ta por aqui e confira outros dos nossos tutoriais aqui no blog.

Sair da versão mobile