Как сделать постраничную навигацию грамотно

Как сделать постраничную навигацию грамотно

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

Как я и обещал продолжение статьи Построение простой постраничной навигации.

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

Количество выводимых ссылок за раз нам нужно ограничить по простой причине: если в БД имеется огромное количество элементов для построения постраничной навигации, то представьте какого размера будет эта самая навигация на странице - она разобьёт вам весь дизайн нахрен.

Ограничение числа количества ссылок (в нашем случае 5) - вынуждает нас дополнительно создать такие элементы (ссылки) как "вперед", "назад", "в начало", "в конец". На рисунке ниже они показаны стрелочками в соответствующие стороны, к тому же в определённые моменты их нужно будет делать не активными, я надеюсь вы понимаете о чём я, если нет смотрим рисунок:

Общий вид постраничной навигации

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

Когда пользователь нажимает на циферки - он перемещается по страницам. Допустим, он нажимает на ссылку с номером 5 и попадает на страницу - последнюю для состояния - 1, и что бы переключиться на следующие страницы ему нужно будет нажать на ссылку "вперед", обозначенную на рисунке в виде стрелочки вправо и тогда отсчёт уже пойдёт со страницы 6 (Состояние - 2). Одним словом ссылки "вперед и назад" при пограничных страницах ( в нашем случае это страницы 1,5,6,10,11,15 или 16 ) - это переходы между состояниями. Подобное решение используется в Joomla по крайней мере в админке ( но в детали джумловского алгоритма я не вникал ). Таким образом мы можем предоставить ссылки на все наши многочисленные страницы, выводя лишь ограниченное число ссылок, не нарушающее дизайн сайта.

Это одно из условий, которое отметает напрочь тривиальное построение постраничной навигации при помощи цикла, как в предыдущей статье. Всю "малину" портят возможности последнего из состояний: оно может содержать различное количество страниц - это так же зависит от количества выводимых элементов.

После неудачных попыток написания "простых" - километровых скриптов постраничной навигации я понял... нужно придумать что то принципиально новое, что бы было простым и надёжным... и придумал! Да - да сам придумал! Теперь вот делюсь с вами :)

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

  • $limit - Количество элементов на странице - 7
  • $all - Общее количество элементов - 110
  • $linkLimit - Количество ссылок в состоянии - 5
  • $start - Текущее смещение ( для первой страницы будет отсутствовать поэтому эту ситуацию нужно учесть)

По ходу объяснения появятся ещё переменные и не только... :)

Что можно с этим сделать? - Всё что нужноДля начала получаем количество страниц ( обзовём это значение $pageCount ), которое нам понадобиться для отображения 110 элементов.

Для этого делим 110 на 7 ... ага на цело не делиться :)

Подумаем... 110/7 = 15 и 5 в остатке. - здесь нам нужно округлять результат в большую сторону - получим 16 страничек. Почему в большую? - Объясняю: у нас получается 15 страничек по 7 элементов, и остаётся ещё 5 элементов ( те что в остатке) их тоже нужно показать, они поместятся (естественно) на 1 странице, а 15 + 1 = 16 - всего :)

И это будет справедливо для всех чисел не кратных 7, и вообще для любых входных данных!

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

Оказывается очень просто! - Создаём ассоциативниый массив, длинной равной количеству страниц ( т.е.нашему значению $pageCount ) . В этом массиве каждый ключ - будет номером страницы, а значением ключа - число, расчитываемое по формуле: ( номер_страницы * кол-во_элементов_на_странице ) - оно будет нужным нам смещением (START) для БД. Его же и будет содержать GET-параметр передающийся от страницы к странице. Сделать это можно например так:

$pages = ceil( $all / $limit ); // кол-во страниц
      
// Заполняем массив: ключ - это номер страницы, значение - это смещение для БД.
// Нумерация здесь нужна с единицы, а смещение с шагом = кол-ву материалов на странице.
for( $i = 0; $i < $pages; $i++) {
    $pagesArr[$i+1] = $i * $limit;
}

Теперь главная "фишка" - вспоминаем, что в PHP есть функция: array_chunk() Теперь с помощью неё разбиваем получившийся массив $pagesArr на части (чанки) количеством равным $linkLimit, передав третим параметром true - что бы сохранить ключи.

// Теперь что бы на странице отображать нужное кол-во ссылок
// дробим массив на чанки:
$allPages = array_chunk($pagesArr, $linkLimit, true);

В нашем случае получим массив $allPages, содержащий в себе ещё 4 массива: три по 5 элементов, а последний будет содержать остаток - 1 элемент, т.к. 16 на 5 нацело не делиться. Эти 4 массива и есть наши 4 состояния!

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

Постраничная навигация: алгоритм

Теперь, как понять какое из состояний выводить на экран в виде списка ссылок?

Очень просто! Функцию поиска в двумерном массиве подсказать, или сами напишите?

