Определение запросов
Definitions API описывает в одном классе, что запрос клиента может получить от ресурса: поля, подгрузки связей, фильтры, сортировки и пагинацию. Он добавляет типизированные фильтры с операторами, сортировки по связям, виртуальные поля, вложенные определения и сообщает обо всех ошибках запроса в одном ответе 422.
Определение
use App\Models\Post;
use App\Models\User;
use MyProgs\QueryBuilder\Definitions\Field;
use MyProgs\QueryBuilder\Definitions\Filter;
use MyProgs\QueryBuilder\Definitions\Pagination;
use MyProgs\QueryBuilder\Definitions\QueryDefinition;
use MyProgs\QueryBuilder\Definitions\Relationship;
use MyProgs\QueryBuilder\Definitions\Sort;
final class UserQuery extends QueryDefinition
{
public function model(): string
{
return User::class;
}
public function type(): string
{
return 'users';
}
public function fields(): array
{
return ['id', 'name', 'email', Field::virtual('display_name', dependsOn: ['name', 'nickname'])];
}
public function includes(): array
{
return [
Relationship::to('posts', PostQuery::class)->withFilters(),
'profile',
Relationship::count('posts'),
];
}
public function filters(): array
{
return [
Filter::partial('name'),
Filter::bool('active'),
Filter::datetime('created_at')->operators(),
Filter::int('posts.views')->operators(),
];
}
public function sorts(): array
{
return ['name', 'created_at', Sort::relation('profile.country.name')->as('country'), Sort::count('posts')];
}
public function defaultSorts(): array
{
return ['-created_at'];
}
public function pagination(): Pagination
{
return Pagination::make(default: 20, max: 100);
}
}
final class PostQuery extends QueryDefinition
{
public function model(): string
{
return Post::class;
}
public function type(): string
{
return 'posts';
}
public function fields(): array
{
return ['id', 'title', 'views', 'published_at'];
}
public function filters(): array
{
return [Filter::exact('status')];
}
public function sorts(): array
{
return ['views'];
}
}model() и type() обязательны, остальные методы — нет: то, чего определение не объявляет, запрос клиента просить не может.
Контроллер только запускает его:
public function index(Request $request)
{
return UserQuery::for($request)->paginate();
}GET /users?fields[users]=name&fields[posts]=title&include=posts,postsCount&filter[posts.views][gte]=20&sort=-nameselect "users"."name", "users"."id",
(select count(*) from "posts" where "users"."id" = "posts"."user_id") as "posts_count"
from "users"
where exists (select * from "posts" where "users"."id" = "posts"."user_id" and "posts"."views" >= ?)
order by "users"."name" descТот же фильтр сужает и подгруженные посты, а запрос постов выбирает title вместе с нужными ему ключами.
Определения создаются через контейнер, поэтому могут принимать зависимости в конструкторе. Переопределите query(), чтобы начинать не с Model::query(), а с уже ограниченного builder:
public function query(): Builder
{
return User::query()->where('team_id', $this->team->id);
}Запрос без класса
Разовому запросу класс определения не нужен:
use MyProgs\QueryBuilder\Definitions\Query;
Query::for(User::class) // or a builder, or a relation
->fields('id', 'name')
->includes('posts')
->filters(Filter::exact('name'))
->sorts('name')
->defaultSorts('name')
->pagination(default: 20, max: 100)
->get();Запрос клиента берётся из контейнера; другой можно передать вторым аргументом for(). Без определения тип ресурса — имя таблицы.
Те же методы можно вызывать цепочкой после UserQuery::for($request): поля, подгрузки связей, фильтры и сортировки добавляются к объявленным в определении, а type(), defaultSorts() и pagination() заменяют их.
Когда выполняется запрос
Запрос не трогается до первого вызова, читающего запрос: get, paginate, first, toSql, getEloquentBuilder и так далее. Тогда запрос клиента проверяется целиком, и применяются фильтры, сортировки, поля и подгрузки связей.
validate()проверяет запрос клиента, ничего не выполняя.getSubject()возвращает builder в том виде, в каком он был передан.- Остальные методы builder (
where,with, скоупы, ...) уходят прямо во внутренний builder.
Запись выполняется по отфильтрованным строкам: UserQuery::for($request)->update([...]), delete(), increment() и подобные проверяют запрос клиента и применяют его фильтры, но не сортировки, поля и подгрузки связей.
Макрос, зарегистрированный через Builder::macro() (на Eloquent builder, базовом query builder или связи), считается вызовом, читающим запрос, поэтому объявления после него в цепочке бросают LogicException. Макросы, которые глобальный скоуп добавляет в своём extend() (withTrashed(), onlyTrashed(), ваш собственный withArchived()), только ограничивают запрос и оставляют цепочку открытой; restore(), restoreOrCreate() и createOrRestore() читают или пишут его.
select() на запросе заменяется запрошенными полями, как и select, с которым был передан исходный builder; addSelect(), withCount() и другие колонки, добавленные позже, сохраняются.