Пагинация
Пагинация разбивает большую выборку на страницы. Winter даёт два API:
Wrapper::paginator() — для нумерованных страниц («стр. 3 из 8»), и Paginator —
с тремя стратегиями (offset, array, cursor) для API. Оба возвращают
JSON-совместимый результат, готовый отдать из контроллера.
Что такое пагинация и зачем
Пагинация — постраничная выдача результата вместо всех строк сразу.
Проблема. Вернуть тысячи строк одним ответом — это удар по памяти, задержке и
трафику; клиенту столько за раз и не нужно. Считать LIMIT/OFFSET и COUNT(*)
руками в каждом эндпоинте — рутина.
Решение. Передайте собранный запрос в пагинатор — он посчитает общее число,
вырежет нужную страницу и вернёт готовый конверт {meta, data}. Об этом и раздел.
Нумерованные страницы — Wrapper::paginator()
Самый частый случай — админки и списки с номерами страниц. Сигнатура:
final public static function paginator(
array|RepositoryViewInterface $repo,
int $limit,
int $page = 1,
?string $entityClassName = null,
?callable $mapper = null,
): WrapResult| Параметр | Назначение |
|---|---|
$repo |
Источник: репозиторий (SQL) или массив (in-memory) |
$limit |
Размер страницы (≥ 1, иначе ValueError) |
$page |
Номер страницы с 1 (по умолчанию 1) |
$entityClassName |
Переопределение гидрации (для репозитория) |
$mapper |
Трансформер каждого элемента страницы |
Результат WrapResult — JsonSerializable, его можно отдать напрямую:
use Flytachi\Winter\K2\Unit\Pagination\Wrapper;
#[GetMapping]
public function index(
#[RequestQuery] int $page = 1,
#[RequestQuery] int $limit = 20,
): ResponseEntity {
$result = Wrapper::paginator(
PostRepository::instance()
->where(Qb::eq('published', true))
->orderBy('id DESC'),
limit: $limit,
page: $page,
);
return ResponseEntity::ok($result);
}Форма ответа — page-центрик:
{
"meta": {
"current": 3, "size": 20, "total": 156,
"pages": 8, "previous": 2, "next": 4
},
"data": [ ... ]
}previous / next равны null (не 0) на границах — удобно для типизированного
фронтенда.
Трансформация элементов
mapper превращает сущности в DTO ответа до сборки страницы:
$result = Wrapper::paginator(
ProductRepository::instance()->orderBy('name ASC'),
limit: 20,
page: 1,
mapper: fn (Product $p) => ProductRes::from($p),
);
// data → list<ProductRes>Мутация репозитория
Для SQL-источника Wrapper вызывает $repo->limit(...) — состояние репозитория
меняется. Если планируете переиспользовать $repo после пагинации, склонируйте:
Wrapper::paginator(clone $base, ...).
Стратегии Paginator
Когда нужен offset-центрик ответ или курсорная выдача — используйте Paginator
напрямую:
| Метод | Стратегия | Когда |
|---|---|---|
Paginator::repo($repo, $size, $offset) |
Offset по репозиторию + COUNT(*) |
API с offset, фильтры |
Paginator::array($items, $size, $offset) |
Offset по массиву в памяти | Данные уже загружены |
Paginator::cursor($repo, $size, $key, $cursor) |
Курсор (before/after), без COUNT |
Ленты, бесконечный скролл |
use Flytachi\Winter\K2\Unit\Pagination\Paginator;
// Offset: {"meta":{"offset":0,"size":20,"total":156},"data":[...]}
$result = Paginator::repo(
UserRepository::instance()->where(Qb::eq('status', 'active'))->orderBy('id ASC'),
size: 20,
offset: 0,
);Курсор — константная цена
Курсорная пагинация (Paginator::cursor) не делает COUNT и стабильна при записи
во время листания — идеальна для лент и бесконечного скролла. Направление
закодировано в непрозрачном токене курсора.
Wrapper или Paginator?
Wrapper::paginator() |
Paginator::repo/array/cursor |
|
|---|---|---|
| Форма meta | page-центрик (current, pages, previous, next) |
offset-центрик (offset, size, total) или курсор |
| Для чего | Нумерованные страницы, админки | API, offset-клиенты, бесконечный скролл |
| Производительность | Идентична (делегирует в Paginator) |
— |
Дальше
- CRUD и выборка —
findAll, который пагинируется - Конструктор запросов — сборка запроса под пагинацию
- Ответы — отдача
WrapResultиз контроллера