Pressione enter para ver os resultados ou esc para cancelar.

Laravel: filtrando queries utilizando scopes

É 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.

  • Eita… Massa, hein? Mas, por exemplo, em uma loja cada User teria os próprios Products… Vou SEMPRE colocar a chamada de método para o scope?
  • Não! 😎

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.