1С Битрикс · 7 мин чтения

Свой раздел в админке Битрикс: список и форма редактирования на CAdminList

Свой раздел в админке Битрикс нужен, как только бизнес-логика перерастает инфоблоки: заявки из 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 дней, в зависимости от того, сколько связей с другими сущностями нужно подтянуть. Если добавляются права по группам пользователей и групповые действия, закладываю ещё день на тесты.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Самозанятый Калинкин Н. А. · работаю с физлицами и юрлицами

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно