Миграции для Highload-блоков через Bitrix API или как создать HL блок Битрикс API

Как устроено создание HL-блока

При создании Highload-блока задействованы два слоя API.

1. Метаданные HL-блока — HighloadBlockTable

use Bitrix\Main\Loader;
use Bitrix\Highloadblock\HighloadBlockTable;

Loader::includeModule('highloadblock');

$result = HighloadBlockTable::add(array(
    'NAME' => 'DemoCatalog',
    'TABLE_NAME' => 'demo_catalog',
));

if (!$result->isSuccess()) {
    throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}

$hlblockId = (int)$result->getId();

Что происходит внутри:

  1. Запись попадает в b_hlblock_entity.
  2. В БД создаётся физическая таблица (demo_catalog) с колонкой ID (PK, autoincrement).

2. Пользовательские поля — CUserTypeEntity

Для HL-блоков ENTITY_ID всегда имеет вид HLBLOCK_{ID}:

$entityId = HighloadBlockTable::compileEntityId($hlblockId); // HLBLOCK_12

Поля добавляются через legacy-API (D7 UserFieldTable::add() намеренно не реализован):

$userType = new \CUserTypeEntity();

$fieldId = $userType->Add(array(
    'ENTITY_ID' => $entityId,
    'FIELD_NAME' => 'UF_NAME',
    'USER_TYPE_ID' => 'string',
    // ...
));

Особенность хранения для HL

Для обычных сущностей UF-поля хранятся в b_uts_{entity} / b_utm_{entity}. Для Highload-блоков модуль highloadblock перехватывает события OnBeforeUserTypeAdd / onAfterUserTypeAdd и:

  • отключает стандартное хранение (PROVIDE_STORAGE => false);
  • добавляет колонку UF_* прямо в таблицу HL-блока;
  • для множественных полей создаёт отдельную UTM-таблицу {table_name}_{field_name}.
flowchart TD
    subgraph meta [Метаданные]
        A[HighloadBlockTable::add] --> B[b_hlblock_entity]
        C[CUserTypeEntity::Add] --> D[b_user_field]
    end
    subgraph storage [Хранение значений]
        B --> E[Физическая таблица HL]
        C --> F[ALTER TABLE: колонка UF_*]
        C --> G[UTM-таблица для MULTIPLE=Y]
    end

Ограничения и валидация

HL-блок (HighloadBlockTable)

Поле Правило
NAME Обязательно. До 100 символов. Формат: ^[A-Z][A-Za-z0-9]*$. Нельзя заканчиваться на Table. Нельзя значение collection (в любом регистре). Уникально.
TABLE_NAME Обязательно. До 64 символов. Только a-z, 0-9, _. Уникально. Таблица не должна существовать в БД.

Пользовательское поле (CUserTypeEntity::CheckFields)

Поле Правило
ENTITY_ID Обязательно. До 50 символов. Только 0-9, A-Z, _.
FIELD_NAME Обязательно. От 4 до 50 символов. Должно начинаться с UF_. Только 0-9, A-Z, _.
USER_TYPE_ID Обязательно. Должен быть зарегистрирован в системе.

Дополнительно для HL: запрещены имена полей с суффиксом _REF (зарезервировано для ORM-ссылок).

SHOW_FILTER

Допустимые значения: N (не показывать), I (точное совпадение), E (маска), S (подстрока).

Что нельзя изменить после создания

CUserTypeEntity::Update() не позволяет менять: ENTITY_ID, FIELD_NAME, USER_TYPE_ID, MULTIPLE.


Минимальная миграция: создать HL-блок

<?php

use Bitrix\Main\Loader;
use Bitrix\Highloadblock\HighloadBlockTable;

if (!Loader::includeModule('highloadblock')) {
    throw new \RuntimeException('Модуль highloadblock не установлен');
}

// Идемпотентность: ищем по NAME
$existing = HighloadBlockTable::query()
    ->addSelect('ID')
    ->where('NAME', 'DemoCatalog')
    ->exec()
    ->fetch();

