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