Laravel Query Builder v7: Laravel 构建 API 的必备套件
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,将仅选中 id 和 email 字段。
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 资源类已经妥善解决了输出格式的问题,但我们的查询构建器则专注于处理输入端:它负责从请求中解析出 filter、sort、include 和 fields 等参数,并将其转化为相应的 Eloquent 查询语句。两者强强联手,助你以极少的样板代码,即可实现一套完整的 JSON:API 解决方案。