Development of Custom ORM Event Handlers for 1C-Bitrix
ORM D7 is not just a wrapper over SQL. It's a full-fledged object layer with its own event model, which works differently from "old" core events. While OnAfterIBlockElementUpdate fires on any element update via any API, ORM events are tied to a specific entity and operations on it via DataManager. This gives point control: intercept adding a record to b_sale_order_props_value table without writing database triggers.
How ORM Events Are Structured
Each DataManager class generates events at four points in a record's lifecycle:
-
OnBeforeAdd— before insert, can modify fields or cancel operation -
OnAfterAdd— after successful insert,IDfield already available -
OnBeforeUpdate— before UPDATE, can change passed fields -
OnAfterUpdate— after successful UPDATE -
OnBeforeDelete— before DELETE, can cancel deletion -
OnAfterDelete— after DELETE
Event is formatted as {ClassName}::On{Action}. For class Bitrix\Sale\Internals\OrderTable, event before adding would be Bitrix\Sale\Internals\OrderTable::OnBeforeAdd.
Registration:
use Bitrix\Main\EventManager;
EventManager::getInstance()->addEventHandler(
'sale',
'\Bitrix\Sale\Internals\OrderTable::OnAfterAdd',
[\MyProject\Handlers\OrderOrmHandler::class, 'onAfterAdd']
);
Event Object and Available Data
An \Bitrix\Main\Entity\Event object is passed to the handler. Parameters are extracted from it:
public static function onAfterAdd(\Bitrix\Main\Entity\Event $event): void
{
$result = $event->getParameter('result'); // Result object with ID
$fields = $event->getParameter('fields'); // array of saved fields
$newId = $result->getId();
$userId = $fields['USER_ID'] ?? null;
}
For Before-events — you can modify fields via Result object:
public static function onBeforeAdd(\Bitrix\Main\Entity\Event $event): \Bitrix\Main\Entity\EventResult
{
$result = new \Bitrix\Main\Entity\EventResult();
// Add/change field before save
$result->modifyFields(['CREATED_BY' => \CUser::GetID()]);
// Or abort operation
// $result->addError(new \Bitrix\Main\Error('Forbidden'));
return $result;
}
Practical Scenarios
Change audit. Log who and when changed a record in a custom HL table:
public static function onAfterUpdate(\Bitrix\Main\Entity\Event $event): void
{
$id = $event->getParameter('id')['ID'];
$fields = $event->getParameter('fields');
\MyProject\AuditLog::write([
'entity' => 'MyHlTable',
'entity_id' => $id,
'user_id' => \CUser::GetID(),
'changes' => json_encode($fields),
'timestamp' => new \Bitrix\Main\Type\DateTime(),
]);
}
Auto-filling fields. When adding a record, automatically set fields that shouldn't be trusted to client code:
public static function onBeforeAdd(\Bitrix\Main\Entity\Event $event): \Bitrix\Main\Entity\EventResult
{
$result = new \Bitrix\Main\Entity\EventResult();
$result->modifyFields([
'CREATED_AT' => new \Bitrix\Main\Type\DateTime(),
'STATUS' => 'DRAFT',
'HASH' => md5(uniqid('', true)),
]);
return $result;
}
Cascade deletion. Before deleting main record, clean related data (which ORM doesn't delete automatically without explicit relationships):
public static function onBeforeDelete(\Bitrix\Main\Entity\Event $event): void
{
$id = $event->getParameter('id')['ID'];
// delete related records via their DataManager
\MyProject\RelatedItemTable::deleteByParentId($id);
}
Difference Between ORM Events and "Old" Events
| Parameter | ORM Events (D7) | Old Events (CMain) |
|---|---|---|
| Binding | Specific DataManager class | Any code calling API |
| Event object | \Bitrix\Main\Entity\Event |
Array $arParams |
| Field modification | Via EventResult::modifyFields() |
Via changing passed array by reference |
| Operation abort | EventResult::addError() |
Return false or event-specific |
| Registration readability | Class name + operation | String event identifier |
Important nuance: if a record is created via direct SQL (Application::getConnection()->query(...)) or old API (CIBlockElement::Add()), ORM events don't fire. ORM events work only via DataManager::add(), ::update(), ::delete().
Highload Blocks and ORM Events
For HL blocks, DataManager class is generated dynamically. Find class name:
$hlblock = \Bitrix\Highloadblock\HighloadBlockTable::getById($hlId)->fetch();
$entity = \Bitrix\Highloadblock\HighloadBlockTable::compileEntity($hlblock);
$className = $entity->getDataClass(); // something like HlbomOrderStatusTable
Then register handler for this class. If working with multiple HL blocks — convenient to create universal handler that routes by class name.
Timeline
| Task | Duration |
|---|---|
| 2–4 handlers for one ORM entity (audit, auto-filling, validation) | 2–4 days |
| Audit system for 5–10 ORM tables with change history | 1–1.5 weeks |
| Migration of "old" handlers to ORM events with testing | 1–2 weeks |
ORM events give granular control over data lifecycle without database triggers and without intercepting broad core events. Properly organized handlers — this is audit, validation, and business logic that lives with the data, not scattered across the project.







