Постраничный вывод и базы данных

В SQL-запросах, для ограничения количества строк в результате используется инструкция LIMIT, например следующий вернёт первые 10 записей:

SELECT * FROM `table` ORDER BY `id` LIMIT 0, 10
SQL

В следующем запросе будут записи с 11 по 20:

SELECT * FROM `table` ORDER BY `id` LIMIT 11, 10
SQL

Для построения пагинации требуется знать сколько всего записей вернет SQL-запрос без LIMIT, для этого в MySQL есть опция SQL_CALC_FOUND_ROWS. С ней запрос вернет обычный результат.

SELECT SQL_CALC_FOUND_ROWS * FROM `table` ORDER BY `id` LIMIT 0, 10
SQL

Но теперь, если следом выполнить запрос SELECT FOUND_ROWS(), то он вернёт общее количество записей в предыдущем запросе.

SELECT FOUND_ROWS()
SQL

На этом принципе был написан PHP-класс для постраничного вывода из базы данных.

class DBPaginator
{
	public  $page    = 1;   /* Текущая страница */
	public  $amt     = 0;   /* Кол-во страниц */
	public  $limit   = 10;  /* Кол-во элементов на странице */
	public  $total   = 0;   /* Общее кол-во элементов */
	public  $display = '';	/* HTML-код навигации */

	private $url     = '';       
	private $carrier = 'page';
 
	/**
	 * Конструктор.
	 */
	public function __construct($url, $limit = 0)
	{
		$this->url = $url;		
		
		if (!empty($limit)) {
			$this->limit = $limit;
		}		
 
		$page = intval(@$_GET['page']);
		if (!empty($page)) {
			$this->page = $page;
		}
 
		$query = parse_url($this->url, PHP_URL_QUERY);
		if (empty($query)) {
			$this->carrier = '?' . $this->carrier . '=';
		} else {
			$this->carrier = '&' . $this->carrier . '=';
		}
	}
 
	/**
	 * Формирование HTML-кода навигации в переменную display.
	 */
	public function getItems($sql)
	{
		// Подключение к БД
		$dbh = new PDO('mysql:dbname=db_name;host=localhost', 'логин', 'пароль');

		// Получение записей для текущей страницы
		$start = ($this->page != 1) ? $this->page * $this->limit - $this->limit : 0;
		if (strstr($sql, 'SQL_CALC_FOUND_ROWS') === false) {
			$sql = str_replace('SELECT ', 'SELECT SQL_CALC_FOUND_ROWS ', $sql) . ' LIMIT ' . $start . ', ' . $this->limit;
		} else {
			$sql = $sql . ' LIMIT ' . $start . ', ' . $this->limit;
		}		
		
		$sth = $dbh->prepare($sql);
		$sth->execute();		
		$array = $sth->fetchAll(PDO::FETCH_ASSOC);
		
		// Узнаем сколько всего записей в БД 
		$sth = $dbh->prepare("SELECT FOUND_ROWS()");
		$sth->execute();
		$this->total = $sth->fetch(PDO::FETCH_COLUMN);
		
		$this->amt = ceil($this->total / $this->limit);	
		if ($this->page > $this->amt) {
			$this->page = $this->amt;
		}
 
		if ($this->amt > 1) {
			$adj = 2;	
			$this->display = '<nav class="pagination-row"><ul class="pagination justify-content-center">';
 
			/* Назад */
			if ($this->page == 1) {
				$this->addSpan('«', 'prev disabled');
			} elseif ($this->page == 2) {
				$this->addLink('«', '', 'prev');
			} else {
				$this->addLink('«', $this->carrier . ($this->page - 1), 'prev');
			}
 
			if ($this->amt < 7 + ($adj * 2)) {
				for ($i = 1; $i <= $this->amt; $i++){
					$this->addLink($i, $this->carrier . $i);			
				}
			} elseif ($this->amt > 5 + ($adj * 2)) {
				$lpm = $this->amt - 1;
				if ($this->page < 1 + ($adj * 2)){
					for ($i = 1; $i < 4 + ($adj * 2); $i++){
						$this->addLink($i, $this->carrier . $i);
					}
					$this->addSpan('...', 'separator');	
					$this->addLink($lpm, $this->carrier . $lpm);
					$this->addLink($this->amt, $this->carrier . $this->amt);	
				} elseif ($this->amt - ($adj * 2) > $this->page && $this->page > ($adj * 2)) {
					$this->addLink(1);
					$this->addLink(2, $this->carrier . '2');
					$this->addSpan('...', 'separator');	
					for ($i = $this->page - $adj; $i <= $this->page + $adj; $i++) {
						$this->addLink($i, $this->carrier . $i);
					}
					$this->addSpan('...', 'separator');	
					$this->addLink($lpm, $this->carrier . $lpm);
					$this->addLink($this->amt, $this->carrier . $this->amt);	
				} else {
					$this->addLink(1, '');
					$this->addLink(2, $this->carrier . '2');
					$this->addSpan('...', 'separator');	
					for ($i = $this->amt - (2 + ($adj * 2)); $i <= $this->amt; $i++) {
						$this->addLink($i, $this->carrier . $i);
					}
				}
			}
 
			/* Далее */
			if ($this->page == $this->amt) {
				$this->addSpan('»', 'next disabled');				
			} else {			
				$this->addLink('»', $this->carrier . ($this->page + 1));
			}
 
			$this->display .= '</ul></nav>';
		}
 
		return $array;	
	}
 
