编程

Laravel Query Builder v7: Laravel 构建 API 的必备套件

10 2026-04-10 10:02:00

Spatie 刚刚发布了 spatie/laravel-query-builder 的 v7 版本。这是一个旨在帮助你轻松构建灵活 API 端点的软件包。如果你正在使用 Laravel 构建 API,几乎肯定会需要允许调用方对结果进行过滤、排序、加载关联关系以及选取特定字段。若为每一个端点都手动编写这些逻辑,工作很快就会变得枯燥重复;而且,这也极易导致你无意中暴露了原本不打算公开的字段或关联关系。

Spaatie 的查询构建器能够为你妥善处理这一切。它能自动读取 URL 中的查询参数,将其转化为相应的 Eloquent 查询语句,并确保只有你明确允许查询的内容才会被执行。

// GET /users?filter[name]=John&include=posts&sort=-created_at

$users = QueryBuilder::for(User::class)
    ->allowedFilters('name')
    ->allowedIncludes('posts')
    ->allowedSorts('created_at')
    ->get();

// select * from users where name = 'John' order by created_at desc

此主版本要求使用 PHP 8.3 及以上版本,以及 Laravel 12 或更高版本;它带来了更加简洁的 API,并包含了一些期盼已久的功能。

接下来,让我带你了解该扩展包的运作原理以及其中的新增特性。

使用该包

其理念非常简单:API 调用方通过 URL 传递查询参数,而该扩展包会将这些参数转化为相应的 Eloquent 查询。你只需定义允许的查询规则即可。

假设你有一个 User 模型,并希望允许 API 调用方根据姓名进行筛选。你所需要做的全部工作如下:

use Spatie\QueryBuilder\QueryBuilder;

$users = QueryBuilder::for(User::class)
    ->allowedFilters('name')
    ->get();

现在,当有人请求 /users?filter[name]=John 时,该软件包会将相应的 WHERE 子句添加到查询中:

select * from users where name = 'John'

仅有你明确允许的过滤器才会生效。如果有人尝试访问 /users?filter[secret_column]=something,该扩展包将抛出 InvalidFilterQuery 异常。你的数据库表结构将对 API 调用方保持隐蔽。

你可以一次性允许多个过滤器,并将其与排序功能结合使用:

$users = QueryBuilder::for(User::class)
    ->allowedFilters('name', 'email')
    ->allowedSorts('name', 'created_at')
    ->get();

现在,针对 /users?filter[name]=John&sort=-created_at 的请求将按名称进行过滤,并按创建时间(created_at)进行降序排序(前缀 - 表示降序)。

引入关联(Relationship)的操作方式也完全相同。假设你希望调用方能够对用户的文章(Post)进行预加载(Eager-load):

$users = QueryBuilder::for(User::class)
    ->allowedFilters('name', 'email')
    ->allowedIncludes('posts', 'permissions')
    ->allowedSorts('name', 'created_at')
    ->get();

现在,向 /users?include=posts&filter[name]=John&sort=-created_at 发送请求,将返回名为 John 的用户列表;该列表按创建日期排序,且其关联的帖子已通过“预加载”(eager-loading)方式一并加载。

你还可以指定特定的字段进行筛选,以使响应数据保持精简:

$users = QueryBuilder::for(User::class)
    ->allowedFields('id', 'name', 'email')
    ->allowedIncludes('posts')
    ->get();

若使用 /users?fields=id,email&include=posts,将仅选中 idemail 字段。

QueryBuilder 扩展了 Laravel 默认的 Eloquent 构建器,因此你常用的所有方法依然有效。你可以将其与现有的查询结合使用:

$query = User::where('active', true);

$users = QueryBuilder::for($query)
    ->withTrashed()
    ->allowedFilters('name')
    ->allowedIncludes('posts', 'permissions')
    ->where('score', '>', 42)
    ->get();

查询参数的命名尽可能严格遵循 JSON API 规范。这意味着你将获得一个一致且文档详尽的 API 接口,而无需费心考量命名约定。

v7 的新特性

可变参数

所有 allowed* 允许的方法 现在均接受可变参数,而非数组。

// Before (v6)
QueryBuilder::for(User::class)
    ->allowedFilters(['name', 'email'])
    ->allowedSorts(['name'])
    ->allowedIncludes(['posts']);

