Пагинация данных в PHP: объектный подход для работы с внешними источниками

Раздел: Продвинутое программирование и интеграция -> Работа с API и ООП

Пагинация в 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)

Пояснение шагов:

  1. Конструктор принимает базовый URL эндпоинта, количество элементов на странице и заголовки (например, для авторизации).
  2. fetch() собирает URL с query-параметрами, выполняет GET-запрос, разбирает ответ и возвращает массив данных текущей страницы.
  3. nextPage() и previousPage() управляют номером страницы. После вызова fetch() снова вернёт данные новой страницы.
  4. 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 не выполняется, данные берутся из кэша.

Получение страниц (пагинация) в PHP - comments

En
Php get pages (php)