当前位置:网站首页>Filter the eloquent model using multiple optional filters

Filter the eloquent model using multiple optional filters

2022-04-22 01:12:00 rorg

wpcmf: wpcmf cms , Content management system , similar wordpress System

When displayed in the view , We often need to filter eloquent Model . If we have a few filters , This could be good , But if you need to add multiple filters , The controller may become cluttered and difficult to read .

This is especially true when dealing with multiple optional filters that can be used in combination .

however , There are some ways to create these filters , You can even make them reusable . At the end of this article , You will be able to better handle the complex filtering options in your project .

Define the problem
For example , I have a controller method , It returns all the products in our store , This can be used for API, Pass it to the blade template , Or whatever , The controller may look like this :

<?php

namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Http\Request;

class ProductsController extends Controller
{
    public function index()
    {
        return Product::all();
    }
}


However , It's not realistic at all . Usually , We need to add some filters , for example , Suppose we just want to get products in a given category , We can send the category in the query string slug And then use slug Filter to do this . So our new controller looks like this :

class ProductsController extends Controller
{
    public function index()
    {
        if ($request->filled('category')) {
            $categorySlug = $request->category;

            return Product::whereHas('category', function ($query) use ($categorySlug) {
                $query->where('slug', $categorySlug);
            });
        }

        return Product::all();
    }
}


If we only need one filter , This is good . however , See what happens if we only introduce two other filtering options :

class ProductsController extends Controller
{
    public function index(Request $request)
    {
        $query = Product::query();

        if ($request->filled('price')) {
            list($min, $max) = explode(",", $request->price);

            $query->where('price', '>=', $min)
                  ->where('price', '<=', $max);
        }

    
        if ($request->filled('category')) {
            $categorySlug = $request->category;

            $query->whereHas('category', function ($query) use ($categorySlug) {
                $query->where('slug', $categorySlug);
            });
        }

        if ($request->filled('brand')) {
            $brandSlug = $request->brand;

            $query->whereHas('brand', function ($query) use ($brandSlug) {
                $query->where('slug', $brandSlug);
            });
        }


        return $query->get();
    }
}


We have introduced two more filters , One for brand slug, A price range . in my opinion , It's too busy , It's hard to track what's happening here , If we need to add more filtering options , Things will get worse soon .

for example , When you are in eBay When searching for products on , You usually get more than a dozen optional filters . We need to find another way to do this .

Imagine
If we didn't include all these filters in our controller , What if we can do something like this :

class ProductsController extends Controller
{
    public function index(ProductFilters $filters)
    {
        return Product::filter($filters)->get();
    }
}


ad locum , We received a ProductFilters Classes that may contain all filters , Then we can apply them to a project called The query scope of filter. This makes our controller very thin , It's easy to guess what happened to it . We are screening products , If we need more details , We can see ProductFilters class .

Implement new methods
First , Let's add scope to our model :

class Product extends Model
{
    use HasFactory;

    public function category() 
    {
        return $this->belongsTo(Category::class);
    }

    public function brand() 
    {
        return $this->belongsTo(Brand::class);
    }
    
    // This is the scope we added
    public function scopeFilter($query, $filters)
    {
        return $filters->apply($query);
    }
}


In this range , We receive a QueryBuilder Instance and ProductFilters The example we passed down from the controller .apply And then we're in this $filter Call method on instance .

up to now , We know this ProductFilters Class looks like this :

namespace App\Filters;

class ProductFilters
{
    public function apply($query)
    {
        if (request()->filled('price')) {
            list($min, $max) = explode(",", $request->price);

            $query->where('price', '>=', $min)
                  ->where('price', '<=', $max);
        }

    
        if (request()->filled('category')) {
            $categorySlug = $request->category;

            $query->whereHas('category', function ($query) use ($categorySlug) {
                $query->where('slug', $categorySlug);
            });
        }

        if (request()->filled('brand')) {
            $brandSlug = $request->brand;

            $query->whereHas('brand', function ($query) use ($brandSlug) {
                $query->where('slug', $brandSlug);
            });
        }


        return $query->get();
    }
}


This code works , But not much better than the filter in the controller . Instead of doing that , I want to have a separate filter class , Only their filtering logic .

Let's do this for the category filter :

namespace App\Filters;


class CategoryFilter
{
    function __invoke($query, $categorySlug)
    {
        return $query->whereHas('category', function ($query) use ($categorySlug) {
            $query->where('slug', $categorySlug);
        });
    }
}


Here we have a self-contained class , We can even use it in other models , Suppose we have a blog attached to the store , We can use the same filter for blog posts .

By the way , If you are not familiar with it __invoke Magic methods , More information can be found here :https ://www.php.net/manual/en/language.oop5.magic.php#object.invoke . In short , It just lets us call an instance of a class like a function .

After creating the class for the filter , We need to find a way from ProductFilters Method of calling middle note in class . There are many ways to do this , For this article , I will use the following methods :


namespace App\Filters;

class ProductFilters
{
    protected $filters = [
        'price' => PriceFilter::class,
        'category' => CategoryFilter::class,
        'brand' => BrandFilter::class,
    ];

    public function apply($query)
    {
        foreach ($this->receivedFilters() as $name => $value) {
            $filterInstance = new $this->filters[$name];
            $query = $filterInstance($query, $value);
        }

        return $query;
    }


    public function receivedFilters()
    {
        return request()->only(array_keys($this->filters));
    }
}


This is the completed course , Let me break down and explain each part .

How it works ?
First , Let's start with $filters Property start

 

   protected $filters = [
        'category' => CategoryFilter::class,
        'price' => PriceFilter::class,
        'brand' => BrandFilter::class,
    ];


ad locum , I defined each optional filter for the product , The key of the array is the key used to check whether the filter is in the request , The value of the array is the class that defines the behavior of the filter . This allows us to easily add more filters , When we need to add a new filter , We just need to create a new filter class and add it to this array .

The second part is this method :

    public function receivedFilters()
    {
        return request()->only(array_keys($this->filters));
    }


This method is used to find which filters are being used in the request , So we only apply the required filters . We also call the only Methods and $filters The key of the array is passed to it , To prevent us from trying to call a non-existent filter class . Suppose someone sends a filter “ size ”, But we don't support it at the moment , This will cause the request key to be ignored .

Now let's talk about this kind of meat ,apply Method :

    public function apply($query)
    {
        foreach ($this->receivedFilters() as $name => $value) {
            $filterInstance = new $this->filters[$name];
            $query = $filterInstance($query, $value);
        }

        return $query;
    }


This method is just a for each Loop through the received filter to create a filter class , And then use $query The value received in the request and the value call filter .

for example , Suppose we receive a request as follows :

['category' => 'mobile-phones', 'price' => '100,150']


This class will first create an instance ,CategoryFilter And then $query and “ mobile phone ” Pass it on . Essentially , It will do this :

$filterInstance = new CategoryFilter();
$query = $filterInstance($query, 'mobile-phones');


Next same thing PhoneFilter:

$filterInstance = new PhoneFilter();
$query = $filterInstance($query, '100,150');


Then after applying all the queries , It will return $query Objects with each request filter . This is what we received when we applied the scope in the controller :

class ProductsController extends Controller
{
    public function index(ProductFilters $filters)
    {
        return Product::filter($filters)->get();
    }
}


Conclusion
We created an extensible way to make multiple filters for our products , And here are some things we can improve , For example, add validation for filter values or create a base class ,ProductFilters So that we can add the same type of filter to other models .

版权声明
本文为[rorg]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/04/202204220033597214.html

随机推荐