Skip to content

Определение запросов ​

Definitions API описывает в одном классе, что запрос клиента может получить от ресурса: поля, подгрузки связей, фильтры, сортировки и пагинацию. Он добавляет типизированные фильтры с операторами, сортировки по связям, виртуальные поля, вложенные определения и сообщает обо всех ошибках запроса в одном ответе 422.

Определение ​

php
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() обязательны, остальные методы — нет: то, чего определение не объявляет, запрос клиента просить не может.

Контроллер только запускает его:

php
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=-name
sql
select "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:

php
public function query(): Builder
{
    return User::query()->where('team_id', $this->team->id);
}

Запрос без класса ​

Разовому запросу класс определения не нужен:

php
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() и другие колонки, добавленные позже, сохраняются.