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

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

Одной из задач пагинации является одновременный вывод не всех элементов, а их ограниченного числа, скажем, не более $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 для так называемой банковой навигации:

$first = $pn - 1 - ($pn - 2) % $range;
$last = $first + $range < $pc ? $first + $range : $pc;

В $range нужно поместить значение на 1 меньше требуемой ширины диапазона, например для вывода не более чем семи регулярных ссылок на страницы в $range нужно поместить значение 6. Если объединить все выше написанное, получится так:

$pp = 10;

if ($result = mysqli_query($link, 'SELECT FLOOR((COUNT(*)+'.($pp-1).')/'.$pp.') FROM `table`'))
{
  list($pc) = mysqli_fetch_row($result);
  mysqli_free_result($result);

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

  if ($pn == 0 || $pn > $pc) error(404);
  elseif ($result = mysqli_query($link, 'SELECT * FROM `table` LIMIT '.(($pn-1)*$pp).','.$pp))
  {
    $range = 6;
    $first = $pn - 1 - ($pn - 2) % $range;
    $last = $first + $range < $pc ? $first + $range : $pc;
  }
  else error(503);
}
else error(503);

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

<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>

В представленной реализации первая регулярная ссылка (если это не ссылка на первую страницу) позволяет переключаться на предыдущий банк страниц. Если требуется, чтобы последняя регулярная ссылка позволяла так же переключаться на следующий банк страниц (то есть работала аналогично ссылке «Еще» и могла ее заменить), можно в первой формуле в качестве делителя вместо переменной $range указать выражение ($range - 1).

Функция 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.

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

  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 для так называемой плавающей навигации (можно использовать и функции min/max):

    $last = $pn + ($range >> 1) < $pc ? $pn + ($range >> 1) : $pc;
    $first = $last - $range > 1 ? $last - $range : 1;
    $last = $first + $range < $pc ? $first + $range : $pc;
    

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

    При использовании данных формул нет необходимости делать отдельную ссылку для перехода на страницу с номером, находящимся за пределами выводимого диапазона, т.к. каждый раз при переходе на следующую или предыдущую страницу диапазон будет автоматически смещаться в нужном направлении, но все-таки будет лучше как-то обозначить, что после выводимого диапазона присутствуют и другие страницы, например при помощи «пассивного многоточия».

  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. S59

    В конце кода вы вызываете функцию, но что это за функция, не объяснено.

  6. Михаил

    Если вы спрашиваете про notavail (там есть еще похожая функция notfound), то это функция обработки ошибки. Я изменил код в статье, указав более современный аналог упомянутых функций, из записи которого можно понять основное назначение функции обработки ошибки: подготовка к выводу страницы ошибки с указанным в параметре HTTP-статусом. Именно подготовка, потому что оригинальный код функции ничего кроме статуса не выводит. В G-Drive при обработке ошибок не используются исключения или прерывания. Вместо этого внутри функции обработки ошибки изменяется текущее состояния служебных переменных, что приводит к выводу страницы ошибки вместо запрошенной страницы на последнем этапе работы движка.

    Если вы не используете G-Drive, можете оставить свой способ обработки ошибок или в крайнем случае использовать вместо функции error функцию die (exit), только учтите, что параметр данной функции не имеет никакого отношения к HTTP-статусу.

  7. Михаил

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

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

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

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

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

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

  9. Михаил

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

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

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

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