// $pagesList - массив с чанками
// $needPage - Здесь это наш GET - параметр (START)
// Вернёт int - индекс нужного чанка:
function searchPage( array $pagesList, /*int*/ $needPage )
{
    foreach( $pagesList AS $chunk => $pages  ){
        if( in_array($needPage, $pages) ){
            return $chunk;
        }
    }
    return 0;
} 

Функция вернёт целочисленный индекс чанка, в котором присутствует переданное смещение (GET-параметр) т.е. номер того массива, в котором есть нужная нам страница.

// Получаем индекс чанка в котором находится нужное смещение.
// И далее только из него сформируем список ссылок:
$needChunk = searchPage( $allPages, $start );

Далее вывод нужного чанка лишь дело техники:

// Собсно выводим ссылки из нужного чанка
foreach( $allPages[$needChunk] AS $pageNum => $ofset )  {
  // Делаем текущую страницу не активной (т.е. не ссылкой):
  if( $ofset == $start  ) {
      $htmlOut .= '
  • '. $pageNum .'
  • '; continue; } $htmlOut .= '
  • '. $pageNum . '
  • '; }

    Вот полный листинг класса, обеспечивающего построение постраничной навигации:

    <?php
    /**
     * @author Artem aka Moskitos <arty-komarov@yandex.ru>
     */
    class SimPageNav
    {
        protected $id;
        protected $startChar;
        protected $prevChar;
        protected $nextChar;
        protected $endChar;
        
    	/**
    	 * Конструктор
    	 * @param string $id        - атрибут ID элемента <UL> - постраничной навигации
    	 * @param string $startChar - текст ссылки "В начало"
    	 * @param string $prevChar  - текст ссылки "Назад"
    	 * @param string $nextChar  - текст ссылки "Вперед"
    	 * @param string $endChar   - текст ссылки "В конец"
    	 */
    	public function __construct( /*string*/ $id     = 'pagination', 
    	                             /*string*/ $startChar = '&laquo;', 
    	                             /*string*/ $prevChar  = '&lsaquo;', 
    	                             /*string*/ $nextChar  = '&rsaquo;', 
    	                             /*string*/ $endChar   = '&raquo;'  )
    	{
    	  $this->id = $id;
    	  $this->startChar = $startChar;
    	  $this->prevChar  = $prevChar;
    	  $this->nextChar  = $nextChar;
    	  $this->endChar   = $endChar;
    	}	
    	
      /**
       * Получить HTML - код постраничной навигации
       * @param int $all        - Полное кол-во элементов (Материалов в категории) 
       * @param int $limit      - Кол-во элементов на странице
       * @param int $start      - Текущее смещение элементов
       * @param int $linkLimit  - Количество ссылок в состоянии
       * @param string $varName - Имя GET - переменной которая будет использоваться в постр. навигации.
       * @return string
       */
        public function getLinks( /*int*/ $all, /*int*/ $limit, /*int*/ $start, $linkLimit = 10, $varName = 'start' )
        {
          // Нихрена не делаем, если лимит больше или равен кол-ву всех элементов вообще,
          // И если лимит = 0. 0 - будет означать "не разбивать н астраницы".
          if ( $limit >= $all || $limit == 0 ) {
            return NULL;
          }		
            
          $pages     = 0;       // кол-во страниц в пагинации
          $needChunk = 0;       // индекс нужного в данный момент чанка
          $queryVars = array(); // ассоц. массив полученный из строки запроса
          $pagesArr  = array(); // пременная для промежуточного хранения массива навигации
          $htmlOut   = '';      // HTML - код постраничной навигации
          $link      = NULL;    // формируемая ссылка
          
          // В этом блоке мы просто строим ссылку - такую же, как та, по которой
          // пришли на данную страницу, но извлекаем из неё нашу GET-переменную: 
          parse_str($_SERVER['QUERY_STRING'], $queryVars ); //   &$queryVars
          
          // Убиваем нашу GET-переменную
          if( isset($queryVars[$varName]) ) {
            unset( $queryVars[$varName] );
          }
          
          // Формируем такую же ссылку, ведущую на эту же страницу:
          $link  = $_SERVER['PHP_SELF'].'?'.http_build_query( $queryVars );
    
          //-------------------------------------------------------- 
          
          $pages = ceil( $all / $limit ); // кол-во страниц
          
          // Заполняем массив: ключ - это номер страницы, значение - это смещение для БД.
          // Нумерация здесь нужна с единицы. А смещение с шагом = кол-ву материалов на странице.
          for( $i = 0; $i < $pages; $i++) {
              $pagesArr[$i+1] = $i * $limit;
          }
          
          // Теперь что бы на странице отображать нужное кол-во ссылок
          // дробим массив со значениями [№ страницы] => "смещение" на 
          // Части (чанки)
          $allPages = array_chunk($pagesArr, $linkLimit, true);
          
          // Получаем индекс чанка в котором находится нужное смещение.
          // И далее только из него сформируем список ссылок:
          $needChunk = $this->searchPage( $allPages, $start );
          
          // Формируем ссылки "В начало", "передыдущая" ------------------------------------------------
          
          if ( $start > 1 ) {
          	$htmlOut .= '<li><a href="'.$link.'&'.$varName.'=0">'.$this->startChar.'</a></li>'.
          				'<li><a href="'.$link.'&'.$varName.'='.($start - $limit).'">'.$this->prevChar.'</a></li>';	
          } else {
          	$htmlOut .= '<li><span>'.$this->startChar.'</span></li>'.
          				'<li><span>'.$this->prevChar.'</span></li>';	
          }
          // Собсно выводим ссылки из нужного чанка
          foreach( $allPages[$needChunk] AS $pageNum => $ofset )  {
            // Делаем текущую страницу не активной:
            if( $ofset == $start  ) {
                $htmlOut .= '<li><span>'. $pageNum .'</span></li>';            
                continue;
            }        
            $htmlOut .= '<li><a href="'.$link.'&'.$varName.'='. $ofset .'">'. $pageNum . '</a></li>';
          }
            
          // Формируем ссылки "следующая", "в конец" ------------------------------------------------
        	
          if ( ($all - $limit) >  $start) {
          	$htmlOut .= '<li><a href="' . $link . '&' . $varName . '=' . ( $start + $limit) . '">' . $this->nextChar . '</a></li>'.
          			    '<li><a href="' . $link . '&' . $varName . '=' . array_pop( array_pop($allPages) ) . '">' . $this->endChar . '</a></li>';			
          }	else {
          	$htmlOut .= '<li><span>' . $this->nextChar . '</span></li>'.
          			    '<li><span>' . $this->endChar . '</span></li>';         
          }      	
          return '<ul id="'.$this->id.'">' . $htmlOut . '<ul>';
        }
    
        /**
         * Ищет в каком чанке находится сраница со смещением $needPage
         * @param array $pagesList массив чанков (массивов страниц разбитый по лимиту ссылок на странице)
         * @param int $needPage - смещение
         * @return number Ключ чанка в котором есть нужная страница
         */
        protected function searchPage( array $pagesList, /*int*/$needPage )
        {
            foreach( $pagesList AS $chunk => $pages  ){
                if( in_array($needPage, $pages) ){
                    return $chunk;
                }
            }
            return 0;
        }    
    }
    

    При желании сам механизм построения (строки с 89-116) можно делегировать другому классу и написать на его основе целую кучу вариантов (стратегий) построения постраничной навигации.

    Подведём итоги: мы получили простой, надёжный механизм, инкапсулированный в одном классе, с одним доступным методом. Обеспечивающий настройку нужных нам параметров, встраиваемый в любую систему парой строчек:

    //Пользоваться очень просто:
    
    $start = isset($_GET['start']) ? intval($_GET['start']) : 0;
    
    $pageNav = new SimPageNav();
    
    echo $pageNav->getLinks( 110, 7, $start, 5, 'start' );
    
    

    UPD: По просьбе товарища Сани привожу пример использования:

    // В примере используется, но не изменяется.
    $count = isset($_GET['count']) ? $_GET['count'] : 5;
    // Смещение для БД
    $start = isset($_GET['start']) ? $_GET['start'] : 0;
    
    try {
    
    	// Получаем объект для работы с БД:
    	$db = new PDO(
    		'mysql:dbname=YOU_DBNAME', 
    		'USERNAME', 'USERPASS',
    		array(
    			PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    			PDO::ATTR_EMULATE_PREPARES => false
    		)
    	);
    	
    	// Получаем общее кол-во "страниц"
    	$all = $db->query(
    		'SELECT COUNT(*) FROM `posts`'
    	)->fetchColumn();	
    
    	// Подготавливаем запрос на получение
    	// данных теущей "страницы"
    	$stmt = $db->prepare(
    		'SELECT *
    		 FROM `posts`
    		 LIMIT  :limit
    		 OFFSET :offset'
    	);	
    
    	// Привязываем значения к плейсхолдерам запроса 
    	$stmt->bindValue(':limit', $count, PDO::PARAM_INT);
    	$stmt->bindValue(':offset', $start, PDO::PARAM_INT);
    
    	// Отправляем привязанные данные
    	$stmt->execute();
    	// Получаем результат запрса:
    	$pages = $stmt->fetchAll(PDO::FETCH_ASSOC);
    	
    	// Цикл в котором выводим данные: 
    	foreach ($pages AS $page) {
    		// ... ВЫВОД ДАННЫХ ...
    	}
    	
    	// Выводим блок ссылок с постраничной навигацией:
    	$pagenav = new SimPageNav();
    	echo $pagenav->getLinks($all, $count, $start, 10, 'start' );
    	
    } catch (PDOException $e) {
    	echo $e->getMessage();
    }
    

    Осталось только прописать красивые CSS - стили и привести ссылки к чпу - виду и вуаля...

    Надеюсь идею до вас я донёс, теперь можно спать спокойно - постраничную навигацию мы разобрали по полочкам - пользуйтесь на здоровье.

    Добавить комментарий


    Защитный код
    Обновить


    
    Кто на сайте
    Сейчас 38 гостей онлайн