Laravel过滤服务怎么设计

wen PHP项目 12

本文目录导读:

Laravel过滤服务怎么设计

  1. 方案一:简单封装(适合小型项目)
  2. 方案二:专用 Filter 类(推荐中大型项目)
  3. 方案三:使用 Laravel 管道(Pipeline)模式(高级/可扩展)
  4. 方案四:使用 Laravel 官方推荐的 Query Builder 模式(Laravel 10+)
  5. 企业级最佳实践总结
  6. 额外建议:过滤参数安全

设计 Laravel 的过滤服务(通常称为 Filter ServiceQuery 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 资源过滤)需要更具体的代码,可以告诉我,我可以帮你扩展。

抱歉,评论功能暂时关闭!