Как сделать пагинацию?

Пагинация, включая навигацию по страницам, – достаточно простая вещь, но у начинающих часто возникают проблемы с ее созданием, а главное, пониманием ее работы...

Одной из задач пагинации является одновременный вывод не всех элементов, а их ограниченного числа, скажем, не более $pp элементов, причем, какая именно группа элементов выводится, зависит от входного параметра $pn – номера этой группы. Этот номер, собственно, и есть номер страницы. Тут нужен такой запрос:

SELECT * FROM `table` LIMIT {($pn - 1) * $pp},{$pp}

Значение $pn уменьшается на 1, чтобы, например, для первой страницы списка при значении $pp, равном 10, выбирались элементы с 0 по 9, а не с 10 по 19. Естественно, если страницы нумеруются с нуля, уменьшать на 1 значение $pn в запросе не нужно.

Для решения следующей задачи, а именно построения навигации по страницам, прежде всего необходимо узнать общее количество страниц списка. Для этого можно сначала запросить общее количество элементов. Или прямо в запросе вычислить общее количество страниц на основе общего количества элементов:

SELECT FLOOR((COUNT(*)+{$pp - 1})/{$pp}) FROM `table`

Для получения общего количества страниц $pc здесь использована достаточно известная формула pc = (count + per_page - 1) div per_page, но с поправкой на использование функции FLOOR вместо целочисленного деления (div). Можно использовать и оператор DIV, который поддерживается в MySQL достаточно давно.

Если на страницах нужно выводить только по одному элементу, представленные выше запросы можно упростить.

После того, как общее количество страниц $pc получено, можно сразу выводить ссылки на все страницы списка при помощи цикла со счетчиком в диапазоне от 1 до $pc, однако обычно используют более изысканную навигацию, при которой одновременно выводятся ссылки не на все страницы, а только на страницы с номерами в ограниченном зависящем от номера текущей страницы диапазоне. Например, вот формулы для получения ограничивающих диапазон значений $first и $last для так называемой плавающей навигации внутри специальной функции:

function pagination($pn, $pc, $range = 4)
{
  $last = min($pn + ($range >> 1), $pc);
  $first = max($last - $range, 1);
  $last = min($first + $range, $pc);
  return [$first, $last];
}

В $range нужно поместить значение на 1 меньше требуемой ширины диапазона, например для вывода не более чем пяти регулярных ссылок на страницы нужно поместить в $range значение 4. Значение должно быть четным (и ненулевым), чтобы количество регулярных ссылок (в общем случае) было нечетным и, таким образом, ссылка на текущую страницу выводилась точно посередине диапазона.

Если объединить выше написанное, получится так:

<?php

require INCLUDE_PATH.'query.php'; // подключение функции query

$pp = 10;

$result = query('SELECT FLOOR((COUNT(*)+'.($pp - 1).')/'.$pp.') FROM `table`');
list($pc) = $result->fetch_row();
$result->free();

// здесь можно вызывать ротатор номеров страниц 0 и 1

if ($pn == 0 || $pn > $pc)
{
  error(404);
  return;
}

$result = query('SELECT * FROM `table` LIMIT '.(($pn - 1) * $pp).','.$pp);

list($first, $last) = pagination($pn, $pc);

Остается только вывести строку навигации:

<nav>
  <ul class="pagination">
<?php for ($i = $first; $i <= $last; $i++): ?>
    <li<?= $i == $pn ? ' class="active"' : '' ?>><a href="<?= pagelink($i) ?>"><?= $i ?></a></li>
<?php endfor; if ($pc > $last): ?>
    <li><a href="<?= pagelink($i) ?>">Еще</a></li>
<?php endif; ?>
  </ul>
</nav>

Функция pagelink отвечает за формирование адресов страниц, в том числе той их части, в которой задается номер страницы. При использовании ротатора номеров страниц 0 и 1 данную функцию лучше разместить рядом с функцией ротатора, т.к. она должна компенсировать действие ротатора, позволяя получать в ссылке адрес / вместо /?p=1 для его соответствия нулевому значению входного параметра $pn:

function pagelink($p)
{
  return $p > 1 ? '/?p='.$p : '/';
}

function rotate01()
{
  global $pn;

  if ($pn == 0) $pn++;
  elseif ($pn == 1) $pn--;
}

Также можно увязать поведение pagelink с фактом вызова ротатора: $p > $flag.

