本文目录导读:

- 方案一:简单封装(适合小型项目)
- 方案二:专用 Filter 类(推荐中大型项目)
- 方案三:使用 Laravel 管道(Pipeline)模式(高级/可扩展)
- 方案四:使用 Laravel 官方推荐的
Query Builder模式(Laravel 10+) - 企业级最佳实践总结
- 额外建议:过滤参数安全
设计 Laravel 的过滤服务(通常称为 Filter Service 或 Query Filter),核心目标是将复杂的查询条件(如 ?status=active&sort=created_at&category=tech)从控制器中解耦,使代码更干净、可测试且易于扩展。
下面提供一套从简单到企业级的渐进式设计方案,包含实战代码和最佳实践。
简单封装(适合小型项目)
如果条件较少,可以直接在模型上封装一个本地作用域。
定义 Trait
// app/Traits/Filterable.php
namespace App\Traits;
trait Filterable
{
public function scopeFilter($query, array $filters)
{
return $query
->when($filters['status'] ?? false, fn($q, $value) => $q->where('status', $value))
->when($filters['search'] ?? false, fn($q, $value) => $q->where('title', 'like', "%$value%"))
->when($filters['category'] ?? false, fn($q, $value) => $q->whereHas('category', fn($q) => $q->where('slug', $value)));
}
}
在模型中使用
class Post extends Model
{
use Filterable;
}
控制器调用
public function index(Request $request)
{
return Post::filter($request->only(['status', 'search', 'category']))
->latest()
->paginate();
}
缺点:条件多了以后 Trait 会膨胀,无法复用。
专用 Filter 类(推荐中大型项目)
将每个模型的过滤逻辑抽离为独立的类,支持依赖注入和单元测试。
创建抽象基类
// app/Services/Filter/QueryFilter.php
namespace App\Services\Filter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
abstract class QueryFilter
{
protected Builder $builder;
protected Request $request;
protected array $filters = [];
public function __construct(Request $request)
{
$this->request = $request;
}
public function apply(Builder $builder): Builder
{
$this->builder = $builder;
foreach ($this->getFilters() as $name => $value) {
if (method_exists($this, $name)) {
$this->$name($value);
}
}
return $this->builder;
}
protected function getFilters(): array
{
// 只返回在 $filters 白名单中的参数
return array_filter($this->request->only($this->filters));
}
}
创建具体的 Filter 类
// app/Services/Filter/PostFilter.php
namespace App\Services\Filter;
class PostFilter extends QueryFilter
{
protected array $filters = ['status', 'search', 'category', 'date_from', 'date_to'];
public function status($value)
{
$this->builder->where('status', $value);
}
public function search($value)
{
$this->builder->where(function ($q) use ($value) {
$q->where('title', 'like', "%$value%")
->orWhere('content', 'like', "%$value%");
});
}
public function category($slug)
{
$this->builder->whereHas('category', fn($q) => $q->where('slug', $slug));
}
public function dateFrom($date)
{
$this->builder->whereDate('created_at', '>=', $date);
}
public function dateTo($date)
{
$this->builder->whereDate('created_at', '<=', $date);
}
// 可以添加排序逻辑
public function sort($field)
{
$direction = $this->request->get('direction', 'asc');
$this->builder->orderBy($field, $direction);
}
}
在模型中使用
class Post extends Model
{
public function scopeFilter($query, PostFilter $filter)
{
return $filter->apply($query);
}
}
控制器中注入
use App\Services\Filter\PostFilter;
class PostController extends Controller
{
public function index(PostFilter $filter)
{
return Post::filter($filter)->latest()->paginate();
}
}
使用 Laravel 管道(Pipeline)模式(高级/可扩展)
适用于需要多个中间步骤处理查询的场景(如权限过滤、数据脱敏、缓存的查询)。
创建 Pipeline 类
// app/Services/Filter/PipelineFilter.php
namespace App\Services\Filter;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
class PipelineFilter
{
protected Request $request;
protected array $pipelines = [];
protected array $filters = [];
public function __construct(Request $request)
{
$this->request = $request;
}
public function addFilter($filter): self
{
$this->filters[] = $filter;
return $this;
}
public function addPipeline(callable $pipeline): self
{
$this->pipelines[] = $pipeline;
return $this;
}
public function apply(Builder $builder): Builder
{
// 先应用所有过滤器
foreach ($this->filters as $filter) {
$builder = (new $filter($this->request))->apply($builder);
}
// 再运行管道
foreach ($this->pipelines as $pipeline) {
$builder = $pipeline($builder);
}
return $builder;
}
}
创建具体的管道步骤
// app/Services/Filter/Pipelines/SortPipe.php
namespace App\Services\Filter\Pipelines;
use Closure;
use Illuminate\Database\Eloquent\Builder;
class SortPipe
{
public function handle(Builder $builder, Closure $next, $order = 'created_at')
{
$sortedBuilder = $builder->orderBy($order, 'desc');
return $next($sortedBuilder);
}
}
在控制器中使用
use App\Services\Filter\PipelineFilter;
use App\Services\Filter\PostFilter;
use App\Services\Filter\Pipelines\SortPipe;
class PostController extends Controller
{
public function index(Request $request)
{
$pipeline = new PipelineFilter($request);
$pipeline->addFilter(PostFilter::class)
->addPipeline(fn($builder) => $builder->where('visible', true));
return Post::query()
->tap(fn($query) => $pipeline->apply($query))
->paginate();
}
}
使用 Laravel 官方推荐的 Query Builder 模式(Laravel 10+)
利用 Laravel 10 新增的 Builder 宏和 invokable 类。
创建 Invokable Filter 类
// app/QueryFilters/PostStatusFilter.php
namespace App\QueryFilters;
use Closure;
use Illuminate\Database\Eloquent\Builder;
class PostStatusFilter
{
public function __construct(protected string $status) {}
public function __invoke(Builder $query, Closure $next)
{
if ($this->status === 'active') {
$query->where('status', 'published');
}
return $next($query);
}
}
在控制器中链式调用
use App\QueryFilters\PostStatusFilter;
use App\QueryFilters\SearchFilter;
public function index(Request $request)
{
return Post::query()
->tap(new PostStatusFilter($request->status))
->tap(new SearchFilter($request->search))
->paginate();
}
企业级最佳实践总结
| 特性 | 简单 Trait | 专用 Filter 类 | Pipeline | Invokable |
|---|---|---|---|---|
| 代码量 | 少 | 中 | 多 | 中 |
| 可测试性 | 差 | 好 | 极好 | 好 |
| 扩展性 | 低 | 高 | 极高 | 高 |
| 复用性 | 低 | 高 | 极高 | 高 |
| 适合项目 | 小型CRUD | 中大型项目 | 复杂业务系统 | 微服务/Laravel10+ |
额外建议:过滤参数安全
- 白名单验证:在
$filters属性中只声明允许的参数。 - 类型转换:
$this->builder->where('price', '>=', (float) $value)。 - SQL 注入防护:使用参数绑定,避免拼接
orderBy(request('sort'))→ 使用白名单映射。
protected array $sortable = ['created_at', 'title', 'price'];
public function sort($field)
{
if (!in_array($field, $this->sortable)) {
return; // 忽略非法排序
}
$this->builder->orderBy($field, $this->request->get('direction', 'asc'));
}
选择哪种方案取决于你的项目阶段:
- 快速原型 → 方案一
- 标准商业项目 → 方案二
- 需要高度可扩展(如 SaaS) → 方案三 / 方案四
如果还有特定场景(如多表关联过滤、嵌套条件、API 资源过滤)需要更具体的代码,可以告诉我,我可以帮你扩展。