if ($existing) {
    $hlblockId = (int)$existing['ID'];
} else {
    $result = HighloadBlockTable::add(array(
        'NAME' => 'DemoCatalog',
        'TABLE_NAME' => 'demo_catalog',
    ));

    if (!$result->isSuccess()) {
        throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
    }

    $hlblockId = (int)$result->getId();
}

$entityId = HighloadBlockTable::compileEntityId($hlblockId);

Добавить русское название

Название для админки хранится в b_hlblock_entity_lang:

use Bitrix\Highloadblock\HighloadBlockLangTable;

$langResult = HighloadBlockLangTable::add(array(
    'ID' => $hlblockId,
    'LID' => 'ru',
    'NAME' => 'Демо-каталог',
));

if (!$langResult->isSuccess()) {
    // При повторном запуске — обновляем
    HighloadBlockLangTable::update(
        array('ID' => $hlblockId, 'LID' => 'ru'),
        array('NAME' => 'Демо-каталог')
    );
}

Шаблон пользовательского поля

Универсальный массив для CUserTypeEntity::Add():

$userType = new \CUserTypeEntity();

$fieldId = $userType->Add(array(
    'ENTITY_ID' => $entityId,
    'FIELD_NAME' => 'UF_CODE',
    'USER_TYPE_ID' => 'string',
    'XML_ID' => 'UF_CODE',           // для импорта/экспорта
    'SORT' => 100,
    'MULTIPLE' => 'N',               // N | Y
    'MANDATORY' => 'N',              // N | Y
    'SHOW_FILTER' => 'I',            // N | I | E | S
    'SHOW_IN_LIST' => 'Y',           // Y | N
    'EDIT_IN_LIST' => 'Y',             // Y | N
    'IS_SEARCHABLE' => 'N',          // Y | N
    'SETTINGS' => array(
        // зависят от USER_TYPE_ID — см. справочник ниже
    ),
    'EDIT_FORM_LABEL' => array('ru' => 'Символьный код', 'en' => 'Code'),
    'LIST_COLUMN_LABEL' => array('ru' => 'Код', 'en' => 'Code'),
    'LIST_FILTER_LABEL' => array('ru' => 'Код', 'en' => 'Code'),
    'ERROR_MESSAGE' => array('ru' => '', 'en' => ''),
    'HELP_MESSAGE' => array('ru' => 'Уникальный код элемента', 'en' => ''),
));

if (!$fieldId) {
    global $APPLICATION;
    throw new \RuntimeException($APPLICATION->GetException()
        ? $APPLICATION->GetException()->GetString()
        : 'Не удалось создать поле UF_CODE');
}

Подробнее о подписях и флагах редактирования — в разделах ниже.


Подписи полей в разных представлениях

При создании поля через CUserTypeEntity::Add() / Update() можно задать разные подписи для разных мест админки. Значения хранятся в b_user_field_lang (по одной строке на язык сайта).

Ключ в API Где используется Fallback, если пусто
EDIT_FORM_LABEL Подпись поля в форме редактирования элемента HL FIELD_NAME
LIST_COLUMN_LABEL Заголовок колонки в таблице списка элементов FIELD_NAME
LIST_FILTER_LABEL Подпись поля в форме фильтра списка FIELD_NAME
ERROR_MESSAGE Текст ошибки при неуспешной валидации значения
HELP_MESSAGE Подсказка (hint) рядом с полем в форме редактирования

Каждый ключ — массив сообщений по языкам: array('ru' => '...', 'en' => '...'). Язык подставляется из текущего контекста админки ($GLOBALS['lang']).

Пример: разные подписи в форме, списке и фильтре

$userType->Add(array(
    'ENTITY_ID' => $entityId,
    'FIELD_NAME' => 'UF_TITLE',
    'USER_TYPE_ID' => 'string',
    'SORT' => 100,
    'SETTINGS' => array(
        'SIZE' => 80,
        'ROWS' => 1,
        'DEFAULT_VALUE' => '',
    ),
    // Длинная подпись в форме
    'EDIT_FORM_LABEL' => array('ru' => 'Полное наименование элемента'),
    // Короткий заголовок колонки в таблице
    'LIST_COLUMN_LABEL' => array('ru' => 'Название'),
    // Отдельная формулировка в фильтре
    'LIST_FILTER_LABEL' => array('ru' => 'Искать по названию'),
    // Подсказка под полем
    'HELP_MESSAGE' => array('ru' => 'Отображается в публичной части сайта'),
    // Сообщение при ошибке валидации (если тип/настройки его поддерживают)
    'ERROR_MESSAGE' => array('ru' => 'Укажите корректное наименование'),
));

