Класс PageNavigation

Класс \Bitrix\Main\UI\PageNavigation рассчитывает границы выборки для постраничного вывода. Объект хранит номер текущей страницы, размер страницы и общее число записей, а по ним вычисляет смещение и ограничение для запроса.

Класс не обращается к базе данных и не выводит разметку. Он считает числа, которые разработчик передает в запрос и в компонент вывода. Поэтому один и тот же объект подходит для выборки через ORM, для массива в памяти и для внешнего источника данных.

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

Контракты методов

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

Метод

Параметры

Результат

Ограничения

__construct($id)

$id — строка, идентификатор навигации. Совпадает с именем параметра в адресе

Создает объект

Идентификатор должен быть уникальным на странице, иначе два списка прочитают из адреса один номер страницы

getId()

Нет

Возвращает строку — идентификатор навигации

Изменить идентификатор после создания объекта нельзя

initFromUri()

Нет

Ничего не возвращает, поэтому завершает цепочку вызовов. Задает номер текущей страницы и размер страницы по адресу запроса

Размер страницы применяется, только если он есть в списке setPageSizes(). Режим показа всех записей включается, только если разрешен через allowAllRecords(true)

setPageSize($n)

$n — число записей на странице, приводится к целому

Возвращает объект навигации

Значение не проверяется по списку setPageSizes()

getPageSize()

Нет

Возвращает целое число — заданный размер страницы

Значение по умолчанию — 20

setPageSizes($sizes)

$sizes — массив целых чисел

Возвращает объект навигации

Список управляет только выбором размера через адрес и через интерфейс. Текущий размер страницы метод не меняет

getPageSizes()

Нет

Возвращает массив разрешенных размеров страницы

По умолчанию массив пуст, поэтому размер из адреса не применяется

setRecordCount($n)

$n — общее число записей, приводится к целому

Возвращает объект навигации

Число записей объект самостоятельно не вычисляет

getRecordCount()

Нет

Возвращает целое число или null, если число записей не задано

Возвращает null до первого вызова setRecordCount()

getPageCount()

Нет

Возвращает целое число страниц. Неполная последняя страница считается отдельной страницей

Возвращает 0, если число записей не задано. В режиме показа всех записей возвращает 1

setCurrentPage($n)

$n — номер страницы, приводится к целому

Возвращает объект навигации

Значение не ограничивается числом страниц. Номер больше числа страниц дает смещение за пределами выборки

getCurrentPage()

Нет

Возвращает целое число — номер текущей страницы

Возвращает 1, если номер не задан

getOffset()

Нет

Возвращает целое число — смещение первой записи текущей страницы. Отсчет начинается с нуля

В режиме показа всех записей возвращает 0

getLimit()

Нет

Возвращает размер страницы

На последней неполной странице возвращает размер страницы, а не фактическое число записей. В режиме показа всех записей возвращает общее число записей, а если оно не задано — null

allowAllRecords($mode)

$mode — логическое значение, приводится к типу bool

Возвращает объект навигации

Само по себе значение true режим не включает: режим включает initFromUri(), когда находит в адресе page-all

allRecordsAllowed()

Нет

Возвращает true, если режим показа всех записей разрешен

Показывает разрешение, а не текущее состояние

allRecordsShown()

Нет

Возвращает true, если режим показа всех записей включен для текущего запроса

Возвращает false, пока initFromUri() не нашел в адресе page-all

addParams($uri, $sef, $page, $size)

$uri — объект \Bitrix\Main\Web\Uri. $sef — логическое значение, режим ЧПУ. $page — номер страницы. $size — размер страницы, необязательный параметр

Возвращает объект \Bitrix\Main\Web\Uri с параметрами навигации

Изменяет переданный объект адреса, а не его копию. В режиме ЧПУ сначала удаляет прежние параметры навигации

clearParams($uri, $sef)

$uri — объект \Bitrix\Main\Web\Uri. $sef — логическое значение, режим ЧПУ

Возвращает объект \Bitrix\Main\Web\Uri без параметров навигации

