<?php
/**
* Демо-миграция Highload-блока со всеми стандартными типами пользовательских полей.
*
* Использование (после prolog_before.php):
* $migration = new DemoHlMigration();
* $migration->up();
* // $migration->down();
*
* CLI: php /opt/www/local/migrations/run-demo-hl-migration.php up
*
*/
use Bitrix\Highloadblock\HighloadBlockLangTable;
use Bitrix\Highloadblock\HighloadBlockTable;
use Bitrix\Main\Loader;
class DemoHlMigration
{
/** Символьный код HL-блока (ORM entity name) */
private const HL_NAME = 'DemoHlMigration';
/** Имя физической таблицы в БД */
private const HL_TABLE = 'demo_hl_migration';
/** Справочный HL-блок для поля типа hlblock */
private const REF_HL_NAME = 'DemoHlMigrationRef';
private const REF_HL_TABLE = 'demo_hl_migration_ref';
/**
* Применяет миграцию: создаёт HL-блоки и все стандартные UF-поля.
*
* @return void
* @throws \RuntimeException
*/
public function up(): void
{
$this->ensureModules();
$refHlblockId = $this->createHlblock(self::REF_HL_NAME, self::REF_HL_TABLE, 'Справочник для привязки');
$hlblockId = $this->createHlblock(self::HL_NAME, self::HL_TABLE, 'Демо HL-блок (все типы полей)');
$refEntityId = HighloadBlockTable::compileEntityId($refHlblockId);
$entityId = HighloadBlockTable::compileEntityId($hlblockId);
$this->addRefFields($refEntityId);
$this->addAllFields($entityId, $refHlblockId);
}
/**
* Откатывает миграцию: удаляет HL-блоки каскадно.
*
* @return void
* @throws \RuntimeException
*/
public function down(): void
{
$this->ensureModules();
$this->deleteHlblockByName(self::HL_NAME);
$this->deleteHlblockByName(self::REF_HL_NAME);
}
/**
* Подключает необходимые модули.
*
* @return void
* @throws \RuntimeException|\Bitrix\Main\LoaderException
*/
private function ensureModules(): void
{
if (!Loader::includeModule('highloadblock')) {
throw new \RuntimeException('Модуль highloadblock не установлен');
}
}
/**
* Создаёт HL-блок или возвращает ID существующего (идемпотентность).
*
* @param string $name Символьный код (DemoCatalog)
* @param string $tableName Имя таблицы (demo_catalog)
* @param string $langName Русское название для админки
*
* @return int
* @throws \RuntimeException
*/
private function createHlblock(string $name, string $tableName, string $langName): int
{
$existing = HighloadBlockTable::query()
->addSelect('ID')
->where('NAME', $name)
->exec()
->fetch()
;
if ($existing) {
$hlblockId = (int) $existing['ID'];
} else {
$result = HighloadBlockTable::add([
'NAME' => $name,
'TABLE_NAME' => $tableName,
]);
if (!$result->isSuccess()) {
throw new \RuntimeException(
'Ошибка создания HL-блока ' . $name . ': ' . implode('; ', $result->getErrorMessages())
);
}
$hlblockId = (int) $result->getId();
}
$this->saveLangName($hlblockId, $langName);
return $hlblockId;
}
/**
* Сохраняет или обновляет русское название HL-блока.
*
* @param int $hlblockId
* @param string $name
*
* @return void
*/
private function saveLangName(int $hlblockId, string $name): void
{
$langRow = HighloadBlockLangTable::getByPrimary([
'ID' => $hlblockId,
'LID' => 'ru',
])->fetch();
if ($langRow) {
HighloadBlockLangTable::update(
['ID' => $hlblockId, 'LID' => 'ru'],
['NAME' => $name]
);
} else {
HighloadBlockLangTable::add([
'ID' => $hlblockId,
'LID' => 'ru',
'NAME' => $name,
]);
}
}
/**
* Добавляет минимальные поля в справочный HL-блок.
*
* @param string $entityId
*
* @return void
*/
private function addRefFields(string $entityId): void
{
$this->addUserField($entityId, [
'FIELD_NAME' => 'UF_TITLE',
'USER_TYPE_ID' => 'string',
'SORT' => 100,
'SETTINGS' => [
'SIZE' => 50,
'ROWS' => 1,
'DEFAULT_VALUE' => '',
],
'EDIT_FORM_LABEL' => ['ru' => 'Название'],
'LIST_COLUMN_LABEL' => ['ru' => 'Название'],
]);
}
/**
* Добавляет все стандартные типы пользовательских полей.
*
* @param string $entityId
* @param int $refHlblockId ID справочного HL-блока для типа hlblock
*
* @return void
*/
private function addAllFields(string $entityId, int $refHlblockId): void
{
$this->addUserField($entityId, [
'FIELD_NAME' => 'UF_STRING',
'USER_TYPE_ID' => 'string',
'SORT' => 100,
'MANDATORY' => 'Y',
'SHOW_FILTER' => 'S',
'IS_SEARCHABLE' => 'Y',
'SETTINGS' => [
'SIZE' => 50,
'ROWS' => 1,
'REGEXP' => '',
'MIN_LENGTH' => 1,
'MAX_LENGTH' => 255,
'DEFAULT_VALUE' => '',
],
'EDIT_FORM_LABEL' => ['ru' => 'Строка'],
'LIST_COLUMN_LABEL' => ['ru' => 'Строка'],
'LIST_FILTER_LABEL' => ['ru' => 'Строка'],
'HELP_MESSAGE' => ['ru' => 'Тип string, однострочный ввод'],
]);
$this->addUserField($entityId, [
'FIELD_NAME' => 'UF_TEXT',
'USER_TYPE_ID' => 'string',
'SORT' => 110,
'SETTINGS' => [
'SIZE' => 80,
'ROWS' => 5,
'MIN_LENGTH' => 0,
'MAX_LENGTH' => 0,
'DEFAULT_VALUE' => '',
],
'EDIT_FORM_LABEL' => ['ru' => 'Текст'],
'LIST_COLUMN_LABEL' => ['ru' => 'Текст'],
]);
$this->addUserField($entityId, [
'FIELD_NAME' => 'UF_INTEGER',
'USER_TYPE_ID' => 'integer',
'SORT' => 120,
'SHOW_FILTER' => 'I',
'SETTINGS' => [
'SIZE' => 20,
'MIN_VALUE' => 0,
'MAX_VALUE' => 999999,
'DEFAULT_VALUE' => null,
],
'EDIT_FORM_LABEL' => ['ru' => 'Целое число'],
'LIST_COLUMN_LABEL' => ['ru' => 'Число'],
]);
$this->addUserField($entityId, [
'FIELD_NAME' => 'UF_DOUBLE',
'USER_TYPE_ID' => 'double',
'SORT' => 130,
'SHOW_FILTER' => 'I',
'SETTINGS' => [
'PRECISION' => 2,
'SIZE' => 20,
'MIN_VALUE' => 0,
'MAX_VALUE' => 0,
'DEFAULT_VALUE' => null,
],
'EDIT_FORM_LABEL' => ['ru' => 'Дробное число'],
'LIST_COLUMN_LABEL' => ['ru' => 'Дробное'],
]);
$this->addUserField($entityId, [
'FIELD_NAME' => 'UF_BOOLEAN',
'USER_TYPE_ID' => 'boolean',
'SORT' => 140,
'SHOW_FILTER' => 'I',
'SETTINGS' => [
'DEFAULT_VALUE' => 0,
'DISPLAY' => 'CHECKBOX',
'LABEL' => ['Нет', 'Да'],
'LABEL_CHECKBOX' => 'Активен',
],
'EDIT_FORM_LABEL' => ['ru' => 'Флаг'],
'LIST_COLUMN_LABEL' => ['ru' => 'Активен'],
]);
$enumFieldId = $this->addUserField($entityId, [
'FIELD_NAME' => 'UF_ENUM',
'USER_TYPE_ID' => 'enumeration',
'SORT' => 150,
'SHOW_FILTER' => 'I',
'SETTINGS' => [
'DISPLAY' => 'LIST',
'LIST_HEIGHT' => 5,
'CAPTION_NO_VALUE' => '— не выбрано —',
'SHOW_NO_VALUE' => 'Y',
],
'EDIT_FORM_LABEL' => ['ru' => 'Список'],
'LIST_COLUMN_LABEL' => ['ru' => 'Статус'],
]);
if ($enumFieldId) {
$this->addEnumValues($enumFieldId);
}
$this->addUserField($entityId, [
'FIELD_NAME' => 'UF_DATE',
'USER_TYPE_ID' => 'date',
'SORT' => 160,
'SHOW_FILTER' => 'I',
'SETTINGS' => [
'DEFAULT_VALUE' => [
'TYPE' => 'NONE',
'VALUE' => '',
],
],
'EDIT_FORM_LABEL' => ['ru' => 'Дата'],
'LIST_COLUMN_LABEL' => ['ru' => 'Дата'],
]);
$this->addUserField($entityId, [
'FIELD_NAME' => 'UF_DATETIME',
'USER_TYPE_ID' => 'datetime',
'SORT' => 170,
'SHOW_FILTER' => 'I',
'SETTINGS' => [
'DEFAULT_VALUE' => [
'TYPE' => 'NONE',
'VALUE' => '',
],
'USE_SECOND' => 'Y',
'USE_TIMEZONE' => 'N',
],
'EDIT_FORM_LABEL' => ['ru' => 'Дата и время'],
'LIST_COLUMN_LABEL' => ['ru' => 'Дата/время'],
]);
$this->addUserField($entityId, [
'FIELD_NAME' => 'UF_FILE',
'USER_TYPE_ID' => 'file',
'SORT' => 180,
'SETTINGS' => [
'SIZE' => 20,
'LIST_WIDTH' => 200,
'LIST_HEIGHT' => 200,
'MAX_SHOW_SIZE' => 0,
'MAX_ALLOWED_SIZE' => 0,
'EXTENSIONS' => [
'jpg' => true,
'jpeg' => true,
'png' => true,
'gif' => true,
'pdf' => true,
],
'TARGET_BLANK' => 'Y',
],
'EDIT_FORM_LABEL' => ['ru' => 'Файл'],
'LIST_COLUMN_LABEL' => ['ru' => 'Файл'],
]);
$this->addUserField($entityId, [
'FIELD_NAME' => 'UF_URL',
'USER_TYPE_ID' => 'url',
'SORT' => 190,
'SETTINGS' => [
'POPUP' => 'Y',
'SIZE' => 50,
'MIN_LENGTH' => 0,
'MAX_LENGTH' => 0,
'ROWS' => 1,
'DEFAULT_VALUE' => '',
],
'EDIT_FORM_LABEL' => ['ru' => 'Ссылка'],
'LIST_COLUMN_LABEL' => ['ru' => 'Ссылка'],
]);
$this->addUserField($entityId, [
'FIELD_NAME' => 'UF_STRING_FORMATTED',
'USER_TYPE_ID' => 'string_formatted',
'SORT' => 200,
'SETTINGS' => [
'SIZE' => 50,
'ROWS' => 1,
'REGEXP' => '',
'MIN_LENGTH' => 0,
'MAX_LENGTH' => 0,
'DEFAULT_VALUE' => '',
'PATTERN' => '#VALUE#',
],
'EDIT_FORM_LABEL' => ['ru' => 'Шаблонное выражение'],
'LIST_COLUMN_LABEL' => ['ru' => 'Шаблон'],
]);
$this->addUserField($entityId, [
'FIELD_NAME' => 'UF_HLBLOCK',
'USER_TYPE_ID' => 'hlblock',
'SORT' => 210,
'SHOW_FILTER' => 'I',
'SETTINGS' => [
'DISPLAY' => 'LIST',
'LIST_HEIGHT' => 5,
'HLBLOCK_ID' => $refHlblockId,
'HLFIELD_ID' => 0,
'DEFAULT_VALUE' => '',
],
'EDIT_FORM_LABEL' => ['ru' => 'Привязка к HL'],
'LIST_COLUMN_LABEL' => ['ru' => 'HL-элемент'],
]);
$this->addUserField($entityId, [
'FIELD_NAME' => 'UF_STRING_MULTI',
'USER_TYPE_ID' => 'string',
'SORT' => 300,
'MULTIPLE' => 'Y',
'SETTINGS' => [
'SIZE' => 40,
'ROWS' => 1,
'DEFAULT_VALUE' => '',
],
'EDIT_FORM_LABEL' => ['ru' => 'Теги (множественное)'],
'LIST_COLUMN_LABEL' => ['ru' => 'Теги'],
]);
$enumMultiFieldId = $this->addUserField($entityId, [
'FIELD_NAME' => 'UF_ENUM_MULTI',
'USER_TYPE_ID' => 'enumeration',
'SORT' => 310,
'MULTIPLE' => 'Y',
'SETTINGS' => [
'DISPLAY' => 'CHECKBOX',
'LIST_HEIGHT' => 5,
'SHOW_NO_VALUE' => 'N',
],
'EDIT_FORM_LABEL' => ['ru' => 'Категории (множественный список)'],
'LIST_COLUMN_LABEL' => ['ru' => 'Категории'],
]);
if ($enumMultiFieldId) {
$this->addEnumMultiValues($enumMultiFieldId);
}
}
/**
* Добавляет пользовательское поле, если его ещё нет.
*
* @param string $entityId
* @param array $fieldData
*
* @return int|null ID созданного поля или null если уже существует
* @throws \RuntimeException
*/
private function addUserField(string $entityId, array $fieldData): ?int
{
$fieldName = $fieldData['FIELD_NAME'];
$rs = \CUserTypeEntity::GetList([], [
'ENTITY_ID' => $entityId,
'FIELD_NAME' => $fieldName,
]);
if ($rs->Fetch()) {
return null;
}
$defaults = [
'ENTITY_ID' => $entityId,
'XML_ID' => $fieldName,
'SORT' => 500,
'MULTIPLE' => 'N',
'MANDATORY' => 'N',
'SHOW_FILTER' => 'N',
'SHOW_IN_LIST' => 'Y',
'EDIT_IN_LIST' => 'Y',
'IS_SEARCHABLE' => 'N',
'SETTINGS' => [],
'EDIT_FORM_LABEL' => ['ru' => $fieldName],
'LIST_COLUMN_LABEL' => ['ru' => $fieldName],
'LIST_FILTER_LABEL' => ['ru' => $fieldName],
'ERROR_MESSAGE' => ['ru' => ''],
'HELP_MESSAGE' => ['ru' => ''],
];
$payload = array_merge($defaults, $fieldData);
$payload['ENTITY_ID'] = $entityId;
$userType = new \CUserTypeEntity();
$fieldId = $userType->Add($payload);
if (!$fieldId) {
global $APPLICATION;
$error = $APPLICATION->GetException()
? $APPLICATION->GetException()->GetString()
: 'Неизвестная ошибка';
throw new \RuntimeException(
'Не удалось создать поле ' . $fieldName . ': ' . $error
);
}
return (int) $fieldId;
}
/**
* Добавляет значения для одиночного списка UF_ENUM.
*
* @param int $fieldId
*
* @return void
*/
private function addEnumValues(int $fieldId): void
{
$enum = new \CUserFieldEnum();
$enum->SetEnumValues($fieldId, [
'n0' => [
'VALUE' => 'Новый',
'DEF' => 'Y',
'SORT' => 100,
'XML_ID' => 'NEW',
],
'n1' => [
'VALUE' => 'В работе',
'DEF' => 'N',
'SORT' => 200,
'XML_ID' => 'IN_PROGRESS',
],
'n2' => [
'VALUE' => 'Закрыт',
'DEF' => 'N',
'SORT' => 300,
'XML_ID' => 'CLOSED',
],
]);
}
/**
* Добавляет значения для множественного списка UF_ENUM_MULTI.
*
* @param int $fieldId
*
* @return void
*/
private function addEnumMultiValues(int $fieldId): void
{
$enum = new \CUserFieldEnum();
$enum->SetEnumValues($fieldId, [
'n0' => [
'VALUE' => 'Категория A',
'DEF' => 'N',
'SORT' => 100,
'XML_ID' => 'CAT_A',
],
'n1' => [
'VALUE' => 'Категория B',
'DEF' => 'N',
'SORT' => 200,
'XML_ID' => 'CAT_B',
],
'n2' => [
'VALUE' => 'Категория C',
'DEF' => 'N',
'SORT' => 300,
'XML_ID' => 'CAT_C',
],
]);
}
/**
* Удаляет HL-блок по символьному коду NAME.
*
* @param string $name
*
* @return void
* @throws \RuntimeException
*/
private function deleteHlblockByName(string $name): void
{
$hlblock = HighloadBlockTable::query()
->addSelect('ID')
->where('NAME', $name)
->exec()
->fetch()
;
if (!$hlblock) {
return;
}
$result = HighloadBlockTable::delete((int) $hlblock['ID']);
if (!$result->isSuccess()) {
throw new \RuntimeException(
'Ошибка удаления HL-блока ' . $name . ': ' . implode('; ', $result->getErrorMessages())
);
}
}
}
Пример запуска
<?php
/**
* CLI-runner для DemoHlMigration.
*
* docker compose exec php php /opt/www/local/migrations/run-demo-hl-migration.php up
* docker compose exec php php /opt/www/local/migrations/run-demo-hl-migration.php down
*/
$_SERVER['DOCUMENT_ROOT'] = realpath(__DIR__ . '/../..');
define('NO_KEEP_STATISTIC', true);
define('NOT_CHECK_PERMISSIONS', true);
define('BX_NO_ACCELERATOR_RESET', true);
require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';
require __DIR__ . '/demo-hl-migration.php';
$action = $argv[1] ?? 'up';
try {
$migration = new DemoHlMigration();
if ($action === 'down') {
$migration->down();
echo "OK: down()\n";
exit(0);
}
$migration->up();
echo "OK: up()\n";
$blocks = \Bitrix\Highloadblock\HighloadBlockTable::getList(array(
'filter' => array(
'@NAME' => array('DemoHlMigration', 'DemoHlMigrationRef'),
),
'select' => array('ID', 'NAME', 'TABLE_NAME'),
'order' => array('ID' => 'ASC'),
))->fetchAll();
foreach ($blocks as $block) {
$entityId = \Bitrix\Highloadblock\HighloadBlockTable::compileEntityId((int)$block['ID']);
$fields = \CUserTypeEntity::GetList(
array('SORT' => 'ASC'),
array('ENTITY_ID' => $entityId)
);
$fieldCount = 0;
while ($fields->Fetch()) {
$fieldCount++;
}
echo sprintf(
"HL ID=%d NAME=%s TABLE=%s fields=%d\n",
(int)$block['ID'],
$block['NAME'],
$block['TABLE_NAME'],
$fieldCount
);
}
exit(0);
} catch (\Throwable $e) {
fwrite(STDERR, 'ERROR: ' . $e->getMessage() . "\n");
exit(1);
}