Комментарии: 12

  1. Михаил

    Оптимизированный код ротатора номеров страниц 0 и 1:

    if ($pn < 2) $pn ^= 1;
    

    Без доступа к глобальному параметру:

    function rotate01($p)
    {
      return $p < 2 ? $p ^ 1 : $p;
    }
    
  2. Михаил

    Формулы для базы и лимита, позволяющие уменьшить количество элементов первой страницы списка на $xpp элементов:

    $base = ($pn - 1) * $pp - ($pn > 1) * $xpp;
    $limit = $pp - ($pn < 2) * $xpp;
    

    Естественно, при уменьшении количества элементов первой страницы на 1 элемент коэффициент $xpp можно не использовать.

  3. Михаил

    Формулы на основе тернарного оператора для получения значений $first и $last для плавающей навигации внутри специальной функции:

    function pagination($pn, $pc, $range = 4)
    {
      $last = $pn + ($range >> 1) < $pc ? $pn + ($range >> 1) : $pc;
      $first = $last - $range > 1 ? $last - $range : 1;
      $last = $first + $range < $pc ? $first + $range : $pc;
      return [$first, $last];
    }
    
  4. Михаил

    Пример разметки для плавающей навигации совместно с навигацией типа prev/next:

      <div class="nav-links">
    <?php if ($pn > 1): ?>
        <a class="prev page-numbers" href="<?= pagelink($pn-1) ?>">Пред.</a>
    <?php endif; for ($i = $first; $i <= $last; $i++): if ($i == $pn): ?>
        <span class="page-numbers current"><?= $i ?></span>
    <?php else: ?>
        <a class="page-numbers" href="<?= pagelink($i) ?>"><?= $i ?></a>
    <?php endif; endfor; if ($pn < $pc): ?>
        <a class="next page-numbers" href="<?= pagelink($pn+1) ?>">След.</a>
    <?php endif; ?>
      </div>
    

    Будет лучше как-то обозначить, что после выводимого диапазона присутствуют и другие страницы, например при помощи «пассивного многоточия». Данный блог имеет такую же разметку, расширенную кнопкой «Еще», которая не выводится по причине небольшого количества страниц в списках на этот момент. Кнопки prev/next присутствуют в разметке, но они могут быть скрыты стилистически при большой ширине страниц, а при малой наоборот может быть скрыта стилистически основная навигация.

  5. Михаил

    Если при значении $pc, равном нулю, не нужно (например отсутствуют комментарии какого-либо материала) или не желательно выводить страницу ошибки 404, перед проверкой условия на вывод соответствующей страницы следует скорректировать значение $pc:

    if ($pc == 0) $pc++;
    

    При получении в запросе общего количества элементов $count, а не сразу общего количества страниц можно делать коррекцию непосредственно при вычислении значения $pc:

    $pc = (int)(($count + $pp - ($count > 0)) / $pp);
    
  6. Katerina1993

    В общем я сделала навигацию с использованием $xpp. Только вот мне не удаётся правильно посчитать страницы в навигации. Выходит то больше, то меньше страниц.

    Как правильно посчитать?

  7. Михаил

    Вам нужно в запросе для получения общего количества страниц сделать так, как будто на первой странице присутствует столько же элементов, сколько и на других. Т.е. прибавьте к делимому в запросе значение $xpp:

    SELECT FLOOR((COUNT(*)+{$xpp+$pp-1})/{$pp}) FROM `table`
    
  8. Николай

    Как сделать пагинацию с работающей сортировкой (по возрастанию/убыванию по алфавиту, дате...)? Отдельно сортировка и пагинация работают, вместе только на первой странице, дальше сортировка сбрасывается (устанавливается по дефолту).

  9. Михаил

    Вам нужно указывать в адресах других страниц списка не только их номера, но и требуемый тип сортировки. Вы используете G-Drive?

    Добавлено. В общем в G-Drive можно завести в таблице категорий специальное поле sort и сохранить в нем фрагменты текста запроса, определяющие все возможные типы сортировки:

    id...sortmodule
    ...`name` ASClist
    sort-by-name-desc...`name` DESClist
    sort-by-date...`date` ASClist
    sort-by-date-desc...`date` DESClist

    В тексте запроса нужно сделать соответствующую вставку:

    ... ORDER BY {$page['sort']} ...
    

    А при формировании ссылок указать текущий идентификатор:

    <a href="<?= pagelink($i, $page['id']) ?>"><?= $i ?></a>
    

    Также вместо поля sort можно использовать ассоциативный массив, индексируемый значениями поля id.

Отправить комментарий

Ваш адрес E-mail не будет опубликован.