Изменяет переданный объект адреса, а не его копию

Класс \Bitrix\Main\Web\Uri разбирает адрес на части и позволяет менять их по отдельности. Он входит в набор объектов HTTP-клиента — о них читайте в статье HTTP-клиент.

Создать объект навигации

Конструктор принимает единственный аргумент — идентификатор навигации. Этот же идентификатор становится именем параметра в адресе страницы.

use Bitrix\Main\UI\PageNavigation;

$navigation = new PageNavigation('users');

С таким идентификатором адрес третьей страницы выглядит как /company/?users=page-3.

Метод getId() возвращает идентификатор — он нужен шаблонам навигации, чтобы собрать имя параметра.

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

Задать порядок вызовов

Порядок вызовов важен: метод initFromUri() учитывает только те настройки, которые заданы до его вызова. Из-за нарушенного порядка навигация продолжает работать, но игнорирует часть параметров из адреса.

Задавайте настройки в таком порядке.

  1. Разрешите или запретите показ всех записей методом allowAllRecords().

  2. Задайте список разрешенных размеров страницы методом setPageSizes().

  3. Задайте размер страницы по умолчанию методом setPageSize().

  4. Задайте общее число записей методом setRecordCount().

  5. Прочитайте параметры из адреса методом initFromUri().

Пример. Настройка объекта перед выборкой.

use Bitrix\Main\UI\PageNavigation;
use Bitrix\Main\UserTable;

$totalCount = UserTable::getCount();

$navigation = new PageNavigation('users');
$navigation
    ->allowAllRecords(false)
    ->setPageSizes([10, 20, 50])
    ->setPageSize(20)
    ->setRecordCount($totalCount)
    ->initFromUri()
;

Метод initFromUri() ничего не возвращает, поэтому он завершает цепочку вызовов. Продолжить цепочку после него нельзя — следующие настройки задавайте отдельными выражениями.

Если вызвать setPageSizes() после initFromUri(), размер страницы из адреса не применится. Метод initFromUri() сверяет значение из адреса со списком разрешенных размеров, а на момент проверки список еще пуст. То же правило действует для allowAllRecords(true): вызов после initFromUri() не включит режим показа всех записей для текущего запроса.

Число записей действует иначе, чем остальные настройки. Если задать его до initFromUri(), метод ограничит номер страницы сверху: запрос страницы 50 при пяти страницах вернет пятую страницу. Если задать после, номер страницы уже не ограничивается — смещение уйдет за пределы выборки, и запрос вернет пустой результат.

Когда число записей заранее неизвестно

Иногда общее число приходит из того же запроса, что и данные, — например, при выборке без отдельного подсчета. Задать его до initFromUri() в таком случае невозможно, и ограничение номера страницы сверху не сработает.

Тогда проверяйте номер страницы сами: после выборки сравните getCurrentPage() с getPageCount() и при выходе за границы покажите пустой результат или перенаправьте на последнюю страницу. Готовый прием для больших таблиц описан в разделе Как работает навигация без COUNT.

Задать размер страницы

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

  • setPageSize() — задает текущий размер страницы напрямую, без проверки по списку разрешенных значений,

  • setPageSizes() — задает список размеров, которые разрешено выбирать через адрес страницы.

Список задает допустимые значения: initFromUri() применит размер из адреса, только если он есть в списке. При списке [10, 20, 50] адрес ?users=page-2-size-1000 изменит номер страницы, но не размер. Так посетитель не сможет запросить произвольно большую выборку одним изменением адреса.

Список доступен компоненту вывода через getPageSizes(). Переключатель размеров по этому списку строит только шаблон admin компонента main.pagenavigation. Остальные системные шаблоны его не выводят.

Задать число записей

Общее число записей объект не вычисляет — его задает разработчик методом setRecordCount(). По этому числу считается количество страниц.

$navigation->setRecordCount($totalCount);

Метод getPageCount() делит число записей на размер страницы и округляет результат вверх: при 47 записях и размере страницы 20 получается три страницы, последняя из них неполная.

