Механизм пагинации в веб-приложениях на PHP через индекс p

Раздел: Веб-разработка -> Вывод данных (пагинация)

Реализация пагинации в PHP

Какое решение пагинации является наиболее эффективным и безопасным?

Основной подход использует запросы SQL с LIMIT и OFFSET через PDO. Номер страницы передаётся как p в URL. Вычисляется смещение: offset = (page - 1) * perPage. Общее количество записей получается отдельным запросом COUNT(*). Это даёт число страниц ceil(total / perPage). Затем формируются ссылки на каждую страницу.


<?php
$perPage = 10;
$currentPage = isset($_GET['p']) ? (int)$_GET['p'] : 1;
if ($currentPage < 1) $currentPage = 1;
$offset = ($currentPage - 1) * $perPage;

$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);

$totalStmt = $pdo->query("SELECT COUNT(*) FROM articles");
$total = $totalStmt->fetchColumn();
$totalPages = ceil($total / $perPage);

$stmt = $pdo->prepare("SELECT * FROM articles ORDER BY created_at DESC LIMIT :limit OFFSET :offset");
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

// Вывод статей
foreach ($rows as $row) {
    echo '<div class="fw-bold">' . htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8') . '</div>';
}

// Пагинация
if ($totalPages > 1) {
    for ($i = 1; $i <= $totalPages; $i++) {
        $active = ($i == $currentPage) ? ' class="active"' : '';
        echo '<a href="?p=' . $i . '"' . $active . '>' . $i . '</a> ';
    }
}
?>

Index php p 2 (пагинация в php через p=2)

Цель: безопасная и производительная работа с базами данных. Случай использования: проекты с умеренным объёмом данных (до нескольких сотен тысяч записей).

Типичные ошибки:

  • Использование непроверенного GET-параметра p в SQL без подготовки – уязвимость для инъекций. Решение: PDO с prepared statements и привязка параметров.
  • Отсутствие проверки на отрицательные и нулевые значения p – может привести к некорректному OFFSET. Решение: if ($currentPage < 1) $currentPage = 1;
  • Необработанное превышение максимальной страницы – пользователь может ввести p=999. Решение: ограничить p значением totalPages или перенаправить.
  • Забытый вызов htmlspecialchars при выводе данных – XSS-уязвимость.

Как реализовать пагинацию с использованием курсора для больших таблиц?

Для таблиц с миллионами записей страдает производительность из-за OFFSET. Альтернатива – пагинация на основе курсора (keyset pagination). Вместо номера страницы передаётся значение последнего ID предыдущей страницы.


<?php
$perPage = 10;
$lastId = isset($_GET['last']) ? (int)$_GET['last'] : 0;

$stmt = $pdo->prepare("SELECT * FROM articles WHERE id > :last ORDER BY id ASC LIMIT :limit");
$stmt->bindValue(':last', $lastId, PDO::PARAM_INT);
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

if (count($rows) > 0) {
    $lastId = end($rows)['id'];
    echo '<a href="?last=' . $lastId . '">Следующая страница</a>';
}
?>

Цель: высокая скорость при больших наборах данных. Случай использования: ленты новостей, логи, где важна производительность.

Проблемы:

  • Невозможность перейти на произвольную страницу (например, 5). Решение: комбинировать с нумерацией, если требуется.
  • Зависимость от уникального упорядоченного столбца (обычно id).

Как отобразить пагинацию с диапазоном и многоточием (например, 1 ... 5 6 7 ... 20)?

Когда число страниц велико, удобно показывать только их часть. Реализуется расчётом начала и конца блока с учётом текущей страницы.


<?php
$currentPage = isset($_GET['p']) ? max(1, (int)$_GET['p']) : 1;
$totalPages = 20;
$range = 2; // количество ссылок слева и справа от активной

$start = max(1, $currentPage - $range);
$end = min($totalPages, $currentPage + $range);

if ($start > 1) {
    echo '<a href="?p=1">1</a> ... ';
    $start = $start + 0; // вывод многоточия отдельно
}
for ($i = $start; $i <= $end; $i++) {
    $active = ($i == $currentPage) ? ' class="active"' : '';
    echo '<a href="?p='.$i.'"'.$active.'>'.$i.'</a> ';
}
if ($end < $totalPages) {
    echo '... <a href="?p='.$totalPages.'">'.$totalPages.'</a>';
}
?>

Ошибки:

  • Неправильный расчёт границ при близости к началу или концу. Решение: использовать max и min.
  • Дублирование многоточия при малом totalPages. Условиями проверить > 1.

Как реализовать динамическую пагинацию через AJAX (без перезагрузки страницы)?

Используется JavaScript для отправки запроса на сервер и вставки полученного HTML. Сервер возвращает только фрагмент данных и ссылки пагинации.


// PHP (ajax_pagination.php)
$perPage = 10;
$currentPage = isset($_GET['p']) ? (int)$_GET['p'] : 1;
if ($currentPage < 1) $currentPage = 1;
$offset = ($currentPage - 1) * $perPage;

$pdo = new PDO(/* ... */);
$total = $pdo->query("SELECT COUNT(*) FROM articles")->fetchColumn();
$totalPages = ceil($total / $perPage);
$stmt = $pdo->prepare("SELECT * FROM articles LIMIT :limit OFFSET :offset");
/* ... выполнение и fetchAll */

// Возвращаем JSON или HTML
$html = '';
foreach ($rows as $row) {
    $html .= '<div>' . htmlspecialchars($row['title']) . '</div>';
}
$html .= '<div class="pagination">';
for ($i = 1; $i <= $totalPages; $i++) {
    $active = ($i == $currentPage) ? ' class="active"' : '';
    $html .= '<a href="#" data-page="'.$i.'"'.$active.'>'.$i.'</a> ';
}
$html .= '</div>';
echo $html;