	private function addSpan($text, $class = '')
	{
		$class = 'page-item ' . $class;
		$this->display .= '<li class="' . trim($class) . '"><span class="page-link">' . $text . '</span></li>';		
	}	
 
	private function addLink($text, $url = '', $class = '')
	{
		if ($text == 1) {
			$url = '';
		}
 
		$class = 'page-item ' . $class . ' ';
		if ($text == $this->page) {
			$class .= 'active';
		}
		$this->display .= '<li class="' . trim($class) . '"><a class="page-link" href="' . $this->url . $url . '">' . $text . '</a></li>';
	}	
}
PHP

Использование класса

В класс нужно передать URL страницы, количество записей на странице и SQL-запрос выборки из БД (например товары).

<?php
// Текущий URL и 6 шт. на странице
$peger = new DBPaginator('https://example.com', 6); 
$items = $peger->getItems("SELECT * FROM `prods`");

/* Инфо о текущей странице */
echo '<p>Страница ' . $peger->page . ' из ' . $peger->amt . '</p>';	
 
/* Вывод текста только на первой странице */
if ($peger->page == 1) {
	echo '<p>Описание на первой странице</p>';
}
?>
 
<div class="prod-list">
	<?php foreach ($items as $row): ?>
	<div class="prod-item">
		<div class="prod-item-img">
			<a href="#"><img src="/images/<?php echo $row['img']; ?>" alt=""></a>			
		</div>
		<div class="prod-item-name">
			<a href="#"><?php echo $row['name']; ?></a>			
		</div>
		<div class="prod-item-btn">
			<a href="#">Купить</a>
		</div>
	</div>
	<?php endforeach; ?>
</div>
 
<?php echo $peger->display; ?>
HTML

CSS-стили пагинатора:

.pagination-row {
	overflow: hidden;
	clear: both;
	margin: 0 0 20px 0;
	margin-bottom: 15px;
}
.pagination {
	padding: 0;
	margin: 0;
	text-align: center;
 }
.pagination .page-item {
	display: inline-block;
	margin: 0 2px 3px;
}
.pagination .page-link {
	display: inline-block;
	height: 28px;
	min-width: 28px;
	line-height: 28px;
	font-size: 15px;
	text-decoration: none;
	text-align: center;
	border: 1px solid #ddd;
	border-radius: 3px;    
	background: #fbfbfb;
	text-decoration: none;
	color: #000;
}
.pagination a.page-link:hover {
	background: #ffd57b;
}
.pagination .active a.page-link {
	background: #2bc2e0;
	border-color: #a5a5ca;
}
.pagination .separator .page-link {
	border-color: #fff;
	background: #fff;
}
.pagination .disabled .page-link {
	color: #999;
}
CSS

Стили для списка товаров:

.prod-list {
	overflow: hidden;
	margin: 0 -25px 20px 0;
}
.prod-item {
	width: 180px;
	height: 290px;
	float: left;
	border: 1px solid #ddd;
	padding: 20px;
	margin: 0 20px 20px 0;
	text-align: center;
	border-radius: 6px;
}
.prod-item-name {
	margin-bottom: 5px;
}
.prod-item-btn a {
	color: rgb(25, 25, 25);
	line-height: 24px;
	border-radius: 4px;
	background-color: #2bc2e0;
	display: inline-block;
	text-decoration: none;
	padding: 0 30px;
	border: none;
	box-shadow: 0px 2px 1px 0px #a5a5ca;
}
CSS

Пример в действии:

16.12.2020, обновлено 04.12.2021
17148
Предыдущая запись CSS фильтры

Комментарии 3

Сергей Антонов Сергей Антонов
20 февраля 2021 в 10:00
А как на счет SQL инъекций там веть сразу в коде execute()
Майкл Миллер Майкл Миллер
17 июня 2021 в 23:31
Как подружить эту пагинацию с фильтрацией?
Кирилл Зарубко Кирилл Зарубко
22 февраля 2023 в 17:00
Как сделать вывод по условию? При добавлении условия просто пустой экран

, чтобы добавить комментарий.

Другие публикации

PHP-класс обертка для PDO
Класс значительно упрощает работу с PDO, сокращает код. Реализован на статических классах и не требует создание экземпляра класса.
23034
+11
Как получить текущий URL в PHP?
Сформировать текущий адрес страницы можно с помощью элементов массива $_SERVER. Рассмотрим на примере URL...
182956
+14
Массив $_SERVER
Описание значений глобального массива $_SERVER с примерами.
52641
+3
Поиск похожих текстов в базе данных MySQL + PHP
Один из вариантов поиска похожих статей в базе данных основан на схождении слов в двух текстах.
7275
+6
Получить фото из Instagram без API
Так как Instagram и Fasebook ограничили доступ к API, а фото с открытого аккаунта всё же нужно периодически получать и...
24707
+7
Счетчик просмотров страниц с графиком
Для примера возьмем статейный сайт, на нём нужно сделать счетчик просмотров статей, с выводом результатов за день,...
19416
+23