Как устроено создание 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();
Что происходит внутри:
- Запись попадает в
b_hlblock_entity. - В БД создаётся физическая таблица (
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.
Типичный порядок в миграции:
- Создать «справочный» HL-блок (например, статусы).
- Создать основной HL-блок.
- В
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
|