通常我们在将模型显示到视图时需要过滤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。我很乐意回答您的问题。
driesvints、cosmeoes、ovillafuerte94、iamadriandev、arnonm-intel、mathewslima、simonbarrettact、phcostabh、simomrh、elkdev 等人喜欢了这篇文章