Свой раздел в админке Битрикс нужен, как только бизнес-логика перерастает инфоблоки: заявки из Telegram-бота на aiogram, лог синхронизации с СДЭК, таблица промокодов из Tilda-скрипта. Держать это в highload-блоке через инфоблочный интерфейс неудобно, а городить отдельную панель на голом PHP вне админки долго и небезопасно. Штатный инструмент для этого - класс CAdminList вместе с CAdminForm, они дают готовый список с фильтром, сортировкой, массовыми действиями и форму редактирования, которая выглядит как часть системы, а не как самопальная страница.
Из чего состоит свой раздел в админке Битрикс
Минимальный набор файлов у меня обычно такой: модуль (даже локальный, без публикации в маркетплейсе), таблица в базе под задачу, файл списка records_list.php, файл формы record_edit.php и menu.php для пункта в меню. На практике я почти всегда делаю модуль локальным - кладу его в /local/modules/имя.модуля/ и подключаю через install/index.php с CModule::CreateModuleTable() и AddModuleToAdminMenu(). Так раздел переживает обновления ядра и не путается с чужими модулями из маркетплейса.
Таблицу под свою сущность создаю руками через миграцию или install-скрипт модуля, ORM Bitrix (BitrixMainEntity) тут не обязателен, но с ним удобнее собирать выборки для списка через getList вместо ручного SQL.
Регистрация модуля и пункта меню в разделе админки Битрикс
Пункт меню появляется через файл menu.php внутри /bitrix/modules/имя.модуля/admin/ (или /local/modules/…/admin/), который возвращает массив с описанием раздела:
<?php
if (!$USER->IsAdmin() && !CModule::IncludeModule('main')) {
return false;
}
return array(
array(
'parent_menu' => 'global_menu_content',
'sort' => 100,
'text' => 'Заявки из бота',
'title' => 'Заявки, собранные Telegram-ботом',
'icon' => 'bot_requests_menu_icon',
'page_icon' => 'bot_requests_menu_icon',
'items_id' => 'menu_bot_requests',
'items' => array(
array(
'text' => 'Список заявок',
'url' => 'bot_requests_list.php?lang='.LANGUAGE_ID,
'more_url' => array('bot_requests_edit.php'),
),
),
),
);
Последние два параметра, page_icon и items_id, часто забывают - без них пункт не подсвечивается активным на странице формы, потому что более_url не подхватывает related-страницы. У меня было так дважды: список открывался нормально, а при переходе на форму редактирования подсветка в меню слетала на «Контент» вместо конкретного раздела - искал минут двадцать, пока не сверился с оригинальными менюшками инфоблоков.
Список записей на CAdminList: таблица, фильтр и сортировка
CAdminList берёт на себя пагинацию, сортировку по столбцам, чекбоксы для массовых действий и рендер контекстного меню строки - руками писать это нет смысла. Базовый каркас files/bot_requests_list.php:
<?php
require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_admin_before.php');
global $APPLICATION;
if (!$USER->IsAdmin()) {
$APPLICATION->AuthForm('Доступ только у администраторов');
}
$sTableID = 'tbl_bot_requests';
$oSort = new CAdminSorting($sTableID, 'ID', 'DESC');
$lAdmin = new CAdminList($sTableID, $oSort);
$arFilterFields = array('find_name', 'find_phone', 'find_status');
$lAdmin->InitFilter($arFilterFields);
$arFilter = array();
if ($_REQUEST['find_name'] <> '') {
$arFilter['%NAME'] = $_REQUEST['find_name'];
}
if ($_REQUEST['find_status'] <> '' && $_REQUEST['find_status'] <> 'NOT_REF') {
$arFilter['STATUS'] = $_REQUEST['find_status'];
}
$rsData = CBotRequests::GetList(
array($by => $order),
$arFilter,
false,
$lAdmin->InitNavStart(20)
);
$rsData->NavStart();
$lAdmin->NavText($rsData->GetNavPrint('Записи'));
while ($arRes = $rsData->GetNext()) {
$row = &$lAdmin->AddRow($arRes['ID'], $arRes);
$row->AddViewField('NAME', htmlspecialcharsbx($arRes['NAME']));
$row->AddActions(array(
array(
'ICON' => 'edit',
'DEFAULT' => true,
'TEXT' => 'Изменить',
'ACTION' => $lAdmin->ActionRedirect('bot_requests_edit.php?ID='.$arRes['ID'].'&lang='.LANGUAGE_ID),
),
array(
'ICON' => 'delete',
'TEXT' => 'Удалить',
'ACTION' => "if(confirm('Удалить запись?')) ".$lAdmin->ActionDoGroup($arRes['ID'], 'delete'),
),
));
}
$lAdmin->AddHeaders(array(
array('id' => 'ID', 'content' => 'ID', 'sort' => 'id', 'default' => true),
array('id' => 'NAME', 'content' => 'Имя', 'sort' => 'name', 'default' => true),
array('id' => 'PHONE', 'content' => 'Телефон', 'sort' => 'phone', 'default' => true),
array('id' => 'STATUS', 'content' => 'Статус', 'sort' => 'status', 'default' => true),
));
$lAdmin->CheckListMode();
require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/title.php');
$lAdmin->DisplayList();
require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/epilog_admin.php');
Это ровно тот сценарий, который я собирал для одного клиента: aiogram-бот пишет заявки напрямую в таблицу через прямой INSERT (без инфоблоков, чтобы не тащить лишний оверхед), а менеджеры смотрят их в этом разделе с фильтром по статусу и телефону. GetList на своей таблице пишется руками с параметризованными запросами - $DB->Query с плейсхолдерами, чтобы не словить SQL-инъекцию через $arFilter.
Бесплатный материал
🎁 Полезный скрипт в подарок
Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.
Без спама. Отписка в 1 клик.
Групповые действия и сортировка
Массовое удаление или смена статуса вешаются через group_action_control - CAdminList сам добавляет чекбоксы и выпадающий список действий, если вызвать $lAdmin->AddGroupActionTable(). Сортировку по столбцам включает параметр ‘sort’ в AddHeaders, а сам порядок ($by, $order) CAdminSorting достаёт из GET-параметров таблицы автоматически, руками парсить $_REQUEST[‘by’] не нужно.
Форма добавления и редактирования записи
Для формы штатный вариант - CAdminForm с вкладками через CAdminTabControl. На маленьких разделах я иногда верстаю форму просто HTML-таблицей внутри тех же тегов вкладок, но для 10+ полей удобнее собрать её через массив полей и рендерить циклом. Каркас bot_requests_edit.php:
<?php
require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_admin_before.php');
$ID = intval($_REQUEST['ID']);
$arRecord = $ID ? CBotRequests::GetByID($ID) : array();
if ($_REQUEST['save'] == 'Y' && check_bitrix_sessid()) {
$arFields = array(
'NAME' => trim($_REQUEST['NAME']),
'PHONE' => trim($_REQUEST['PHONE']),
'STATUS' => $_REQUEST['STATUS'],
);
if ($ID) {
CBotRequests::Update($ID, $arFields);
} else {
$ID = CBotRequests::Add($arFields);
}
LocalRedirect('bot_requests_list.php?lang='.LANGUAGE_ID);
}
$tabControl = new CAdminTabControl('tabControl', array(
array('DIV' => 'edit1', 'TAB' => 'Данные заявки', 'ICON' => 'main_user_edit', 'TITLE' => 'Основные поля'),
));
require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/title.php');
$tabControl->Begin();
?>
<form method="POST" action="<?= $APPLICATION->GetCurPage() ?>?ID=<?= $ID ?>&lang=<?= LANGUAGE_ID ?>">
<?php $tabControl->BeginNextTab(); ?>
<tr>
<td>Имя:</td>
<td><input type="text" name="NAME" value="<?= htmlspecialcharsbx($arRecord['NAME']) ?>"></td>
</tr>
<tr>
<td>Телефон:</td>
<td><input type="text" name="PHONE" value="<?= htmlspecialcharsbx($arRecord['PHONE']) ?>"></td>
</tr>
<?php
$tabControl->Buttons();
?>
<input type="submit" name="save" value="Сохранить">
<?= bitrix_sessid_post() ?>
</form>
<?php
$tabControl->End();
require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/epilog_admin.php');
Важный момент, который легко упустить: если поле нужно и в списке, и на форме, я держу описание валидации в одном месте (методе класса), а не дублирую проверки в обоих файлах - иначе через пару месяцев правки форма и список начинают расходиться по допустимым значениям статуса.
Права доступа и защита от CSRF в своём разделе админки
CAdminList и CAdminForm сами не проверяют права - это моя ответственность как разработчика раздела. На старте файла списка и формы обязательна проверка через $APPLICATION->GetGroupRight(‘имя.модуля’) или простая $USER->IsAdmin() для небольших внутренних инструментов. Для модулей с несколькими уровнями доступа (просмотр / редактирование / удаление) права настраиваются в install/index.php через RegisterModuleDependences и таблицу b_module_group.
За CSRF отвечает check_bitrix_sessid() перед любым изменяющим действием (сохранение, удаление, смена статуса) и bitrix_sessid_post() в самой форме - это генерирует скрытое поле с токеном сессии. Без этой пары форма технически работает, но открыта для подделки запроса с чужого сайта - проверяющие на пентестах бьют это в первую очередь.
| Способ сделать раздел | Скорость разработки | Права и CSRF из коробки | Когда оправдано |
|---|---|---|---|
| CAdminList + CAdminForm | Средняя, но переиспользуемо | Нужно подключить руками, зато штатным способом | Постоянный раздел, которым пользуются менеджеры |
| Голый HTML внутри админки | Быстро на старте | Всё пишется с нуля | Разовый служебный экран для себя |
| Highload-блок вместо своей таблицы | Быстро через визард | Права наследуются от highload-блока | Когда структуру данных ещё будете менять |
Если раздел нужен как часть более крупной интеграции, скажем, синхронизации заказов с СДЭК или выгрузки лидов из n8n-сценария в CRM-таблицу Битрикс, обычно проще заказать разработку кастомного модуля целиком, вместе с фоновыми агентами и обработкой ошибок, чем собирать это по кускам методом проб и ошибок.
Частые ошибки при разработке своего раздела в админке Битрикс
За несколько проектов на CAdminList набрался стандартный список граблей:
- Забытый check_bitrix_sessid() на действии удаления - раздел уязвим к CSRF, а на код-ревью это всплывает сразу.
- Прямая склейка $_REQUEST в SQL внутри GetList вместо параметризованных запросов - при фильтре по имени с апострофом падает с ошибкой, а без апострофа открыт для инъекции.
- Отсутствие htmlspecialcharsbx() при выводе значений в AddViewField - хранимый XSS через поле «Имя», если данные приходят снаружи, например из формы на сайте или от бота.
- Пагинация без InitNavStart() - при полутора тысячах записей список рендерит всё разом и ощутимо тормозит админку.
- Логика сохранения в файле формы вместо метода класса - через полгода правишь бизнес-правило в одном месте, а второе продолжает жить по старой логике.
Чтобы сайт работал без сбоев
Техподдержка
от 15 000 ₽/мес
Подробнее →Частые вопросы
Обязательно ли делать модуль, или можно обойтись одним файлом в /bitrix/admin/?
Можно, для разового внутреннего инструмента я так и делаю: кладу файл списка прямо в /bitrix/admin/ и добавляю пункт в меню без регистрации модуля. Но при обновлении ядра или переносе на новый сервер такой файл легко потерять, а модуль в /local/modules/ переживает обновления и нормально попадает в git.
Можно ли использовать CAdminList для данных из внешнего API, а не из таблицы Битрикс?
Да, GetList не обязан ходить в базу Битрикс - можно дергать внешний REST и собирать массив вручную, лишь бы на выходе была структура с постраничной выборкой. Только имейте в виду, что сортировка и фильтр тогда придётся реализовывать на своей стороне, а не полагаться на SQL ORDER BY.
Как добавить кастомное поле типа «список» с привязкой к другой таблице?
В списке это обычный AddViewField с уже готовым текстовым значением (подтягиваете название по ID заранее в цикле). В форме - обычный select, куда перед рендером подгружаете массив вариантов через свой метод GetStatusList() и рендерите циклом опций, привязки «на лету» CAdminForm не делает.
Сколько времени уходит на такой раздел с нуля?
На список с фильтром и форму с 5-7 полями у меня уходит от 1 до 3 дней, в зависимости от того, сколько связей с другими сущностями нужно подтянуть. Если добавляются права по группам пользователей и групповые действия, закладываю ещё день на тесты.