Иногда задача звучит невинно: «У нас есть таблица в админке, сделайте её как стандартный список товаров в Битриксе». То есть чекбоксы, массовые действия, гамбургер, шестерёнка настройки колонок, сортировка по заголовкам, фильтр, пагинация и AJAX без перезагрузки страницы.
На словах — обычная доработка интерфейса. На практике — переезд с самописной HTML-таблицы на полноценную подсистему административного грида 1С-Битрикс.
В этом кейсе мы разбираем, почему «просто прикрутить шестерёнку» не получится, зачем нужен CAdminUiList, почему AJAX может «работать один раз», чем опасен неправильный BASE_LINK и почему агрегаты лучше материализовать заранее, а не считать на лету в PHP.
Содержание
- Контекст задачи
- Архитектура: HL-блок, D7 ORM и нестандартный путь
- Скелет правильной страницы CAdminUiList
- Грабля №1: AJAX работает только один раз
- Грабля №2: фильтр не привязывается к гриду
- Грабля №3: агрегатные колонки и масштабирование
- Отладка AJAX без браузера
- Окружение и воспроизводимость
- Выводы
- FAQ
Контекст задачи
Исходная ситуация: в административной части проекта был самописный раздел «Правила первичной отбраковки». Данные выводились обычной HTML-таблицей. Таблица работала, но по ощущениям была чужеродной для Битрикса: без стандартной настройки колонок, без нормальных массовых действий, без привычной сортировки и фильтрации.
Запрос клиента был понятный: сделать интерфейс «как в стандартных списках товаров». То есть не просто красиво нарисовать таблицу, а получить поведение штатного административного списка:
- чекбоксы для выбора строк;
- гамбургер действий;
- шестерёнку настройки колонок;
- сортировку кликом по заголовку;
- фильтр;
- пагинацию;
- AJAX-навигацию без полной перезагрузки страницы.
И вот здесь начинается неприятная правда: нативный админ-грид Битрикса — это не набор отдельных кнопок. Это связанная подсистема ядра. Если пытаться прикрутить её кусками к самописной таблице, быстро появляются странные баги: сортировка ведёт не туда, фильтр живёт отдельно, AJAX ломает URL, пагинация начинает накапливать служебные параметры.
Правильный путь — использовать CAdminUiList и строить страницу по правилам ядра. Но даже в этом случае есть несколько грабель, о которых в документации обычно не пишут прямо.
Архитектура: HL-блок, D7 ORM и нестандартный путь
В нашем случае данные хранились не в обычном инфоблоке, а в Highload-блоке. Доступ к ним шёл через D7 ORM: getList() для выборки и getCount() для общего количества записей.
Это важный момент. В стандартных примерах часто подразумевается, что страница лежит в привычном месте, работает с типовой сущностью и использует типовой путь. В реальном проекте всё было сложнее:
- данные находились в Highload-блоке;
- страница физически лежала в
/local/; - в админке она открывалась как
/bitrix/admin/...черезurlrewrite.php; - часть логики была завязана на кастомные классы проекта;
- требовалось сохранить поведение, похожее на стандартные списки ядра.
Поэтому ориентироваться только на документацию было недостаточно. Более полезный источник — живые эталоны ядра:
iblock/admin/iblock_element_admin.php;highloadblock/admin/highloadblock_rows_list.php.
Именно в таких файлах видно, в каком порядке подключается пролог, как формируется список, как привязывается фильтр, как работает навигация и где ядро ожидает «чистую» базовую ссылку.
Скелет правильной страницы CAdminUiList
Первое, что важно принять: у страницы на CAdminUiList критичен порядок выполнения.
Упрощённая логика такая:
- в самом начале подключается
prolog_admin_before.php; - после этого выполняется вся серверная логика страницы;
- готовятся фильтр, сортировка, выборка, навигация, строки грида;
- только потом подключается
prolog_admin_after.php; - после этого вызывается вывод фильтра и списка.
Здесь легко ошибиться, если раньше страница была обычной самописной админкой, где часть логики выполнялась уже после визуального пролога.
Отдельная грабля — проверка прав. Если конструктор вашего класса данных требует администратора и при отсутствии доступа выбрасывает исключение, неавторизованный пользователь может получить 500 ошибку вместо формы логина.
Поэтому проверку лучше делать явно до создания объектов, которые зависят от прав:
require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_admin_before.php');
global $USER, $APPLICATION;
if (!$USER->IsAdmin()) {
$APPLICATION->AuthForm('Access denied');
}
// дальше создаём объекты, готовим фильтр, сортировку, выборку и грид
Кастомный HTML-контент тоже лучше не выводить «голым» echo между прологом и списком. Для этого у административной страницы есть штатный механизм:
$APPLICATION->BeginPrologContent();
// ваш дополнительный контент
$APPLICATION->EndPrologContent();
Это снижает риск сломать структуру административной страницы и поведение компонентов нового UI.
Грабля №1: AJAX работает только один раз
Самая неприятная проблема в этом кейсе выглядела так: первый клик по сортировке или пагинации срабатывал нормально. Данные действительно обновлялись. Но после этого адрес в браузере превращался в AJAX-URL со служебными параметрами. Следующий клик уже работал некорректно, а принудительное обновление страницы через CMD+R могло давать белый экран.
На первый взгляд кажется, что проблема на сервере. Но серверная часть была в порядке:
- сортировка применялась правильно;
- данные приходили корректные;
- AJAX-ответ формировался;
- ошибок в самой ORM-выборке не было.
Проблема оказалась на стыке навигации и клиентской истории грида. Клиентская часть записывала в адресную строку «грязный» URL через механизм истории. В URL попадали служебные параметры вроде internal, grid_action, bxajaxid, а параметр страницы начинал накапливаться.
Причина была в том, что список собирался через CAdminResult и старый механизм NavText(GetNavPrint()) без явного BASE_LINK. В результате включался не тот шаблон пагинации, и новый UI-грид получал некорректную базу для навигации.
Правильное решение — использовать CAdminUiResult и задавать навигацию через SetNavigationParams() с чистым BASE_LINK:
$result = new CAdminUiResult($ormResult, $sTableID);
$lAdmin->SetNavigationParams($result, [
'BASE_LINK' => '/bitrix/admin/your_page.php',
]);
Смысл BASE_LINK простой: гриду нужно явно сказать, какой URL считать нормальной базой страницы. Особенно если физически файл лежит в /local/, а в админке открывается через /bitrix/admin/... и urlrewrite.php.
После этого навигация переключается на нативный механизм административного грида, а AJAX перестаёт превращать адресную строку в мусорную копию служебного запроса.
Практический вывод: в новом административном UI Битрикса навигацию для грида лучше задавать только через CAdminUiResult и SetNavigationParams() с явным BASE_LINK.
Грабля №2: фильтр не привязывается к гриду
Вторая проблема была менее эффектной, но такая же вредная: фильтр визуально отображался, но работал как будто отдельно от грида. Сортировка могла уходить в полную навигацию вместо XHR, а состояние фильтра и списка начинало жить разной жизнью.
Причина — ручное подключение компонента bitrix:main.ui.filter с тем же ID, что и у грида, но без корректного GRID_ID. В результате два компонента фактически боролись за один идентификатор.
В таких случаях лучше не собирать фильтр вручную. У CAdminUiList есть штатный метод:
$lAdmin->DisplayFilter($uiFilter);
Он сам делает важные вещи:
- правильно привязывает фильтр к гриду;
- передаёт нужный
GRID_ID; - включает подписи полей;
- согласует поведение фильтра с AJAX-гридом.
При этом значения фильтра всё равно читаются отдельно через Bitrix\Main\UI\Filter\Options. Затем на их основе вручную строится ORM-фильтр для getList().
Обычно приходится обрабатывать несколько типов полей:
string— текстовый поиск;list— выбор из списка;number— числовые условия через_numsel,_from,_to;date— диапазоны дат.
Здесь важно не путать две задачи. DisplayFilter() отвечает за корректное отображение и привязку UI. А построение ORM-фильтра — это уже ваша серверная логика.
Грабля №3: агрегатные колонки и масштабирование
Отдельный пласт проблем начинается, когда в гриде появляются колонки, которых физически нет в основной сущности.
Например:
- количество срабатываний правила;
- дата последнего срабатывания;
- результат агрегации по другой таблице;
- статистика, рассчитанная по связанным данным.
На маленьких объёмах очень соблазнительно решить это в PHP: получить список правил, отдельно посчитать агрегаты, отфильтровать ID, отсортировать массив и отдать результат в грид.
Это плохой путь.
В нашем кейсе ожидалось около 100 тысяч записей. Наивный подход уже на промежуточном этапе мог тянуть в память 60 тысяч и больше строк. Даже если сегодня это «терпимо», завтра такая страница станет медленной, нестабильной и плохо предсказуемой.
Правильный подход — материализация агрегатов. То есть не считать тяжёлые значения при каждом открытии списка, а сохранять их в отдельных полях записи в момент пересчёта данных.
Например:
UF_LAST_COUNT— количество последних срабатываний;UF_LAST_DATETIME— дата последнего срабатывания.
Такие поля можно обновлять в процессе фонового пересчёта или в методе, который обновляет статистику. После этого фильтрация, сортировка и пагинация снова становятся нормальными SQL-операциями на уровне базы данных.
Это принципиально важно. Админ-грид должен получать уже подготовленную выборку с limit, offset, order и фильтром. Он не должен заставлять PHP загружать весь массив данных в память ради одной страницы.
Также нужны индексы. Без них даже аккуратный UI не спасёт:
- индексы под поля фильтрации;
- индексы под поля сортировки;
- композитные индексы под частые комбинации условий;
- индексы под поля, участвующие в агрегации;
- префиксные индексы для длинных текстовых полей, если они участвуют в поиске.
Лучше оформлять такие изменения через миграции, например через Sprint.Migration. Тогда структура базы будет воспроизводимой, а не «где-то руками добавили индекс на проде».
Отладка AJAX без браузера
Ещё один полезный приём — отлаживать AJAX-запросы не только через браузер, но и напрямую через серверную среду.
В нашем случае помогло воспроизведение реального авторизованного AJAX-запроса через curl с общей файловой сессией. Если CLI и FPM работают в одном контейнере и используют одну директорию сессий, можно увидеть сырой ответ грида и отделить серверную проблему от клиентской.
Это особенно полезно, когда визуально кажется, что «Битрикс просто сломался», а на деле сервер отдаёт корректные данные, но клиентская часть неправильно обновляет историю или URL.
Важно понимать и другой момент: прямое открытие AJAX-URL в браузере может давать белый экран или ошибки вроде BX is not defined. Это не всегда баг. AJAX-ответ не обязан быть самостоятельной HTML-страницей. Он рассчитан на обработку внутри уже загруженной административной страницы.
Поэтому диагностику лучше строить так:
- проверить, что ORM-запрос возвращает правильные данные;
- проверить сырой AJAX-ответ;
- проверить, какие параметры попадают в URL;
- проверить, какой шаблон пагинации используется;
- сравнить поведение со штатными файлами ядра.
Чтение исходников ядра здесь не прихоть, а нормальный рабочий метод. В подобных задачах ответы часто лежат не в документации, а в том, как сами разработчики Битрикса собирают административные списки.
Окружение и воспроизводимость
Такие баги особенно неприятно ловить на боевом сервере. Поэтому нормальное локальное окружение — не роскошь, а обязательная часть работы.
В кейсе проект поднимался локально в Docker на официальных образах bitrix-tools:
- nginx;
- php-fpm;
- mysql или percona;
- cron;
- восстановление базы из бэкапа.
При восстановлении старого или крупного проекта почти всегда всплывают технические нюансы. Например:
- в дампе могут быть внешние ключи на таблицы, которые ещё не созданы, поэтому при импорте приходится временно отключать
FOREIGN_KEY_CHECKS; - для MySQL 8 или Percona могут понадобиться дополнительные права, например
SESSION_VARIABLES_ADMIN, если в подключении выполняетсяSET innodb_strict_mode=0; - версия PHP должна соответствовать коду проекта, особенно если используются современные возможности языка вроде типизированных констант классов.
Воспроизводимое окружение сокращает время отладки в разы. Когда можно спокойно повторить AJAX-запрос, посмотреть сырой ответ, временно включить дополнительный лог и сравнить поведение с ядром, задача перестаёт быть гаданием по интерфейсу.
Выводы
Главный вывод простой: не надо воевать с фреймворком. Если нужен административный список в стиле нового UI Битрикса, лучше сразу строить его на CAdminUiList, а не пытаться оживить самописную HTML-таблицу отдельными кнопками и компонентами.
Три вещи, которые экономят много часов отладки:
CAdminUiResultвместе сSetNavigationParams()и явнымBASE_LINK;- нативный
DisplayFilter()вместо ручной сборкиmain.ui.filter; - материализация агрегатов вместо расчётов и сортировки больших массивов в PHP.
И ещё одно: думать о масштабе нужно сразу. Если в системе ожидаются десятки или сотни тысяч записей, фильтрация, сортировка и пагинация должны выполняться на уровне базы данных. PHP не должен имитировать SQL поверх огромного массива.
Хороший административный интерфейс в Битриксе — это не только «чтобы было похоже на ядро». Это корректная архитектура страницы, чистая навигация, правильная связь фильтра с гридом, индексы в базе и воспроизводимое окружение для отладки.
Если всё это учесть с самого начала, кастомный раздел в админке перестаёт быть хрупкой самописной таблицей и становится нормальным рабочим инструментом для проекта.





