Существует множество вариаций расположения и отображения кнопок, лично я пришел к следующему решению, которое по моему мнению наиболее наглядно и удобно. Подходит как для 5 страниц так и для 5000.
Пример HTML кода
Не буду ходить вокруг да около, сразу приложу пример сформированного скриптом html кода:
<div class="navigation">
<a href="/playlist/1.html?page=6">Назад</a>
<a href="/playlist/1.html">1</a>
<i>...</i>
<a href="/playlist/1.html?page=4">4</a>
<a href="/playlist/1.html?page=5">5</a>
<a href="/playlist/1.html?page=6">6</a>
<span class="link_active">7</span>
<a href="/playlist/1.html?page=8">8</a>
<a href="/playlist/1.html?page=9">9</a>
<a href="/playlist/1.html?page=10">10</a>
<i>...</i>
<a href="/playlist/1.html?page=17">17</a>
<a href="/playlist/1.html?page=8">Вперед</a>
</div>
Логика построения
По настройкам параметров я остановлюсь чуть позже после приведения кода, сейчас опишу логику формирования самих номеров.
С кнопками «Назад» и «Вперед» думаю все понятно, к тому же их можно просто отключить, поэтому на них не буду заострять внимания.
Первый и последний номер страницы отображается всегда, своего рода кнопки «В начало» и «В конец».
Середина формируется уже по простому алгоритму. Отображается просматриваемая страница и по N страниц по бокам. На примере отображается по N=3 страницы. В принципе все просто и понятно, но особая хитрость используется при приближении к краям. Опишу на примерах:
Страница 1-3 (где 3 = N)
1 2 3 4 5 6… 17Отображаются первые N*2 страниц и последняя.
Страница 4
1 2 3 4 5 6 7… 17Отображается первая и дальше сформированная строка от 4-3=1 до 4+3=7. Первая страница зарезервирована поэтому формируются номера от 2 до 7.
Страница 5
1 2 3 4 5 6 7 8… 17от 5-3=2 до 5+3=8.
Страница 6
1 2 3 4 5 6 7 8 9… 17Пожалуй во всех навигациях что я видел (включая хабр) строка была бы сформирована с пропуском, т.е. 1… 3 4 5 6 7 8 9… 17
Но ведь это не логично, отображать многоточие вместо одного числа. При построении второго многоточия выполняется аналогичная проверка.
Страница 7
1… 4 5 6 7 8 9 10… 17Середина уже стандартна.
Формирование окончания аналогично началу
Страница 12
1… 9 10 11 12 13 14 15 16 17
Страница 13
1… 10 11 12 13 14 15 16 17
Страница 14
1… 11 12 13 14 15 16 17
Страница 15-17
1… 12 13 14 15 16 17
Редиректы
Помимо этого из особенностей хочу выделить еще 2 момента, это проверка существования страницы и редирект на «правильный» адрес. Т.е. к примеру, тут же на хабре первая страница может быть доступна сразу по 2м адресам:
habrahabr.ru/sandbox/page1
habrahabr.ru/sandbox
Скрипт не дает зайти на адрес page/1/ и выполняет редирект на «чистый» адрес
Так же если указан слишком большой номер страницы будет выполнен редирект на последнюю существующую. К примеру были удалены материалы или изменено количество записей на страницу. Не могу правда однозначно сказать полезно ли это будет с точки зрения СЕО, но для пользователей мне кажется так будет удобнее.
PHP код и его использование
class PaginateNavigationBuilder
{
/**
* Чистый URL по умолчанию
* В адресе может быть указано место для размещения блока с номером страницы, тег {page}
* Пример:
* /some_url{page}.html
* В итоге адрес будет:
* /some_url.html
* /some_url/page_2.html
* Если тег {page} не указан, то страницы будут дописываться в конец адреса
*
* @var string
*/
private $baseUrl = '/';
/**
* Шаблон ссылки навигации
*
* @var string
*/
public $tpl = 'page/{page}/';
/**
* Обертка кнопок
*
* @var string
*/
public $wrap = "<div class=\"navigation\">{pages}</div>";
/**
* Сколько показывать кнопок страниц до и после актуальной
* Пример:
* $spread = 2
* Всего 9 страниц навигации и сейчас просматривают 5ю
* 1 ... 3 4 5 6 7 ... 9
*
* @var integer
*/
public $spread = 5;
/**
* Разрыв между номерами страниц
*
* @var string
*/
public $separator = "<i>...</i>";
/**
* Имя класса активной страницы
*
* @var string
*/
public $activeClass = 'link_active';
/**
* Номер просматриваемой страницы
*
* @var integer
*/
private $currentPage = 0;
/**
* Показывать кнопки "Вперед" и "Назад"
*
* @var bool
*/
public $nextPrev = true;
/**
* Текст кнопки "Назад"
*
* @var string
*/
public $prevTitle = 'Назад';
/**
* Текст кнопки "Вперед"
*
* @var string
*/
public $nextTitle = 'Вперед';
/**
* Инициализация класса
*
* @param string $baseUrl URL в конец которого будет добавляться навигация
*/
public function __construct($baseUrl = '/')
{
$this->baseUrl = $baseUrl;
}
/**
* Строим навигации и формируем шаблон
*
* @param integer $limit количество записей на 1 страницу
* @param integer $count_all общее количество всех записей
* @param integer $currentPage номер просматриваемой страницы
* @return mixed Сформированный шаблон навигации готовый к выводу
*/
public function build($limit, $count_all, $currentPage = 1)
{
if( $limit < 1 OR $count_all <= $limit ) return;
$count_pages = ceil( $count_all / $limit );
if( $currentPage > $count_pages ) {
header( "HTTP/1.0 301 Moved Permanently" );
header( "Location: " . $this->getUrl( $count_pages ) );
die( "Redirect" );
}
if( $currentPage == 1 AND $_SERVER['REQUEST_URI'] != $this->getUrl( $currentPage ) )
{
header( "HTTP/1.0 301 Moved Permanently" );
header( "Location: " . $this->getUrl( $currentPage ) );
die( "Redirect" );
}
$this->currentPage = intval( $currentPage );
if( $this->currentPage < 1 ) $this->currentPage = 1;
$shift_start = max( $this->currentPage - $this->spread, 2 );
$shift_end = min( $this->currentPage + $this->spread, $count_pages-1 );
if( $shift_end < $this->spread*2 ) {
$shift_end = min( $this->spread*2, $count_pages-1 );
}
if( $shift_end == $count_pages - 1 AND $shift_start > 3 ) {
$shift_start = max( 3, min( $count_pages - $this->spread*2 + 1, $shift_start ) );
}
$list = $this->getItem( 1 );
if ($shift_start == 3) {
$list .= $this->getItem( 2 );
} elseif ( $shift_start > 3 ) {
$list .= $this->separator;
}
for( $i = $shift_start; $i <= $shift_end; $i++ ) {
$list .= $this->getItem( $i );
}
$last_page = $count_pages - 1;
if( $shift_end == $last_page-1 ){
$list .= $this->getItem( $last_page );
} elseif( $shift_end < $last_page ) {
$list .= $this->separator;
}
$list .= $this->getItem( $count_pages );
if( $this->nextPrev ) {
$list = $this->getItem(
$this->currentPage > 1 ? $this->currentPage - 1 : 1,
$this->prevTitle,
true )
. $list
. $this->getItem(
$this->currentPage < $count_pages ? $this->currentPage + 1 : $count_pages,
$this->nextTitle,
true
);
}
return str_replace( "{pages}", $list, $this->wrap );
}
/**
* Формирование адреса
* @param int $page_num номер страницы
* @return string сформированный адрес
*/
private function getUrl( $page_num = 0 )
{
$page = $page_num > 1 ? str_replace( '{page}', $page_num, $this->tpl ) : '';
if( stripos( $this->baseUrl, '{page}' ) !== false ){
return str_replace( '{page}', $page, $this->baseUrl );
} else {
return $this->baseUrl . $page;
}
}
/**
* Формирование кнопки/ссылки
* @param int $page_num номер страницы
* @param string $page_name если указано, будет выводиться текст вместо номера страницы
* @param bool $noclass
* @return - span блок с активной страницей или ссылку.
*/
private function getItem( $page_num, $page_name = '', $noclass = false )
{
$page_name = $page_name ?: $page_num;
$className = $noclass ? '' : $this->activeClass;
if( $this->currentPage == $page_num ) {
return "<span class=\"{$className}\">{$page_name}</span>";
} else {
return "<a href=\"{$this->getUrl($page_num)}\">{$page_name}</a>";
}
}
}
Для наглядности, приведу пример построения навигации песочницы:
habrahabr.ru/sandbox/page12
$navi = new PaginateNavigationBuilder( "/sandbox/" );
$navi->tpl = "page{page}/";
$navi->spread = 4;
$template = $navi->build( $limit, $count_all, $page_num );
Или же если номер страницы прописан внутри URL:
example.com/some_url/1.html — первая страница
example.com/some_url/1-page2.html — вторая страница
$navi = new PaginateNavigationBuilder( "/some_url/1{page}.html" );
$navi->tpl = "-page{page}";
$template = $navi->build( $limit, $count_all, $page_num );
где
$limit — количество записей на страницу
$count_all — общее количество записей
$page_num — номер страницы на которой находится пользователь
На этом, пожалуй, всё. Буду рад любой конструктивной критике.
PS. Огромное спасибо всем отписавшимся, особенно тем кто ругает (и правильно делает).
Обещаю со всем ознакомиться, принять во внимание и исправиться.
Комментарии (21)
cool_grass
16.10.2017 23:47Зачем писать куски кода, каких в интернете полное, и которые может написать любой школьник?
SerafimArts
16.10.2017 23:53А есть ли смысл писать на PHP 4 без PSR, когда на дворе PHP 7.2 и 2017ый год?
P.S. Ну и изобретать велосипеды, когда есть более качественные, удобные и надёжные решения: github.com/illuminate/paginationFinesse
17.10.2017 02:36Laravel хорош, но его компонент пагинации отдельно от фреймворка весьма громоздкий и не удобный.
Я советую Pagerfanta: простой пагинатор на PHP, который решает сразу 3 задачи: получение порции данных (например из БД) для отображения на странице, подсчёт количества страниц и рендер самого пагинатора.
zoryamba
17.10.2017 17:12ковырялся один раз давно в pagerfanta… очень удивило то, что он принимает весь массив записей для организации постранички о_О
или это только одна из его возможностей?Finesse
18.10.2017 02:14Не обязательно получать все записи сразу. Pagerfanta получает информацию для постраничного вывода через адаптер. У интерфейса адаптера 2 метода: «вернуть количество записей» и «вернуть N записей начиная с M-ной записи». БД-адаптеры берут из базы данных ровно те записи, которые нужны для текущей страницы. Есть ещё массив-адаптер, в который подаются все записи сразу, но это не самый оптимальный путь, лучше сделать адаптер для своего источника данных, если его нет среди готовых.
dmirogin
16.10.2017 23:56Статья, доказывающая почему не любят php-шников, №187.
vlreshet
17.10.2017 09:28+3С PHPшниками как с мотоциклистами. Летают по городу на заднем колесе всего несколько процентов, а презирают потом всех. Вот и тут так — есть много толковых PHP разработчиков которые пишут хороший и сложный код, а отношение ко всем строится на основе парочки индивидуумов, которые надумали выплюнуть свои поделки из желудей и спичек на всеобщее обозрение.
dmirogin
17.10.2017 10:18Да, это нормально. В обычном мире мы также действуем по отношению к другим меньшинствам, национальностям и т.д.
tehSLy
17.10.2017 11:38+1Вот я как раз и хочу поделиться своим решением данной задачи.
Но зачем? Чем чаще люди сталкиваются с задачей, тем чаще она находит решение в велосипедах. Пагинаторов на просторах интернетов тьма, с адекватным оформлением, тестами, документацией и прочими плюшками. Нет, надо в который раз выставить PHP сообщество макаками у клавиатур и запостить свой слепленный на коленках агрегат на всеобщее обозрение. Зачем?Sandev Автор
17.10.2017 13:17В некотором роде статью можно назвать «начинающим от начинающих».
На вопрос «зачем» — как минимум я получил отзывы и мнение более продвинутых товарищей, а так же получил направление в котором следует двигаться и развиваться.index0h
18.10.2017 01:00Лучше задавайте вопросы на toster.ru с уточнением, что нужно ревью вашего кода
crmMaster
17.10.2017 12:06+11. Изнасилован PSR и правила именования
2. Внезапный и бесполезный редирект
3. Смешение логики и представления
4. Отрезать руки за
if( $this->next_prev ) $list = $this->get_item( $this->current_page > 1 ? $this->current_page - 1 : 1, $this->prev_title, true ) . $list . $this->get_item( $this->current_page < $count_pages ? $this->current_page + 1 : $count_pages, $this->next_title, true );
index0h
17.10.2017 13:00Sandev На этом, пожалуй, всё. Буду рад любой конструктивной критике.
Рекомендую ознакомиться: https://toster.ru/q/276441#answer_723827, там я описывал, на что стоит обращать внимание при проверке кода
index0h
17.10.2017 14:10Что плохо в вашем коде, при самом не пристальном взгляде
- PSR не поддерживаете
- Про SOLID (SRP особенно) не слышали: ваш навигатор — это роутер + шаблонизатор + генератор урлов + контроллер
- Используете супеглобальные переменные, про это статей написано много
- Параметры расчета захардкожены. Например, нет возможности показать по 4 страницы справа и слева от текущей, для этого нужно менять ваш код.
oxidmod
17.10.2017 14:22Параметры расчета захардкожены. Например, нет возможности показать по 4 страницы справа и слева от текущей, для этого нужно менять ваш код.
Есть такая возможность, но интерфейс класса об этом не говорит))
/** * @var int сколько показывать кнопок страниц до и после актуальной * Пример: * $spread = 2 * Всего 9 страниц навигации и сейчас просматривают 5ю * 1 ... 3 4 5 6 7 ... 9 */ var $spread = 5;
ellrion
17.10.2017 16:20Если воспринимать ваш класс как учебный и как выше вы написали "начинающим от начинающих", то вот вам хелп от меня.
https://gist.github.com/anonymous/782f852d8528c53c2f3958ed8278b519
Взял ваш класс. Прогнал автоформатером кода в IDE. Сменил объявление полей класса сvar
(php4 стиль) на нормальныйpublic
. Сменил через рефакторинг стиль именования методов, параметров метадов и полей со snake_case на camelCase. Поправил докблоки. Убрал "мусорные" символы перевода строки и табуляции в шаблонах (они не чем особо не помогают, а вот сам код засоряют). Ну и главное, определил что данный класс только строит нам код пагинации (например он у вас не определяет текущую страницу) и тогда я дал ему более корректное имя. И убрал самое плохое что было в этом классе — выставление заголовков для редиректа и die. И в случаях некорректных входных данных сделал выброс исключений. Ну и там что то еще наверное делал)
Еще нужно разнести длинный метод билда на подметоды. Убрать паблик параметры, добавив где нужно сеттеры и геттеры.
netmels
Смысл добавлять в ссылку page1 и делать редирект, если можно его просто не добавлять?
Sandev Автор
А ссылка page1 и не добавляется. Это даже в html примере видно.
Редирект нужен, если ранее использовалась навигация в которой формировался url с page1.