// JavaScript (jQuery)
$('.pagination a').on('click', function(e) {
    e.preventDefault();
    var page = $(this).data('page');
    $.get('ajax_pagination.php', {p: page}, function(response) {
        $('#content').html(response);
    });
});

Цель: улучшение пользовательского опыта. Случай использования: интернет-магазины, каталоги.

Типичные проблемы:

  • Кэширование AJAX-запросов – нужно отключать или добавлять случайный параметр.
  • Некорректная обработка истории браузера (кнопка «Назад»). Решение: использовать History API.
  • Ошибки при большом количестве одновременных запросов.

Как сохранить дополнительные GET-параметры (фильтры, сортировку) при пагинации?

Часто пагинация должна работать вместе с другими параметрами. В ссылки нужно включать их через http_build_query.


<?php
$params = $_GET;
unset($params['p']); // удаляем текущую страницу

for ($i = 1; $i <= $totalPages; $i++) {
    $params['p'] = $i;
    $url = '?' . http_build_query($params);
    $active = ($i == $currentPage) ? ' class="active"' : '';
    echo '<a href="' . $url . '"' . $active . '>' . $i . '</a> ';
}
?>

Ошибки:

  • Передача чувствительных данных в URL (например, паролей). Этого следует избегать.
  • Дублирование параметров, если не удалить p перед построением.

Расширенные примеры пагинации

Полноценный класс Pagination с поддержкой всех параметров

Пример

<?php
class Pagination {
    private $total;
    private $perPage;
    private $currentPage;
    private $urlPattern;

    public function __construct($total, $perPage = 10, $currentPage = 1, $urlPattern = '?p=(:num)') {
        $this->total = $total;
        $this->perPage = $perPage;
        $this->currentPage = max(1, $currentPage);
        $this->urlPattern = $urlPattern;
    }

    public function getTotalPages() {
        return ceil($this->total / $this->perPage);
    }

    public function getOffset() {
        return ($this->currentPage - 1) * $this->perPage;
    }

    public function getLimit() {
        return $this->perPage;
    }

    public function buildUrl($page) {
        return str_replace('(:num)', $page, $this->urlPattern);
    }

    public function render($range = 2) {
        $totalPages = $this->getTotalPages();
        if ($totalPages <= 1) return '';

        $html = '<div class="pagination">';
        $html .= $this->renderArrow($this->currentPage - 1, '«');

        $start = max(1, $this->currentPage - $range);
        $end = min($totalPages, $this->currentPage + $range);

        if ($start > 1) {
            $html .= '<a href="'.$this->buildUrl(1).'">1</a>';
            if ($start > 2) $html .= '<span class="dots">…</span>';
        }
        for ($i = $start; $i <= $end; $i++) {
            $active = ($i == $this->currentPage) ? ' class="active"' : '';
            $html .= '<a href="'.$this->buildUrl($i).'"'.$active.'>'.$i.'</a>';
        }
        if ($end < $totalPages) {
            if ($end < $totalPages - 1) $html .= '<span class="dots">…</span>';
            $html .= '<a href="'.$this->buildUrl($totalPages).'">'.$totalPages.'</a>';
        }

        $html .= $this->renderArrow($this->currentPage + 1, '»');
        $html .= '</div>';
        return $html;
    }

    private function renderArrow($page, $label) {
        $totalPages = $this->getTotalPages();
        if ($page < 1 || $page > $totalPages) {
            return '<span class="disabled">'.$label.'</span>';
        }
        return '<a href="'.$this->buildUrl($page).'">'.$label.'</a>';
    }
}
?>
Пример использования класса:
<?php
$pdo = new PDO(/* ... */);
$total = $pdo->query("SELECT COUNT(*) FROM articles")->fetchColumn();
$currentPage = isset($_GET['p']) ? (int)$_GET['p'] : 1;

$pagination = new Pagination($total, 10, $currentPage, '?p=(:num)&sort=date');
$offset = $pagination->getOffset();
$limit = $pagination->getLimit();

$stmt = $pdo->prepare("SELECT * FROM articles ORDER BY created_at DESC LIMIT :limit OFFSET :offset");
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

// Вывод данных...
foreach ($rows as $row) { /* ... */ }

// Вывод пагинации
echo $pagination->render(3);
?>

Результат (пример HTML):

<div class="pagination">
    <a href="?p=1&sort=date">«</a>
    <span class="dots">…</span>
    <a href="?p=4&sort=date">4</a>
    <a href="?p=5&sort=date" class="active">5</a>
    <a href="?p=6&sort=date">6</a>
    <span class="dots">…</span>
    <a href="?p=20&sort=date">»</a>
</div>

Пагинация с сохранением порядка сортировки (несколько сортирующих параметров)

Пример

<?php
$allowedSorts = ['title', 'created_at', 'views'];
$sort = isset($_GET['sort']) && in_array($_GET['sort'], $allowedSorts) ? $_GET['sort'] : 'created_at';
$order = isset($_GET['order']) && $_GET['order'] === 'asc' ? 'ASC' : 'DESC';

$queryParams = $_GET;
unset($queryParams['p']);

for ($i = 1; $i <= $totalPages; $i++) {
    $queryParams['p'] = $i;
    $url = '?' . http_build_query($queryParams);
    // ... вывод ссылок
}
?>

Результат: ссылки вида ?sort=created_at&order=DESC&p=2.

Пагинация в PHP через p=2 - comments

En
Index php p 2 (php)