Highload Blocks (highloadblock)
SkillDev toolsCovers Highloadblock module — HighloadBlockTable manage API (add/update/delete, lang names), compileEntity() and dynamic DataManager CRUD, user fields (HLBLOCK_{id}), hlblock UF relations and _REF, directory iblock property (UF_XML_ID), ORM events, rights operations (hl_element_*), highloadblock.list/view components, performance (cache.ttl, batch by ID, indexes). Key terms — highloadblock, HighloadBlockTable, compileEntity, DataManager, HLBLOCK_, directory, UF_XML_ID, hl_element_read.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Highload Blocks (highloadblock) skill
What this skill tells your AI
The instructions your AI receives, as published by bxmaximum/bitrix-framework-skills in skills/bitrix-highloadblock/SKILL.md and read by ahel’s review.
Highload blocks (HL) are ORM-backed flat tables whose columns are user fields (UF_*). No sections/tree, no iblock SEO model. The name does not guarantee performance — it depends on fields, indexes, filters and volume.
\Bitrix\Main\Loader::includeModule('highloadblock'); // always before API use
When HL vs Iblock vs Custom Tablet
| Need | Prefer |
|---|---|
| Flat dictionary / reference list, admin-editable UF structure, iblock "directory" source | Highload block |
| Sections, SEO, properties, public content UX | Iblock (bitrix-iblocks) |
| Schema fully owned by code, migrations, typed checks | Custom ORM tablet (bitrix-orm) |
Core Concepts
| Term | Meaning |
|---|---|
| HL block | Description row: ID, NAME, TABLE_NAME (+ computed FIELDS_COUNT, LANG reference) |
| Fields | User fields with ENTITY_ID = HLBLOCK_{id} (HighloadBlockTable::compileEntityId($id)) |
| Data class | Runtime class \{NAME}Table (global ns) extending Bitrix\Highloadblock\DataManager |
| Lang names | HighloadBlockLangTable, composite PK ID + LID (≤ 2 chars) |
| Rights | HighloadBlockRightsTable (HL_ID, TASK_ID, ACCESS_CODE) |
Naming rules: NAME — starts with capital Latin letter, Latin letters/digits only, ≤ 100 chars, must not end with Table and must not be Collection; TABLE_NAME — lowercase Latin/digits/underscore, ≤ 64 chars; both unique (table must not pre-exist). UF codes ending _REF are rejected (reserved for ORM references).
Manage HL Blocks
<?php declare(strict_types=1);
use Bitrix\Highloadblock\HighloadBlockTable;
use Bitrix\Main\Loader;
Loader::includeModule('highloadblock');
// Idempotent create: look up by stable NAME first
$hl = HighloadBlockTable::getList([
'select' => ['ID'], 'filter' => ['=NAME' => 'ProductColor'], 'limit' => 1,
])->fetch();
if (!$hl) {
$result = HighloadBlockTable::add(['NAME' => 'ProductColor', 'TABLE_NAME' => 'product_color']);
if (!$result->isSuccess()) {
throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}
$hlId = (int)$result->getId(); // table with ID column created; rolled back on failure
} else {
$hlId = (int)$hl['ID'];
}
getListextras:FIELDS_COUNT,'LANG_NAME' => 'LANG.NAME'(current-language title).update($id, [...]): changingNAMEaffects the class name on next compile; changingTABLE_NAMEphysically renames the table + multiple-value storages. Finish the request, recompile in a new request.delete($id): removes description, records, UFs, files, lang names, rights, tables. Irreversible — backup first.- Structure ops are multi-step and not transactional: check every
Result, never run the same migration concurrently. - Lang titles:
HighloadBlockLangTable::add/updatewith primary['ID' => $hlId, 'LID' => 'ru'].
User Fields
Attach via CUserTypeEntity with ENTITY_ID = HLBLOCK_{id}. Key params: FIELD_NAME (UF_ prefix, uppercase Latin/digits/_, 4–50 chars), USER_TYPE_ID (string, integer, boolean, file, enumeration, hlblock, …), MULTIPLE/MANDATORY (Y|N, default N), SORT, SETTINGS, EDIT_FORM_LABEL/LIST_COLUMN_LABEL/LIST_FILTER_LABEL, SHOW_FILTER (N|I|E|S — hide / exact / mask / substring).
$entityId = HighloadBlockTable::compileEntityId($hlId); // 'HLBLOCK_7'
$exists = \CUserTypeEntity::GetList([], ['ENTITY_ID' => $entityId, 'FIELD_NAME' => 'UF_NAME'])->Fetch();
if (!$exists) {
$fieldId = (new \CUserTypeEntity())->Add([
'ENTITY_ID' => $entityId, 'FIELD_NAME' => 'UF_NAME',
'USER_TYPE_ID' => 'string', 'MANDATORY' => 'Y',
]);
if (!$fieldId) { /* $APPLICATION->GetException()?->GetString() */ }
}
Update()cannot changeENTITY_ID,FIELD_NAME,USER_TYPE_ID,MULTIPLE— create new field, migrate values, delete old. Label updates replace all stored labels.Delete()removes value storage; file fields delete their files (block delete too).- Multiple field → extra value storage synced by
DataManager; never touch it (or the main table) with raw SQL.
Recompile rules. First compileEntity() per request builds the PHP class; repeated calls return it. After UF changes, prefer a new request; to continue in the same request re-fetch the block and call compileEntity($hlblock, true) (rebuilds ORM map but does NOT redefine the loaded PHP class), then verify via $entity->hasField('UF_X'). NAME/TABLE_NAME changes always need a new request.
Records CRUD
compileEntity($hlblock) accepts array, ID, or NAME; returns Bitrix\Main\ORM\Entity. Compile once per request.
<?php declare(strict_types=1);
use Bitrix\Highloadblock\HighloadBlockTable;
use Bitrix\Main\Loader;
Loader::includeModule('highloadblock');
$entity = HighloadBlockTable::compileEntity('ProductColor');
/** @var class-string<\Bitrix\Highloadblock\DataManager> $dataClass */
$dataClass = $entity->getDataClass();
$add = $dataClass::add(['UF_NAME' => 'Red', 'UF_CODE' => 'red', 'UF_TAGS' => ['a', 'b']]);
if (!$add->isSuccess()) { /* getErrorMessages() */ }
$row = $dataClass::getRow(['select' => ['ID', 'UF_NAME'], 'filter' => ['=UF_CODE' => 'red']]); // array|null
$list = $dataClass::getList([
'select' => ['ID', 'UF_NAME'], 'filter' => ['=UF_ACTIVE' => 1],
'order' => ['ID' => 'ASC'], 'limit' => 20, 'offset' => 0, 'count_total' => true,
]);
$total = $list->getCount();
$dataClass::update((int)$row['ID'], ['UF_NAME' => 'Dark red']); // partial: pass only changed fields
$dataClass::delete((int)$row['ID']); // also removes multiple values and UF files
- Object API:
fetchObject()/fetchCollection()work ($obj->get('UF_NAME')); classes are runtime-generated. - UF validation runs in
add()/update()(mandatory, type checks) — errors land in theResult. Unknown field keys throw. - Multiple field update replaces the whole set;
[]clears it. Single optional link cleared withnull. - Files:
\CFile::MakeFileArray($path)as value; replace by adding'old_id' => $oldFileIdto the new array; clear with array['error' => UPLOAD_ERR_NO_FILE, 'old_id' => $id, 'del' => true, ...]. File deletion happens before the operation completes — a DB transaction does not restore files. - No optimistic locking:
update()overwrites concurrent changes; lock at application level if needed. - Idempotent import:
getRowby uniqueUF_CODE→ add or update (sequential runs only; use a DB unique constraint for parallel safety).
Relations Between HL Blocks (UF type hlblock)
Stores the target record's numeric ID. SETTINGS: HLBLOCK_ID (required, target block), HLFIELD_ID (display field, 0 = ID), DISPLAY (LIST|CHECKBOX|UI|DIALOG), LIST_HEIGHT, DEFAULT_VALUE. MULTIPLE lives on the field, not in SETTINGS. Self-references allowed (e.g. UF_PARENT tree) — guard against cycles yourself.
- Single field gets an auto ORM reference
UF_X_REF: select'CAT_NAME' => 'UF_CATEGORY_REF.UF_NAME', filter'=UF_CATEGORY_REF.UF_ACTIVE' => 1. - Multiple field has no
_REFalias — only the array value and a_SINGLEhelper expression. Collect IDs, then one batched query with'@ID' => $ids. - No FK constraints: existence of the target is never checked and dangling links are not cleaned. Validate before save, define delete rules (block / clear / replace / tolerate).
Directory Property (Iblock ← HL)
Iblock property of type Справочник stores the UF_XML_ID of the HL record (not the ID). Create with PROPERTY_TYPE => 'S', USER_TYPE => 'directory', USER_TYPE_SETTINGS => ['TABLE_NAME' => $hl['TABLE_NAME']]; multiplicity via property MULTIPLE, not USER_TYPE_SETTINGS.
Directory service fields: UF_XML_ID (required, stable, unique — no auto constraint), UF_NAME, UF_SORT, UF_FILE, UF_DEF (default flag), UF_DESCRIPTION, UF_FULL_DESCRIPTION, UF_LINK. Resolve values: read property → query HL data class with '@UF_XML_ID' => $values. After a TABLE_NAME rename, update the property's USER_TYPE_SETTINGS or its options stop loading. For cross-environment transfer keep UF_XML_ID stable; numeric IDs differ per environment.
Events
Dynamic-class ORM events; register via Bitrix\Main\ORM\EventManager with the data class (register before calling the operation; permanent handlers go in init.php or module install):
use Bitrix\Main\ORM\{Event, EventManager, EventResult, EntityError};
use Bitrix\Main\ORM\Data\DataManager;
EventManager::getInstance()->addEventHandler(
$dataClass,
DataManager::EVENT_ON_BEFORE_ADD,
static function (Event $event): EventResult {
$result = new EventResult();
$fields = $event->getParameter('fields');
$result->modifyFields(['UF_CODE' => mb_strtolower(trim((string)$fields['UF_CODE']))]);
// or: $result->addError(new EntityError('...')); to cancel
return $result;
}
);
| Event | Cancellable | Notes |
|---|---|---|
OnBeforeAdd / OnBeforeUpdate | Yes | modifyFields(), unsetFields(), addError(); update gets fields + oldFields (scalar) |
OnBeforeDelete | Yes | no fields; record data in oldFields |
OnAdd / OnUpdate / OnDelete | No | pre-SQL, cannot cancel via EventResult |
OnAfterAdd / OnAfterUpdate / OnAfterDelete | No | id/primary; cache clears, audit, background jobs |
Calling the same data-class method inside its handler re-fires events — guard with a static flag. Handler exceptions abort the PHP flow; after-events are not part of a transaction with external actions.
Rights
Direct data-class calls never check user rights. Operations: hl_element_read, hl_element_write (add+update), hl_element_delete — per block, not per record. Check yourself:
$ops = \Bitrix\Highloadblock\HighloadBlockRightsTable::getOperationsName([$hlId])[$hlId] ?? [];
$allowed = $USER->IsAdmin() || in_array('hl_element_write', $ops, true);
getOperationsName() relies on global $USER — in agents/CLI decide the access model explicitly (never default to admin).
Components
Read-only public output: bitrix:highloadblock.list (BLOCK_ID, ROWS_PER_PAGE, PAGEN_ID — unique per list on one page, FILTER_NAME — name of a global var holding an ORM filter, SORT_FIELD/SORT_ORDER, DETAIL_URL with #ID#/#BLOCK_ID#, CHECK_PERMISSIONS) and bitrix:highloadblock.view (BLOCK_ID, ROW_KEY default ID, ROW_ID*, LIST_URL with #BLOCK_ID#, CHECK_PERMISSIONS). No CACHE_TYPE/CACHE_TIME — they do not cache. CHECK_PERMISSIONS => 'Y' passes if the user has any operation, not strictly read; components show errors but set no HTTP status (pre-check for real 404/403). List result: rows (SHOW_IN_LIST fields pre-rendered as HTML — don't re-escape), fields, nav_object; view result: ERROR, raw row, fields (render via $USER_FIELD_MANAGER->getListView()).
Performance
- Select only needed fields; exact filters (
=UF_CODE) index-friendly,%UF_NAMEis not; avoid functions over indexed columns inruntime. - Batch processing: iterate by
'>ID' => $lastId+order ID ASC+limit, not growingoffset; fix<=IDupper bound for a stable run; persist$lastIdfor resume. - Kill queries-in-loop: collect IDs, one query with
'@ID' => $ids, map by ID. - Query cache:
'cache' => ['ttl' => 300](off by default); JOINs need'cache_joins' => true;add/update/deleteauto-clear the entity cache, external writes need$dataClass::getEntity()->cleanCache(). - Indexes via migration:
$conn->isIndexExists($dataClass::getTableName(), $fields)/$conn->createIndex($table, $name, $fields); never per-request. - Measure with
Bitrix\Main\Diag\SqlTracker:$tracker = $connection->startTracker(), thengetQueries(),getCounter(),getTime().
Anti-Patterns
- Raw SQL against the HL table or multiple-value storage (breaks UF validation, events, sync, cache).
- Treating HL as iblock (no sections, no
API_CODEelement ORM) or hardcoding generated class names withoutcompileEntity. - Assuming rights are enforced by the ORM class, or that
CHECK_PERMISSIONScovers your own queries in templates/AJAX. - Relying on hardcoded numeric HL
IDacross environments — look up byNAME; for directory values rely onUF_XML_ID. - Using target-record
IDin adirectoryproperty (it storesUF_XML_ID) or_REF-suffixed UF codes.
Checklist
-
Loader::includeModule('highloadblock')before API use. -
NAME/TABLE_NAMEsatisfy naming rules; create viaHighloadBlockTable::add, idempotently byNAME. - UF attached to
HLBLOCK_{id}viacompileEntityId(); everyResult/CUserTypeEntityreturn checked. - After structure changes: new request (or forced
compileEntity(..., true)+hasField()check). - CRUD via compiled data class; multiple fields replaced wholesale; files via
MakeFileArray+old_id/del. - Rights checked in code (
hl_element_*); validations/cancels inOnBefore*events. - Relations validated before save (no FK); directory props keyed by stable unique
UF_XML_ID. - Big sets: batch by
ID,cache.ttlwhere data is stable, indexes added by migration only.
Related skills
bitrix-iblocks, bitrix-orm, bitrix-events, bitrix-cms-basics, bitrix-catalog (directory props on products), bitrix-performance.
Signals
- GitHub stars
- 31
- Forks
- 5
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
bitrix-highloadblock- Source
- github.com/bxmaximum/bitrix-framework-skills