支持Laravel.io的持续发展 →

使用多个可选过滤器过滤Eloquent模型

2022年2月10日 阅读时间:7分钟

通常我们在将模型显示到视图时需要过滤Eloquent模型。如果有少量过滤器,这没问题,但如果你需要添加多个过滤器,控制器可能会变得杂乱且难以阅读。

这在使用多个可选项过滤器进行联合时更为明显。

然而,有一些创建这些过滤器的办法,甚至可以使其可重用。在本篇文章结束时,您将更好的应对您项目中的复杂过滤选项。

定义问题

假设,例如,我们有一个控制器方法,它返回我们商店中的所有产品,这可能用于API、传递给blade模板,或任何其他情况,控制器可能看起来像这样

<?php

namespace App\Http\Controllers;

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

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

然而,这完全不实际。通常,我们需要添加一些过滤,例如,假设我们只想获取给定类别的产品,我们可以通过在查询字符串中发送类别的slug来实现,然后使用slug进行过滤。因此,我们新的控制器可能看起来像这样

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();
    }
}

如果我们只需要一个过滤器,那就没问题。但是,看看我们引入 仅仅两个额外的过滤选项 会发生什么。

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();
    }
}

我们引入了两个额外的过滤器,一个用于品牌缩写,一个用于价格范围。在我看来,这太复杂了,很难跟踪这里发生的事情,如果我们需要添加更多的过滤选项,这可能会迅速变得更糟。

例如,当你在eBay上搜索产品时,通常会得到十个或更多的可选过滤器。我们需要寻找另一种处理方式。

梦想

如果我们的控制器中没有所有这些过滤器,我们能否做些类似这样的操作

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

这里我们接收一个 ProductFilters 类,它可能包含我们所有的过滤器,然后我们将其应用于名为 filter 的查询作用域。这使得我们的控制器非常简洁,很容易猜出发生了什么。我们正在过滤产品,如果需要更多详细信息,我们然后可以查看 ProductFilters 类。

实现新方法

首先,让我们添加作用域到我们的模型中

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);
    }
}

在这个作用域中,我们收到了一个 QueryBuilder 实例和从控制器传递下来的 ProductFilters 实例。然后我们在 $filter 这个实例上调用 apply 方法。

到目前为止,我们知道 ProductFilters 类看起来可能如下所示

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();
    }
}

这段代码可以工作,但并不比在控制器中直接添加过滤器好多少。相反,我会希望有单独的过滤类,它们只包含自己的过滤逻辑。

让我们为类别过滤器这样做

namespace App\Filters;


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

这里有一个独立存在的类,我们甚至可以在其他模型中使用,比如说,如果我们的商店链接一个博客,我们可以为博客文章使用相同的过滤器。

顺便说一下,如果你不熟悉 __invoke 魔法方法,你可以在这里找到更多信息:https://php.ac.cn/manual/en/language.oop5.magic.php#object.invoke。简而言之,它只是让我们可以像调用函数一样调用类的实例。

在我们为过滤器创建类之后,我们需要找到从 ProductFilters 类调用它们的方法。有许多方法可以做到这一点,对于本文,我将采取以下方法


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));
    }
}

这是我们完成的类,让我来解释每个部分。

这是怎么工作的?

首先,让我们从 $filters 属性开始。

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

在这里,我定义了产品的每个可选过滤器,数组的键是我们用来检查过滤器是否在请求中的键,数组的值是定义过滤器行为的类。这使得我们很容易添加更多的过滤器,当我们需要添加一个新过滤器时,我们只需创建新的过滤器类并将其添加到该数组中。

谜题的第二部分是这个方法。

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

这个方法用来找出请求中正在使用哪些过滤器,所以我们只为需要的过滤器应用。我们还在调用 only 方法并传递 $filters 数组的键,以防止我们尝试调用不存在的过滤器类。比如说有人发送了“size”过滤器,但我们目前不支持它,这将忽略请求的键。

现在让我们来谈谈这个类的主体部分,即 apply 方法。

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

        return $query;
    }

这个方法只是一个循环,通过接收到的过滤器创建一个类,然后用 $query 和请求中接收到的值调用过滤器。

例如,假设我们收到一个如下所示的请求

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

这个类将首先创建一个 CategoryFilter 的实例,然后将其传递给 $query 和 "mobile-phones"。实质上,它会这样做

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

下一部分对 PhoneFilter 也是一样的。

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

在应用完所有查询后,它将返回每个请求的筛选条件的 $query 对象。在我们的控制器应用范围时,我们就能接收到这些内容。

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

结论

我们创建了一个可扩展的方式来为我们的产品制作多个筛选器,这里有一些我们可以改进的地方,例如添加筛选值的验证或从 ProductFilters 创建一个基类,这样我们就可以将相同类型的筛选添加到其他模型中。但我会把这个留给你作为挑战。

如果您对此有任何疑问或评论,请给我发送电子邮件至 [email& regexp教导]=b9dad6kadcvadfkdvcjad4dc]),=b9dad6kadcvadfkdvcjad4]?îte 或通过 Twitter 给我发推文,请转发给我 @cosmeescobedo。我很乐意回答您的问题。

最后更新 1 年前。

driesvints、cosmeoes、ovillafuerte94、iamadriandev、arnonm-intel、mathewslima、simonbarrettact、phcostabh、simomrh、elkdev 等人喜欢了这篇文章

14
喜欢这篇文章吗?让作者知道并为他们点赞!

你可能还喜欢这些文章

2024年3月11日

如何使用 Larastan 将您的 Laravel 应用从 0 运行到 9

在 Laravel 应用执行之前发现错误是可能的,感谢 Larastan,它可以...

阅读文章
2024年7月19日

无需使用 traits 标准化 API 响应

我注意到大多数用于 API 响应的库都是使用 traits 实现的,...

阅读文章
2024年7月17日

通过 Discord 通知在您的 Laravel 项目中收集反馈

如何在 Laravel 项目中创建反馈模块,并在收到消息时通过 Discord...

阅读文章

我们感谢这些 极佳的公司 支持

您的标志在这里?

Laravel.io

问题解决、知识共享和社区建设的 Laravel 门户。

© 2024 Laravel.io - 版权所有。