База данных · PPA

Пагинация

Пагинация разбивает большую выборку на страницы. Winter даёт два API: Wrapper::paginator() — для нумерованных страниц («стр. 3 из 8»), и Paginator — с тремя стратегиями (offset, array, cursor) для API. Оба возвращают JSON-совместимый результат, готовый отдать из контроллера.

Страницы Wrapper::paginator()Стратегии Paginator::repo/array/cursorUnit Pagination

Что такое пагинация и зачем

Пагинация — постраничная выдача результата вместо всех строк сразу.

Проблема. Вернуть тысячи строк одним ответом — это удар по памяти, задержке и трафику; клиенту столько за раз и не нужно. Считать LIMIT/OFFSET и COUNT(*) руками в каждом эндпоинте — рутина.

Решение. Передайте собранный запрос в пагинатор — он посчитает общее число, вырежет нужную страницу и вернёт готовый конверт {meta, data}. Об этом и раздел.

Нумерованные страницы — Wrapper::paginator()

Самый частый случай — админки и списки с номерами страниц. Сигнатура:

php
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 Трансформер каждого элемента страницы

Результат WrapResultJsonSerializable, его можно отдать напрямую:

main/PostController.php
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-центрик:

json
{
"meta": {
  "current": 3, "size": 20, "total": 156,
  "pages": 8, "previous": 2, "next": 4
},
"data": [ ... ]
}

previous / next равны null (не 0) на границах — удобно для типизированного фронтенда.

Трансформация элементов

mapper превращает сущности в DTO ответа до сборки страницы:

php
$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 Ленты, бесконечный скролл
php
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)

Дальше