Developing custom event handlers for 1C-Bitrix ORM

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1175
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

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, ID field 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.