Pull to refresh

Постраничная навигация на PHP

Reading time 6 min
Views 34K
Часто при разработке и выводе контента появляется необходимость использования постраничной навигации. Кто-то скорее всего использует готовые решения от своего фреймворка. Кто-то, возможно, не заморачивается и лупит страницы просто циклом. У кого-то есть свои наработки в этом направлении. Вот я как раз и хочу поделиться своим решением данной задачи.

Существует множество вариаций расположения и отображения кнопок, лично я пришел к следующему решению, которое по моему мнению наиболее наглядно и удобно. Подходит как для 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 код и его использование


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. Огромное спасибо всем отписавшимся, особенно тем кто ругает (и правильно делает).
Обещаю со всем ознакомиться, принять во внимание и исправиться.
Tags:
Hubs:
-9
Comments 21
Comments Comments 21

Articles