Обновление подписей у существующего поля

Подписи можно менять через CUserTypeEntity::Update() — передавайте только нужные языковые ключи:

$userType = new \CUserTypeEntity();

$userType->Update($fieldId, array(
    'LIST_COLUMN_LABEL' => array('ru' => 'Краткое имя'),
    'EDIT_FORM_LABEL' => array('ru' => 'Полное имя для редактирования'),
));

В админке Bitrix эти поля соответствуют блоку «Языковые настройки» на странице редактирования пользовательского поля (userfield_edit.php).


Видимость и редактирование поля

Помимо MANDATORY (обязательность заполнения) API позволяет управлять отображением поля в списке и возможностью редактирования пользователем в админке.

Ключ Значения По умолчанию Что делает
SHOW_IN_LIST Y / N Y Показывать ли колонку поля в списке элементов HL
EDIT_IN_LIST Y / N Y Разрешать ли редактирование значения пользователем в админке
SHOW_FILTER N / I / E / S N Показывать ли поле в фильтре списка и тип сравнения
MANDATORY Y / N N Обязательно ли заполнять поле при сохранении
IS_SEARCHABLE Y / N N Участвует ли поле в поиске по сущности

SHOW_IN_LIST — колонка в таблице

  • Y — поле попадает в заголовки и строки списка (AdminListAddHeaders, AddUserFields).
  • N — колонка скрыта в списке, но поле может отображаться в форме редактирования элемента.

В форме настройки поля в админке соответствует чекбоксу «Не показывать в списке» (отмечен = N).

EDIT_IN_LIST — можно ли менять значение

  • Y — поле редактируется в форме элемента и (если тип поддерживает) в режиме правки строки списка.
  • N — в админке поле только для просмотра:
    • при сохранении формы значение из $_POST / $_REQUEST игнорируется (EditFormAddFields);
    • в форме подставляется текущее значение из БД, а не пришедшее из запроса;
    • в списке отображается режим просмотра, без inline-редактирования (getadminlistedithtml не вызывается);
    • в современных гридах (main/lib/grid/uf/) поле помечается как нередактируемое.

В админке соответствует чекбоксу «Не разрешать редактирование пользователем» (отмечен = N).

Важно: EDIT_IN_LIST ограничивает редактирование только в интерфейсе админки. Запись через ORM / DataManager::add() / update() / API не блокируется — это контролируется отдельно на уровне кода.

Пример: поле только для чтения в админке

Типичный сценарий — системное поле, заполняемое агентом или импортом:

$userType->Add(array(
    'ENTITY_ID' => $entityId,
    'FIELD_NAME' => 'UF_EXTERNAL_ID',
    'USER_TYPE_ID' => 'string',
    'SORT' => 900,
    'SHOW_IN_LIST' => 'Y',    // видно в таблице
    'EDIT_IN_LIST' => 'N',    // пользователь не может изменить вручную
    'SHOW_FILTER' => 'I',     // можно искать по точному совпадению
    'SETTINGS' => array(
        'SIZE' => 30,
        'ROWS' => 1,
        'DEFAULT_VALUE' => '',
    ),
    'EDIT_FORM_LABEL' => array('ru' => 'Внешний ID (только чтение)'),
    'LIST_COLUMN_LABEL' => array('ru' => 'Ext. ID'),
    'LIST_FILTER_LABEL' => array('ru' => 'Внешний ID'),
    'HELP_MESSAGE' => array('ru' => 'Заполняется автоматически при синхронизации'),
));

Пример: скрытое служебное поле

Поле есть в форме и доступно программно, но не засоряет список:

$userType->Add(array(
    'ENTITY_ID' => $entityId,
    'FIELD_NAME' => 'UF_INTERNAL_NOTE',
    'USER_TYPE_ID' => 'string',
    'SHOW_IN_LIST' => 'N',    // не показывать колонку
    'EDIT_IN_LIST' => 'Y',    // но редактировать в форме можно
    'SHOW_FILTER' => 'N',
    'SETTINGS' => array('SIZE' => 60, 'ROWS' => 3, 'DEFAULT_VALUE' => ''),
    'EDIT_FORM_LABEL' => array('ru' => 'Внутренняя заметка'),
));

Сводка: что куда влияет

flowchart LR
    subgraph labels [Подписи b_user_field_lang]
        A[EDIT_FORM_LABEL] --> B[Форма элемента]
        C[LIST_COLUMN_LABEL] --> D[Таблица списка]
        E[LIST_FILTER_LABEL] --> F[Фильтр списка]
        G[HELP_MESSAGE] --> B
    end
    subgraph flags [Флаги b_user_field]
        H[SHOW_IN_LIST] --> D
        I[EDIT_IN_LIST] --> B
        I --> D
        J[SHOW_FILTER] --> F
    end

Справочник типов полей и SETTINGS

Стандартные типы модуля main, регистрируемые через OnUserTypeBuildList:

USER_TYPE_ID BASE_TYPE Описание
string string Строка / многострочный текст (через ROWS)
integer int Целое число
double double Число с плавающей точкой
boolean int Да/Нет (0/1)
enumeration enum Список
date datetime Дата
datetime datetime Дата и время
file file Файл
url string Ссылка
string_formatted string Шаблонное выражение
hlblock int Привязка к элементу HL-блока (модуль highloadblock)

Типы iblock_element и iblock_section тоже есть в ядре, но для HL-блоков в миграциях обычно используют hlblock.

string — строка / текст

'USER_TYPE_ID' => 'string',
'SETTINGS' => array(
    'SIZE' => 50,              // ширина input (20–255, по умолчанию 20)
    'ROWS' => 1,               // 1 = однострочное; >1 = textarea (до 50)
    'REGEXP' => '',            // регулярное выражение для валидации
    'MIN_LENGTH' => 0,
    'MAX_LENGTH' => 0,         // 0 = без ограничения
    'DEFAULT_VALUE' => '',
),

Многострочный «текст» — тот же тип string с ROWS > 1:

'FIELD_NAME' => 'UF_DESCRIPTION',
'USER_TYPE_ID' => 'string',
'SETTINGS' => array(
    'SIZE' => 80,
    'ROWS' => 5,
    'DEFAULT_VALUE' => '',
),

integer — целое число

'USER_TYPE_ID' => 'integer',
'SETTINGS' => array(
    'SIZE' => 20,
    'MIN_VALUE' => 0,          // 0 = не проверять
    'MAX_VALUE' => 0,
    'DEFAULT_VALUE' => null,
),

double — дробное число

'USER_TYPE_ID' => 'double',
'SETTINGS' => array(
    'PRECISION' => 2,          // знаков после запятой (0–12)
    'SIZE' => 20,
    'MIN_VALUE' => 0,
    'MAX_VALUE' => 0,
    'DEFAULT_VALUE' => null,
),

boolean — да/нет

'USER_TYPE_ID' => 'boolean',
'SETTINGS' => array(
    'DEFAULT_VALUE' => 0,      // 0 или 1
    'DISPLAY' => 'CHECKBOX',   // CHECKBOX | RADIO | DROPDOWN
    'LABEL' => array('Нет', 'Да'),
    'LABEL_CHECKBOX' => '',
),

boolean не поддерживает MULTIPLE и MANDATORY на уровне типа.

date — дата

'USER_TYPE_ID' => 'date',
'SETTINGS' => array(
    'DEFAULT_VALUE' => array(
        'TYPE' => 'NONE',      // NONE | FIXED | NOW
        'VALUE' => '',         // для FIXED: '2026-06-11'
    ),
),

datetime — дата и время

'USER_TYPE_ID' => 'datetime',
'SETTINGS' => array(
    'DEFAULT_VALUE' => array(
        'TYPE' => 'NONE',      // NONE | FIXED | NOW
        'VALUE' => '',         // для FIXED: '2026-06-11 12:00:00'
    ),
    'USE_SECOND' => 'Y',       // Y | N
    'USE_TIMEZONE' => 'N',     // Y | N
),

