Пагинация данных в PHP: объектный подход для работы с внешними источниками
Пагинация в PHP при интеграции с API: объектно-ориентированное решение
Как создать универсальный класс для постраничного получения данных из внешних API?
Основой эффективной работы с пагинацией является класс Paginator, который инкапсулирует логику навигации, хранение параметров запроса и обработку ответа. Предполагается, что внешний API возвращает данные в формате JSON с полями data, total, per_page, current_page и last_page (типичная структура Laravel или JSON:API).
class ApiPaginator {
private string $baseUrl;
private array $headers;
private int $page;
private int $perPage;
private array $payload;
private int $lastPage = 1;
private int $total = 0;
public function __construct(string $baseUrl, int $perPage = 15, array $headers = []) {
$this->baseUrl = $baseUrl;
$this->perPage = $perPage;
$this->headers = $headers;
$this->page = 1;
}
public function fetch(): ?array {
$url = $this->buildUrl();
$response = $this->httpGet($url);
if (!$response) {
return null;
}
$this->parseResponse($response);
return $this->payload['data'] ?? null;
}
public function nextPage(): bool {
if ($this->page < $this->lastPage) {
$this->page++;
return true;
}
return false;
}
public function previousPage(): bool {
if ($this->page > 1) {
$this->page--;
return true;
}
return false;
}
public function hasPages(): bool {
return $this->lastPage > 1;
}
private function buildUrl(): string {
$query = http_build_query([
'page' => $this->page,
'per_page'=> $this->perPage
]);
return $this->baseUrl . '?' . $query;
}
private function httpGet(string $url): ?string {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $this->headers,
CURLOPT_TIMEOUT => 10
]);
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
return null;
}
return $result;
}
private function parseResponse(string $json): void {
$this->payload = json_decode($json, true);
$this->total = (int)($this->payload['total'] ?? 0);
$this->lastPage = (int)($this->payload['last_page'] ?? 1);
$this->page = (int)($this->payload['current_page'] ?? 1);
}
// геттеры для отладки
public function getPage(): int { return $this->page; }
public function getTotal(): int { return $this->total; }
public function getLastPage(): int { return $this->lastPage; }
}Php get pages (получение страниц (пагинация) в php)
Пояснение шагов:
- Конструктор принимает базовый URL эндпоинта, количество элементов на странице и заголовки (например, для авторизации).
- fetch() собирает URL с query-параметрами, выполняет GET-запрос, разбирает ответ и возвращает массив данных текущей страницы.
- nextPage() и previousPage() управляют номером страницы. После вызова fetch() снова вернёт данные новой страницы.
- hasPages() позволяет проверить, есть ли несколько страниц.
Типичные ошибки:
- Игнорирование HTTP-кодов ответа (например, 404 при последней странице). Решение: проверять
httpCodeи выбрасывать исключение. - Неправильные имена полей в ответе API (total вместо count). Решение: сделать поля настраиваемыми через конструктор или сеттеры.
- Бесконечный цикл при вызове nextPage() после последней страницы. Решение: проверять
$this->page < $this->lastPage.
Цели использования: класс ApiPaginator подходит для интеграции с любым REST API, поддерживающим пагинацию через query-параметры. Он позволяет итерировать страницы в цикле без ручного управления URL.
Как реализовать пагинацию через LIMIT/OFFSET в SQL с использованием ООП?
Если данные хранятся в локальной базе данных, и требуется организовать постраничный вывод через собственное API, можно создать класс SqlPaginator. Он принимает PDO-запрос и параметры.
class SqlPaginator {
private \PDO $pdo;
private string $baseQuery;
private array $baseParams;
private int $perPage;
private int $page = 1;
private int $total;
public function __construct(\PDO $pdo, string $baseQuery, array $baseParams = [], int $perPage = 20) {
$this->pdo = $pdo;
$this->baseQuery = $baseQuery;
$this->baseParams = $baseParams;
$this->perPage = $perPage;
$this->total = $this->countTotal();
}
private function countTotal(): int {
$countQuery = "SELECT COUNT(*) FROM ({$this->baseQuery}) AS cnt";
$stmt = $this->pdo->prepare($countQuery);
$stmt->execute($this->baseParams);
return (int)$stmt->fetchColumn();
}
public function fetch(): array {
$offset = ($this->page - 1) * $this->perPage;
$limitQuery = $this->baseQuery . " LIMIT :limit OFFSET :offset";
$stmt = $this->pdo->prepare($limitQuery);
$params = array_merge($this->baseParams, [':limit' => $this->perPage, ':offset' => $offset]);
$stmt->execute($params);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
public function nextPage(): bool {
$maxPage = (int)ceil($this->total / $this->perPage);
if ($this->page < $maxPage) {
$this->page++;
return true;
}
return false;
}
public function getPage(): int { return $this->page; }
public function getTotal(): int { return $this->total; }
}Проблемы:
- Смещение OFFSET при большом количестве страниц замедляет запрос. Решение: заменить на курсорную пагинацию (поиск по последнему ID).
- Если данные изменяются между запросами, возможны дубликаты или пропуски. Решение: фиксировать сортировку и использовать транзакции только для чтения.
Как реализовать курсорную пагинацию при работе с API?
Некоторые API используют курсоры (например, GraphQL Relay, Twitter API). Вместо номера страницы передаётся зашифрованный маркер после/до определённого элемента.
class CursorPaginator {
private string $baseUrl;
private string $cursorParam = 'after';
private ?string $nextCursor = null;
private bool $hasMore = true;
private array $headers;
public function __construct(string $baseUrl, array $headers = []) {
$this->baseUrl = $baseUrl;
$this->headers = $headers;
$this->nextCursor = null;
}
public function fetch(): ?array {
if (!$this->hasMore && $this->nextCursor !== null) return null;
$url = $this->buildUrl();
$response = $this->httpGet($url);
if (!$response) return null;
$data = json_decode($response, true);
$this->nextCursor = $data['pagination']['next_cursor'] ?? null;
$this->hasMore = $data['pagination']['has_more'] ?? false;
return $data['data'] ?? [];
}
private function buildUrl(): string {
$query = http_build_query(array_filter(['cursor' => $this->nextCursor]));
return $this->baseUrl . '?' . $query;
}
private function httpGet(string $url): ?string { /* ... аналогично ApiPaginator */ }
}Частая ошибка: неверное определение следующего курсора при пустом ответе. Решение: проверять наличие ключа next_cursor и обнулять его только при явном значении null.
Расширенные примеры использования пагинации в PHP с ООП
Ниже представлены дополнительные сценарии, которые демонстрируют гибкость объектно-ориентированного подхода при работе с пагинированными данными.
Пример 1. Итерация по всем страницам внешнего API с помощью ApiPaginator
$paginator = new ApiPaginator('https://api.example.com/users', 50, ['Authorization: Bearer token123']);
$allUsers = [];
do {
$users = $paginator->fetch();
if ($users !== null) {
$allUsers = array_merge($allUsers, $users);
}
} while ($paginator->nextPage());
echo 'Получено пользователей: ' . count($allUsers);
// Результат: 'Получено пользователей: 120'Пример 2. Рендеринг нумерации страниц на основе данных SqlPaginator
$pdo = new PDO('mysql:host=localhost;dbname=blog', 'user', 'pass');
$paginator = new SqlPaginator($pdo, 'SELECT * FROM posts WHERE status = "published" ORDER BY created_at DESC', [], 10);
$currentPage = 1;
$totalPages = ceil($paginator->getTotal() / 10);
echo '';Пример 3. Обработка API, который использует ссылки rel="next" (RFC 5988)
Некоторые REST-API возвращают заголовок Link с rel="next". Класс LinkHeaderPaginator извлекает URL следующей страницы из заголовка.
class LinkHeaderPaginator {
private string $currentUrl;
private ?string $nextUrl = null;
private array $headers;
public function __construct(string $initialUrl, array $headers = []) {
$this->currentUrl = $initialUrl;
$this->headers = $headers;
$this->nextUrl = $initialUrl; // первая итерация
}
public function fetch(): ?array {
if ($this->nextUrl === null) return null;
$response = $this->httpGet($this->nextUrl);
if (!$response) return null;
$this->parseLinks($response['headers'] ?? '');
return json_decode($response['body'], true);
}
private function parseLinks(string $headerValue): void {
preg_match('/<([^>]+)>; rel="next"/', $headerValue, $matches);
$this->nextUrl = $matches[1] ?? null;
}
private function httpGet(string $url): ?array {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_HTTPHEADER => $this->headers
]);
$response = curl_exec($ch);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = substr($response, 0, $headerSize);
$body = substr($response, $headerSize);
curl_close($ch);
return ['headers' => $headers, 'body' => $body];
}
}// Пример использования:
$paginator = new LinkHeaderPaginator('https://api.github.com/repos/owner/repo/issues?page=1');
$page1 = $paginator->fetch();
$page2 = $paginator->fetch(); // получаем вторую страницуПример 4. Пагинация с использованием GuzzleHttp и обработкой ошибок
Если проект использует Guzzle, можно создать обёртку с автоматическим повторением при таймаутах.
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
class GuzzlePaginator {
private Client $client;
private string $endpoint;
private array $queryDefaults;
public function __construct(Client $client, string $endpoint) {
$this->client = $client;
$this->endpoint = $endpoint;
$this->queryDefaults = ['page' => 1, 'limit' => 20];
}
public function fetch(int $page = null): array {
$params = $this->queryDefaults;
if ($page !== null) $params['page'] = $page;
try {
$response = $this->client->get($this->endpoint, ['query' => $params]);
$body = json_decode($response->getBody(), true);
return [
'data' => $body['data'] ?? [],
'total_pages'=> $body['total_pages'] ?? 1,
'current' => $body['page'] ?? 1
];
} catch (ClientException $e) {
// логирование и возврат пустого массива
return ['data' => [], 'total_pages' => 0, 'current' => 0];
}
}
}Пример 5. Комбинирование пагинации с кэшированием результатов
Для ускорения повторных запросов к API полезно кэшировать ответы страниц. Ниже пример с использованием файлового кэша.
class CachedApiPaginator extends ApiPaginator {
private string $cacheDir;
private int $ttl;
public function __construct(string $baseUrl, int $perPage, array $headers, string $cacheDir, int $ttl = 300) {
parent::__construct($baseUrl, $perPage, $headers);
$this->cacheDir = rtrim($cacheDir, '/');
$this->ttl = $ttl;
if (!is_dir($this->cacheDir)) {
mkdir($this->cacheDir, 0755, true);
}
}
public function fetch(): ?array {
$cacheKey = md5($this->buildUrl());
$cacheFile = $this->cacheDir . '/' . $cacheKey . '.json';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $this->ttl)) {
$this->parseResponse(file_get_contents($cacheFile));
return $this->payload['data'] ?? null;
}
$data = parent::fetch();
if ($data !== null && $this->payload) {
file_put_contents($cacheFile, json_encode($this->payload));
}
return $data;
}
}Результат: при повторном обращении к одной и той же странице в течение TTL запрос к API не выполняется, данные берутся из кэша.