Создание постраничной навигации на языке PHP
Реализация постраничной навигации в PHP
Основной и наиболее эффективный способ построения постраничной навигации в PHP предполагает использование PDO с подготовленными запросами, вычисление общего количества записей и генерацию ссылок с номерами страниц. Такой подход обеспечивает безопасность от SQL-инъекций, гибкость в настройке и хорошую производительность.
<?php
$db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$perPage = 10;
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$page = max(1, $page);
$offset = ($page - 1) * $perPage;
// Общее количество записей
$countQ = $db->query("SELECT COUNT(*) FROM articles");
$total = $countQ->fetchColumn();
$totalPages = ceil($total / $perPage);
// Получение записей текущей страницы
$stmt = $db->prepare("SELECT * FROM articles LIMIT :limit OFFSET :offset");
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll();
?>Php page 2 (вторая страница php)
Далее формируется разметка для навигации. Пример с простым списком номеров страниц:
<?php if($totalPages > 1): ?>
<div class="pagination">
<?php for($i=1; $i<=$totalPages; $i++): ?>
<a href="?page=<?=$i?>" <?= $i==$page ? 'class="active"' : '' ?>><?=$i?></a>
<?php endfor; ?>
</div>
<?php endif; ?>Index php paging (постраничная навигация в php)
Пояснение шагов: параметр `page` извлекается из URL, приводится к целому числу и ограничивается от 1. Вычисляется количество страниц, чтобы не выйти за пределы. Подготовленный запрос с плейсхолдерами предотвращает SQL-инъекции. Генерация ссылок происходит в цикле, активная страница помечается классом.
Ключевые преимущества: безопасность, универсальность, возможность легко добавить фильтры или сортировку.
Как вывести только первую страницу данных без возможности переключения?
Этот вариант подходит для случаев, когда требуется отобразить только свежие записи, не предоставляя навигацию (например, лента новостей на главной). Он реализуется простым запросом с LIMIT и без вычисления общего количества.
<?php
$perPage = 10;
$stmt = $db->prepare("SELECT * FROM articles ORDER BY created_at DESC LIMIT :limit");
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll();
?>
Цель: быстрое получение небольшого набора данных без нагрузки на подсчёт общего числа строк. Случаи использования: блоки «Последние новости», «Популярные статьи» на главной странице, API с пагинацией «показать ещё».
Типичные ошибки: отсутствие сортировки (ORDER BY) приводит к непредсказуемому порядку записей. Если не указан ключ, разные страницы могут содержать одни и те же данные. Решение: всегда явно задавать сортировку. Проблема также в том, что пользователь не может перейти на другие страницы, что требует дополнительного механизма (например, AJAX-загрузки).
Как отобразить список номеров страниц с возможностью перехода?
Классическая постраничная навигация с нумерованными ссылками. Реализация полностью совпадает с основным решением из rbase, но с добавлением логики для компактного отображения при большом количестве страниц (например, вывод только нескольких соседних номеров).
<?php
$range = 5;
$start = max(1, $page - $range);
$end = min($totalPages, $page + $range);
?>
<div class="pagination">
<?php if($page > 1): ?>
<a href="?page=1">Первая</a>
<a href="?page=<?=$page-1?>">Назад</a>
<?php endif; ?>
<?php for($i=$start; $i<=$end; $i++): ?>
<a href="?page=<?=$i?>" <?= $i==$page ? 'class="active"' : '' ?>><?=$i?></a>
<?php endfor; ?>
<?php if($page < $totalPages): ?>
<a href="?page=<?=$page+1?>">Вперед</a>
<a href="?page=<?=$totalPages?>">Последняя</a>
<?php endif; ?>
</div>
Цель: быстрая навигация по страницам, особенно для больших списков. Случаи: каталог товаров, список пользователей, архив статей.
Ошибки: при большом $totalPages (например, 1000) отображение всех номеров приводит к огромной панели. Решение - ограничить количество показываемых ссылок с помощью диапазона (см. пример). Также часто забывают проверять, что $page не превышает $totalPages, что вызывает пустую страницу. Коррекция: $page = min($page, $totalPages) после вычисления total.
Как реализовать навигацию только с двумя кнопками (предыдущая/следующая)?
Минималистичный вариант для линейного перебора записей. Не требует отображения номеров, подходит для лент с бесконечной прокруткой или для простых списков.
<div class="pagination-simple">
<?php if($page > 1): ?>
<a href="?page=<?=$page-1?>">« Предыдущая</a>
<?php endif; ?>
<?php if($page < $totalPages): ?>
<a href="?page=<?=$page+1?>">Следующая »</a>
<?php endif; ?>
</div>
Цель: простота реализации и пользовательского интерфейса. Случаи: блоги, фотоальбомы, где важна последовательность, а не быстрый переход к середине.
Проблемы: отсутствие прямого перехода к определённой странице может раздражать пользователей, если страниц много. Решение - дополнить нумерацией или выпадающим списком. Также стоит показывать номер текущей страницы.
Как загружать следующую страницу без перезагрузки страницы (AJAX)?
Современный подход для динамических интерфейсов. PHP-скрипт отдаёт только данные (JSON или HTML-фрагмент), а клиентский JavaScript подгружает их и вставляет в DOM.
// PHP-скрипт (ajax.php)
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
// ... получение данных как в rbase ...
// Возвращаем JSON с записями и информацией о пагинации
echo json_encode([
'items' => $rows,
'currentPage' => $page,
'totalPages' => $totalPages
]);
// JavaScript (fetch)
fetch('ajax.php?page=2')
.then(response => response.json())
.then(data => {
// обновление списка и кнопок навигации
});
Цель: плавное перелистывание без полной перезагрузки, улучшение пользовательского опыта. Случаи: интернет-магазины, ленты социальных сетей, панели управления.
Ошибки: некорректная обработка ошибок fetch (сетевые сбои). Проблемы с SEO - контент, загружаемый через AJAX, может не индексироваться. Решение - использовать прогрессивное улучшение: ссылки на страницы работают без JavaScript, а JS добавляет поверх. Также нужно правильно управлять историей браузера (history.pushState).
Как организовать пагинацию с помощью готового компонента (например, Zend\Paginator)?
Готовые библиотеки предоставляют абстракцию для подсчёта записей, работы с различными адаптерами (массивы, БД) и генерации разметки. Это уменьшает количество шаблонного кода.
use Zend\Paginator\Paginator;
use Zend\Paginator\Adapter\ArrayAdapter;
$data = [/* большой массив */];
$paginator = new Paginator(new ArrayAdapter($data));
$paginator->setCurrentPageNumber($page);
$paginator->setItemCountPerPage($perPage);
foreach ($paginator as $item) {
// вывод элемента
}
// генерация ссылок через вызов методов Paginator
Цель: ускорение разработки, стандартизация пагинации в проекте. Случаи: крупные фреймворки (Laravel, Symfony имеют свои встроенные решения), проекты, требующие множества различных пагинаторов.
Недостатки: избыточность для простых списков. Зависимость от внешней библиотеки. При неправильной настройке может упасть производительность, если адаптер загружает все данные в память. Решение - использовать адаптер, работающий напрямую с SQL (например, DbSelect для Zend).
Расширенные примеры постраничной навигации с нетривиальными подходами.
// Пример 1: пагинация с фильтрацией по категории и поиском
$categoryId = isset($_GET['cat']) ? (int)$_GET['cat'] : 0;
$searchTerm = isset($_GET['q']) ? trim($_GET['q']) : '';
$where = '1=1';
$params = [];
if ($categoryId) {
$where .= ' AND category_id = :cat';
$params[':cat'] = $categoryId;
}
if ($searchTerm) {
$where .= ' AND title LIKE :q';
$params[':q'] = '%' . $searchTerm . '%';
}
$countQ = $db->prepare("SELECT COUNT(*) FROM articles WHERE $where");
$countQ->execute($params);
$total = $countQ->fetchColumn();
$totalPages = ceil($total / $perPage);
$dataQ = $db->prepare("SELECT * FROM articles WHERE $where LIMIT :limit OFFSET :offset");
$dataQ->bindValue(':limit', $perPage, PDO::PARAM_INT);
$dataQ->bindValue(':offset', $offset, PDO::PARAM_INT);
$dataQ->execute($params);
$rows = $dataQ->fetchAll();
Результат: страница /articles?cat=2&q=php&page=1 вернёт первые 10 статей категории 2, содержащих слово "php", с корректной пагинацией, учитывающей фильтры.
// Пример 2: Keyset pagination (пагинация на основе курсора) для больших таблиц
// Вместо OFFSET используем WHERE id > :last_seen
$lastId = isset($_GET['last_id']) ? (int)$_GET['last_id'] : 0;
$stmt = $db->prepare("SELECT * FROM articles WHERE id > :last_id ORDER BY id ASC LIMIT :limit");
$stmt->bindValue(':last_id', $lastId, PDO::PARAM_INT);
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll();
// Для генерации ссылки на следующую страницу берём id последней записи
if (count($rows) == $perPage) {
$nextLastId = end($rows)['id'];
$nextUrl = "?last_id=$nextLastId";
}
Результат: пагинация без замедления на больших OFFSET (например, страница 1000). Однако нет прямого перехода к произвольной странице, только последовательный перебор.
// Пример 3: пагинация на основе даты (для ленты новостей)
$lastDate = isset($_GET['before']) ? $_GET['before'] : date('Y-m-d H:i:s');
$stmt = $db->prepare("SELECT * FROM articles WHERE created_at < :last_date ORDER BY created_at DESC LIMIT :limit");
$stmt->bindValue(':last_date', $lastDate);
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll();
if (count($rows) == $perPage) {
$nextDate = end($rows)['created_at'];
$nextUrl = "?before=" . urlencode($nextDate);
}
Результат: удобен для бесконечной прокрутки в новостных сайтах. Не поддерживает перелистывание назад без дополнительной логики.