file — файл

'USER_TYPE_ID' => 'file',
'SETTINGS' => array(
    'SIZE' => 20,
    'LIST_WIDTH' => 0,
    'LIST_HEIGHT' => 0,
    'MAX_SHOW_SIZE' => 0,
    'MAX_ALLOWED_SIZE' => 0,   // байты, 0 = без ограничения
    'EXTENSIONS' => array(      // разрешённые расширения
        'jpg' => true,
        'png' => true,
        'pdf' => true,
    ),
    'TARGET_BLANK' => 'Y',
    'DEFAULT_VIEW' => null,    // tile | list | adaptive
),

url — ссылка

'USER_TYPE_ID' => 'url',
'SETTINGS' => array(
    'POPUP' => 'Y',
    'SIZE' => 50,
    'MIN_LENGTH' => 0,
    'MAX_LENGTH' => 0,
    'ROWS' => 1,
    'DEFAULT_VALUE' => '',
),

string_formatted — шаблонное выражение

'USER_TYPE_ID' => 'string_formatted',
'SETTINGS' => array(
    'SIZE' => 50,
    'ROWS' => 1,
    'REGEXP' => '',
    'MIN_LENGTH' => 0,
    'MAX_LENGTH' => 0,
    'DEFAULT_VALUE' => '',
    'PATTERN' => 'Товар: #NAME#',  // шаблон отображения
),

enumeration — список (метаданные поля)

'USER_TYPE_ID' => 'enumeration',
'SETTINGS' => array(
    'DISPLAY' => 'LIST',       // LIST | CHECKBOX | UI | DIALOG
    'LIST_HEIGHT' => 5,
    'CAPTION_NO_VALUE' => '',  // подпись пустого значения
    'SHOW_NO_VALUE' => 'Y',    // Y | N
),

Значения списка добавляются отдельно — см. следующий раздел.

hlblock — привязка к HL-блоку

'USER_TYPE_ID' => 'hlblock',
'SETTINGS' => array(
    'DISPLAY' => 'LIST',       // LIST | CHECKBOX | UI | DIALOG
    'LIST_HEIGHT' => 5,
    'HLBLOCK_ID' => 3,         // ID целевого HL-блока
    'HLFIELD_ID' => 0,         // ID поля для подписи; 0 = ID элемента
    'DEFAULT_VALUE' => '',     // ID элемента или массив ID для MULTIPLE
),

Список (enumeration) и CUserFieldEnum

После CUserTypeEntity::Add() для типа enumeration нужно добавить варианты:

$enum = new \CUserFieldEnum();

$enum->SetEnumValues($fieldId, array(
    'n0' => array(
        'VALUE' => 'Новый',
        'DEF' => 'Y',          // значение по умолчанию
        'SORT' => 100,
        'XML_ID' => 'NEW',
    ),
    'n1' => array(
        'VALUE' => 'В работе',
        'DEF' => 'N',
        'SORT' => 200,
        'XML_ID' => 'IN_PROGRESS',
    ),
    'n2' => array(
        'VALUE' => 'Закрыт',
        'DEF' => 'N',
        'SORT' => 300,
        'XML_ID' => 'CLOSED',
    ),
));

Правила:

  • новые значения — ключи n0, n1, …;
  • XML_ID должен быть уникален в рамках поля (если пустой — генерируется автоматически);
  • для обновления существующих — ключ = числовой ID записи в b_user_field_enum;
  • удаление — 'DEL' => 'Y' или пустой VALUE.

Привязка к другому HL-блоку (hlblock)

Поле hlblock хранит ID элемента целевого HL-блока. В ORM автоматически создаётся reference {FIELD_NAME}_REF.

Типичный порядок в миграции:

  1. Создать «справочный» HL-блок (например, статусы).
  2. Создать основной HL-блок.
  3. В SETTINGS поля hlblock указать HLBLOCK_ID справочника.
