Development of ORM entities for 1C-Bitrix D7

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

Developing ORM Entities for 1C-Bitrix D7

ORM D7 is Bitrix's answer to the question "how to work with a database without writing raw SQL for every little thing." A DataManager entity is a PHP class that describes a database table, its fields, and relationships with other tables. Instead of $DB->Query("SELECT ...") you write MyTable::getList([...]) and get typed objects instead of raw arrays. The platform appeared in Bitrix 14 and has since become the standard for serious development.

Basic DataManager Structure

Minimal entity:

namespace MyProject\Storage;

use Bitrix\Main\ORM\Data\DataManager;
use Bitrix\Main\ORM\Fields\IntegerField;
use Bitrix\Main\ORM\Fields\StringField;
use Bitrix\Main\ORM\Fields\DatetimeField;

class OrderLogTable extends DataManager
{
    public static function getTableName(): string
    {
        return 'my_order_log';
    }

    public static function getMap(): array
    {
        return [
            new IntegerField('ID', [
                'primary'      => true,
                'autocomplete' => true,
            ]),
            new IntegerField('ORDER_ID', [
                'required' => true,
            ]),
            new StringField('ACTION', [
                'required'  => true,
                'size'      => 100,
            ]),
            new DatetimeField('CREATED_AT', [
                'default_value' => new \Bitrix\Main\Type\DateTime(),
            ]),
        ];
    }
}

The class is registered in the module's autoload map or via PSR-4 in composer.json inside /local/.

ORM Field Types

Field Class DB Type Notes
IntegerField INT primary, autocomplete, unsigned
StringField VARCHAR size — column length
TextField TEXT for long strings
FloatField FLOAT / DECIMAL
BooleanField TINYINT(1) / CHAR(1) values — value pair (N/Y)
DateField DATE returns \Bitrix\Main\Type\Date
DatetimeField DATETIME returns \Bitrix\Main\Type\DateTime
EnumField VARCHAR values — allowed values
JsonField TEXT / JSON automatic serialization/deserialization

For field validation, use validation in the field parameters:

new StringField('STATUS', [
    'required'   => true,
    'values'     => ['DRAFT', 'ACTIVE', 'CLOSED'],
    'validation' => function() {
        return [new \Bitrix\Main\ORM\Fields\Validators\LengthValidator(1, 50)];
    },
]),

Relationships Between Entities

One-to-many (Reference):

use Bitrix\Main\ORM\Fields\Relations\Reference;
use Bitrix\Main\ORM\Query\Join;

// Add relationship with order in OrderLogTable
new Reference(
    'ORDER',
    \Bitrix\Sale\Internals\OrderTable::class,
    Join::on('this.ORDER_ID', 'ref.ID'),
    ['join_type' => 'LEFT']
),

One-to-one and many-to-many are implemented similarly, via Reference with the corresponding keys or through an intermediate table.

After defining the relationship, joined table data can be fetched in queries:

$result = OrderLogTable::getList([
    'select' => ['ID', 'ACTION', 'ORDER_.USER_ID', 'ORDER_.DATE_INSERT'],
    'filter' => ['ORDER_ID' => 42],
]);

Creating the Table: Migrations

The table is created by the createDbTable() method:

// In the module installation method
OrderLogTable::getEntity()->createDbTable();

For modifying an existing table structure — direct DDL queries via Application::getConnection():

$conn = \Bitrix\Main\Application::getConnection();
$conn->query("ALTER TABLE my_order_log ADD COLUMN CONTEXT TEXT DEFAULT NULL");

Bitrix does not have a built-in migration mechanism — for production projects this is solved either by a custom migration script or third-party packages such as arrilot/bitrix-migrations.

ORM Queries: Core Operations

Query with conditions:

$result = OrderLogTable::getList([
    'select'  => ['ID', 'ORDER_ID', 'ACTION', 'CREATED_AT'],
    'filter'  => [
        '>CREATED_AT' => new \Bitrix\Main\Type\DateTime('-7 days'),
        'ACTION'      => 'STATUS_CHANGED',
    ],
    'order'   => ['CREATED_AT' => 'DESC'],
    'limit'   => 50,
    'offset'  => 0,
]);

while ($row = $result->fetch()) {
    // $row — associative array
}

Object interface (D7 EO — Entity Objects, Bitrix 18+):

$result = OrderLogTable::getList([
    'select' => ['*'],
    'filter' => ['ORDER_ID' => 42],
]);

foreach ($result->fetchCollection() as $log) {
    echo $log->getAction(); // typed getter
    $log->setAction('UPDATED');
    $log->save();
}

Insert:

$addResult = OrderLogTable::add([
    'ORDER_ID'   => 42,
    'ACTION'     => 'STATUS_CHANGED',
    'CREATED_AT' => new \Bitrix\Main\Type\DateTime(),
]);

if ($addResult->isSuccess()) {
    $newId = $addResult->getId();
}

Update and delete:

OrderLogTable::update($id, ['ACTION' => 'CANCELLED']);
OrderLogTable::delete($id);

Aggregation and Grouping

$result = OrderLogTable::getList([
    'select' => [
        new \Bitrix\Main\ORM\Query\Query\ExpressionField('CNT', 'COUNT(*)'),
        'ACTION',
    ],
    'group' => ['ACTION'],
]);

Indexes

Indexes are added separately from table creation:

$conn = \Bitrix\Main\Application::getConnection();
$conn->query("CREATE INDEX idx_order_log_order_id ON my_order_log (ORDER_ID)");
$conn->query("CREATE INDEX idx_order_log_created ON my_order_log (CREATED_AT)");

Indexes are mandatory for frequently filtered fields. On a 500,000-row table, a query without an index on ORDER_ID is a full table scan.

Query Caching

ORM integrates with the Bitrix cache:

$result = OrderLogTable::getList([
    'select' => ['ID', 'ACTION'],
    'filter' => ['ORDER_ID' => 42],
    'cache'  => ['ttl' => 3600, 'cache_joins' => true],
]);

The cache is cleared automatically when data is modified through ORM methods (if managed cache is enabled).

Timeline

Task Timeline
1–3 simple entities (no relations, CRUD operations) 2–4 days
Complex schema: 5–10 entities with relations, validation, events 1–2 weeks
Full replacement of legacy tables with a custom ORM schema including data migration 2–4 weeks

DataManager entities are an investment in code readability and maintainability. A year later, a developer who did not write the code will understand the database schema in an hour rather than a day. With raw SQL, there is no such guarantee.