Пока число записей не задано, объект считает, что страниц нет. Компонент вывода в таком случае навигацию не показывает, поэтому ссылки на странице не появляются.

Нулевое число записей до вызова initFromUri() дает отрицательное смещение. Объект ограничит номер страницы значением getPageCount(), то есть нулем, и getOffset() вернет -20 при размере страницы 20. Запрос с таким смещением выполнить нельзя. Случай возникает, когда фильтр не нашел ни одной записи, а в адресе остался номер страницы.

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

if ($navigation->getPageCount() === 0)
{
    // Записей нет: выборку не выполняем, навигацию не выводим
    return [];
}

Задать номер страницы вручную

Обычно номер страницы приходит из адреса через initFromUri().

Метод setCurrentPage() нужен, когда номер берется из другого источника — например, из тела запроса или из сохраненного состояния интерфейса.

$navigation->setCurrentPage(3);

Заданное значение объект не проверяет по числу страниц: номер больше числа страниц даст смещение за пределами выборки, и запрос вернет пустой результат. Номер страницы ограничивает сверху только метод initFromUri() на условиях, описанных в разделе Задать порядок вызовов.

Прочитать параметры из адреса

Метод initFromUri() берет адрес текущего запроса и ищет в нем параметры навигации по идентификатору объекта. Объект поддерживает два формата: параметр запроса и ЧПУ.

Сначала объект ищет параметр запроса с именем, равным идентификатору навигации. Формат значения — пары «ключ-значение» через дефис: page-3-size-20. Для навигации с идентификатором users такой адрес выглядит как /company/?users=page-3-size-20.

Если параметра запроса с таким именем нет, объект разбирает путь адреса. Он ищет в пути фрагмент /идентификатор/page-номер/, за которым может идти /size-размер/: в адресе /company/users/page-3/size-20/ объект прочитает третью страницу по 20 записей.

Из адреса объект читает три значения.

  • Номер страницы. Значения меньше единицы объект пропускает и оставляет прежний номер.

  • Размер страницы. Применяется, только если значение есть в списке setPageSizes().

  • Признак показа всех записей page-all. Включает режим, только если он разрешен через allowAllRecords(true).

Рассчитать границы выборки

Два метода дают числа для запроса:

  • getOffset() — смещение первой записи текущей страницы,

  • getLimit() — ограничение на число записей.

use Bitrix\Main\UserTable;

$users = UserTable::query()
    ->setSelect(['ID', 'LOGIN'])
    ->setOffset($navigation->getOffset())
    ->setLimit($navigation->getLimit())
    ->fetchCollection()
;

Объект отсчитывает смещение от нуля: на первой странице getOffset() возвращает 0, на второй при размере страницы 20 — 20.

На последней неполной странице ограничение оказывается больше, чем фактическое число оставшихся записей. При 47 записях и размере страницы 20 на третьей странице getLimit() вернет 20, а запрос выберет семь записей.

Для запроса такое значение безопасно: ограничение задает верхнюю границу. Использовать getLimit() как число записей на странице нельзя. Чтобы вывести, сколько записей показано, берите фактическое число строк результата.

Показать все записи на одной странице

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

  • allowAllRecords() — разрешает режим, но не включает его,

  • allRecordsAllowed() — возвращает разрешение,

  • allRecordsShown() — показывает, включен ли режим для текущего запроса.

Сам режим включает метод initFromUri(), когда находит в адресе page-all.

Пример. Включение режима и проверка его состояния перед выборкой.

use Bitrix\Main\UI\PageNavigation;
use Bitrix\Main\UserTable;

$navigation = new PageNavigation('users');
$navigation
    ->allowAllRecords(true)
    ->setPageSize(20)
    ->initFromUri()
;

// Число записей нужно задать до расчета границ выборки
$totalCount = UserTable::getCount();
$navigation->setRecordCount($totalCount);

$users = UserTable::query()
    ->setSelect(['ID', 'LOGIN'])
    ->setOffset($navigation->getOffset())
    ->setLimit($navigation->getLimit())
    ->fetchCollection()