// После создания справочника DemoStatus (ID = 5)
'USER_TYPE_ID' => 'hlblock',
'SETTINGS' => array(
    'DISPLAY' => 'LIST',
    'LIST_HEIGHT' => 1,
    'HLBLOCK_ID' => 5,
    'HLFIELD_ID' => 0,
    'DEFAULT_VALUE' => '',
),

Множественные поля

Установите 'MULTIPLE' => 'Y'. При добавлении поля ядро:

  • для HL создаёт UTM-таблицу {table_name}_{field_name_lower};
  • в основной таблице хранит сериализованный кэш значений.

Пример множественного списка:

$userType->Add(array(
    'ENTITY_ID' => $entityId,
    'FIELD_NAME' => 'UF_TAGS',
    'USER_TYPE_ID' => 'string',
    'MULTIPLE' => 'Y',
    'MANDATORY' => 'N',
    'SETTINGS' => array(
        'SIZE' => 50,
        'ROWS' => 1,
        'DEFAULT_VALUE' => '',
    ),
    'EDIT_FORM_LABEL' => array('ru' => 'Теги'),
));

При сохранении через ORM/DataManager передавайте массив значений.

boolean не поддерживает множественность.


Откат миграции

Удалить одно поле

$rs = \CUserTypeEntity::GetList(array(), array(
    'ENTITY_ID' => $entityId,
    'FIELD_NAME' => 'UF_CODE',
));

if ($field = $rs->Fetch()) {
    $userType = new \CUserTypeEntity();
    $userType->Delete((int)$field['ID']);
}

Удалить весь HL-блок

HighloadBlockTable::delete() каскадно:

  • удаляет все UF-поля и их enum-значения;
  • удаляет файлы из file-полей;
  • удаляет UTM-таблицы множественных полей;
  • удаляет языковые записи и права;
  • удаляет физическую таблицу HL-блока.
$hlblock = HighloadBlockTable::query()
    ->addSelect('ID')
    ->where('NAME', 'DemoCatalog')
    ->exec()
    ->fetch();

if ($hlblock) {
    $result = HighloadBlockTable::delete((int)$hlblock['ID']);
    if (!$result->isSuccess()) {
        throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
    }
}

Работа с данными через ORM

После миграции компилируйте entity и работайте через DataManager:

use Bitrix\Highloadblock\HighloadBlockTable;

$hlblock = HighloadBlockTable::resolveHighloadblock('DemoCatalog');
$entity = HighloadBlockTable::compileEntity($hlblock);
$dataClass = $entity->getDataClass();

// Добавить элемент
$addResult = $dataClass::add(array(
    'UF_NAME' => 'Пример',
    'UF_CODE' => 'example',
    'UF_ACTIVE' => 1,
));

if (!$addResult->isSuccess()) {
    throw new \RuntimeException(implode('; ', $addResult->getErrorMessages()));
}

// Выборка
$rows = $dataClass::getList(array(
    'select' => array('ID', 'UF_NAME', 'UF_CODE'),
    'filter' => array('=UF_ACTIVE' => 1),
    'order' => array('ID' => 'DESC'),
    'limit' => 10,
))->fetchAll();

Частые ошибки

Ошибка Причина Решение
NAME не проходит валидацию Имя с маленькой буквы или с _ Используйте DemoCatalog, не demo_catalog
TABLE_NAME already exists Таблица осталась в БД после неудачного отката Удалите таблицу вручную или выберите другое имя
Поле не создаётся FIELD_NAME без префикса UF_ Всегда UF_SOMETHING
_REF reserved Суффикс зарезервирован ORM Переименуйте поле
Enum пустой в админке Забыли SetEnumValues Добавьте значения после Add()
hlblock не показывает элементы Неверный HLBLOCK_ID Проверьте ID целевого блока
Повторный запуск падает Нет идемпотентности Перед add проверяйте существование по NAME / FIELD_NAME
EDIT_IN_LIST=N, но ORM всё равно пишет Флаг действует только в админке Для программной защиты проверяйте значение в своём коде
В списке и форме одна подпись Заполнен только EDIT_FORM_LABEL Задайте отдельно LIST_COLUMN_LABEL и LIST_FILTER_LABEL