// After (v7)
QueryBuilder::for(User::class)
    ->allowedFilters('name', 'email')
    ->allowedSorts('name')
    ->allowedIncludes('posts');

如果是动态列表,请使用扩展运算符:

$filters = ['name', 'email'];
QueryBuilder::for(User::class)->allowedFilters(...$filters);

聚合关联(Aggregate Includes)

这是本次更新中最重要的全新功能。现在,你可以使用 AllowedInclude::min()AllowedInclude::max()AllowedInclude::sum()AllowedInclude::avg() 方法,为关联模型包含聚合数值。在底层实现上,这些方法对应于 Laravel 框架中的 withMin()withMax()withSum()withAvg() 方法。

use Spatie\QueryBuilder\AllowedInclude;

$users = QueryBuilder::for(User::class)
    ->allowedIncludes(
        'posts',
        AllowedInclude::count('postsCount'),
        AllowedInclude::sum('postsViewsSum', 'posts', 'views'),
        AllowedInclude::avg('postsViewsAvg', 'posts', 'views'),
    )
    ->get();

现在,向 /users?include=posts,postsCount,postsViewsSum 发送请求时,将返回用户数据,并附带其发布的文章、文章总数以及所有文章的总浏览量。

你还可以对这些聚合数据进行筛选限制。例如,若仅需统计已发布的文章:

use Spatie\QueryBuilder\AllowedInclude;
use Illuminate\Database\Eloquent\Builder;

$users = QueryBuilder::for(User::class)
    ->allowedIncludes(
        AllowedInclude::count(
            'publishedPostsCount',
            'posts',
            fn (Builder $query) => $query->where('published', true)
        ),
        AllowedInclude::sum(
            'publishedPostsViewsSum',
            'posts',
            'views',
            constraint: fn (Builder $query) => $query->where('published', true)
        ),
    )
    ->get();

所有四种聚合类型均支持这些约束闭包,从而使你能够构建这样的端点:在返回模型数据的同时,一并返回计算所得的数据,且无需编写自定义的查询逻辑。

与 Laravel 的 JSON:API 资源完美匹配

Laravel 13 新增了对 JSON:API 资源的内置支持。这些新的 JsonApiResource 类负责序列化:它们生成的响应符合 JSON:API 规范。

你可以通过添加 --json-api 标志来创建此类:

php artisan make:resource PostResource --json-api

这将生成一个资源类,用于定义属性和关联:

use Illuminate\Http\Resources\JsonApi\JsonApiResource;

class PostResource extends JsonApiResource
{
    public $attributes = [
        'title',
        'body',
        'created_at',
    ];

    public $relationships = [
        'author',
        'comments',
    ];
}

从控制器中将其返回,Laravel 就会生成一个完全符合 JSON:API 规范的响应:

{
    "data": {
        "id": "1",
        "type": "posts",
        "attributes": {
            "title": "Hello World",
            "body": "This is my first post."
        },
        "relationships": {
            "author": {
                "data": { "id": "1", "type": "users" }
            }
        }
    },
    "included": [
        {
            "id": "1",
            "type": "users",
            "attributes": { "name": "Taylor Otwell" }
        }
    ]
}

客户端可以通过诸如 /api/posts?fields[posts]=title&include=author 这样的查询参数,来请求特定的字段(fields)和关联资源(includes)。在响应端,Laravel 的 JSON:API 资源类会全权负责处理这些请求。

Laravel 的官方文档明确提及了我们的扩展包,并将其推荐为一个理想的配套工具:

“Laravel 的 JSON:API 资源类负责处理响应数据的序列化工作。如果您还需要解析传入的 JSON:API 查询参数(例如过滤条件和排序规则),那么 Spatie 推出的 Laravel Query Builder 将是一个极佳的配套扩展包。”

因此,尽管 Laravel 新推出的 JSON:API 资源类已经妥善解决了输出格式的问题,但我们的查询构建器则专注于处理输入端:它负责从请求中解析出 filtersortincludefields 等参数,并将其转化为相应的 Eloquent 查询语句。两者强强联手,助你以极少的样板代码,即可实现一套完整的 JSON:API 解决方案。