;

Порядок вызовов в этом режиме особенно важен. Метод getLimit() возвращает общее число записей, поэтому задайте число записей до расчета границ выборки. Если оно не задано, метод вернет null и выборка окажется неограниченной.

Не включайте режим показа всех записей на больших выборках. Посетитель может добавить page-all в адрес и запросить все записи таблицы одним запросом.

Сформировать ссылки

Компонент bitrix:main.pagenavigation выводит готовые ссылки на страницы. Собирайте адрес вручную в двух случаях: когда встраиваете навигацию в собственный шаблон и когда строите ссылку вне компонента. За это отвечают два метода:

  • addParams() — добавляет в адрес параметры навигации,

  • clearParams() — удаляет их и возвращает базовый адрес списка.

У метода addParams() третий аргумент задает номер страницы, четвертый — размер страницы. Без четвертого аргумента в ссылку попадет только номер страницы, а размер останется прежним.

Аргумент $sef выбирает формат ссылки: в режиме ЧПУ метод добавляет параметры в путь адреса, иначе — в строку запроса. Оба формата совместимы с initFromUri(). Готовый пример с обоими форматами есть в статье Постраничная навигация.

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

Метод clearParams() затрагивает только параметры текущей навигации, остальные параметры адреса остаются на месте. Так вы получите базовый адрес списка — например, для ссылки на первую страницу.

use Bitrix\Main\UI\PageNavigation;
use Bitrix\Main\Web\Uri;

$navigation = new PageNavigation('users');

$uri = new Uri('https://example.com/company/?users=page-3&sort=name');
$isSefMode = false;

echo (string)$navigation->clearParams($uri, $isSefMode);
// https://example.com/company/?sort=name

В режиме ЧПУ метод addParams() сам вызывает clearParams() перед добавлением параметров, поэтому прежний номер страницы в пути не дублируется.

Класс AdminPageNavigation

Класс \Bitrix\Main\UI\AdminPageNavigation наследует PageNavigation и подставляет настройки, принятые в административном разделе. Кроме конструктора, собственных методов у него нет.

Отличий три.

  • Список разрешенных размеров страницы содержит значения 10, 20, 50, 100, 200 и 500.

  • Режим показа всех записей разрешен.

  • Конструктор сам вызывает initFromUri().

use Bitrix\Main\UI\AdminPageNavigation;
use Bitrix\Main\UserTable;

$navigation = new AdminPageNavigation('users');

$totalCount = UserTable::getCount();
$navigation->setRecordCount($totalCount);

Повторно вызывать initFromUri() не нужно: конструктор уже прочитал параметры. По той же причине настройки, заданные после конструктора, на разбор адреса не влияют.

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

Второй наследник — класс ReversePageNavigation. Он считает границы выборки от конца списка и переопределяет расчет страниц, смещения и ограничения.

Получить объект в контроллере

Контроллеру объект навигации создавать не нужно: достаточно объявить аргумент типа PageNavigation, и ядро подставит готовый объект. Ядро создает его с идентификатором nav и уже прочитанными параметрами адреса. Разрешенный размер страницы — от 1 до 50.

final class Iblock extends Controller
{
    // ...

    public function paginationAction(\Bitrix\Main\UI\PageNavigation $pagination): array
    {
        return [
            'page' => $pagination->getCurrentPage(),
            'size' => $pagination->getPageSize(),
            'limit' => $pagination->getLimit(),
            'offset' => $pagination->getOffset(),
        ];
    }
}

Из жестко заданного идентификатора и диапазона размеров следуют два ограничения.

  • Размер страницы вне диапазона от 1 до 50 не применится.

  • Два независимых списка в одном действии получить нельзя: идентификатор задан жестко.

Для этих случаев создавайте объект вручную.

Число записей ядро не задает, поэтому номер страницы из адреса не ограничивается сверху. Задайте число записей после выборки методом setRecordCount() и проверьте номер страницы самостоятельно.

Сценарий целиком с примером действия и ответом описан в статье Контроллеры.