bitrix-orm
D7 ORM tablets, ConditionTree queries, Objectify, batch/merge/deleteByFilter writes. Use for entity design, reads, and persistence.
Install
npx skills add https://github.com/bxmaximum/bitrix-framework-skills/tree/main/skills/bitrix-orm
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install bxmaximum-bitrix-framework-skills@llmmart
git clone https://github.com/bxmaximum/bitrix-framework-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole bxmaximum/bitrix-framework-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Bitrix D7 ORM
Baseline: main 23.0+. Features newer than baseline are marked Since.
Progressive disclosure: open only the rule files that match the task. Do not read every rules/*.md.
How to use
- Identify the layer the task touches.
- Open the matching
rules/*.mdbelow. - Prefer framework-native Bitrix patterns over custom abstractions.
Choose a rule file
When to read rules/tablet-map.md
Read rules/tablet-map.md (Tablet map) when the task involves:
- Naming:
DataManagervs*Table - Tablet Skeleton
- Field Types
- Relations
- User Fields (UF)
When to read rules/reading.md
Read rules/reading.md (Reading / filters) when the task involves:
- Reading Data
- Collections and Annotations
When to read rules/writing.md
Read rules/writing.md (Writing / batch / upsert) when the task involves:
- Writing
When to read rules/events-cache-security.md
Read rules/events-cache-security.md (Events, cache, security) when the task involves:
- Events
- Caching
- Security: User Input in Queries
- Checklist
Checklist
- Opened only the rule file(s) needed for this task.
- Followed DI /
/local// security canons fromAGENTS.md.
Files (bitrix-framework-skills)
-
rules
-
events-cache-security.md 2.8 KB
# Events, cache, security ## Events In the tablet class: ```php public static function onBeforeAdd(\Bitrix\Main\ORM\Event $event): \Bitrix\Main\ORM\EventResult { $result = new \Bitrix\Main\ORM\EventResult(); $fields = $event->getParameter('fields'); if (empty($fields['TITLE'])) { $result->addError(new \Bitrix\Main\ORM\EntityError('Title is required')); } // Modification: $result->modifyFields(['TITLE' => strtoupper($fields['TITLE'])]); return $result; } ``` Events: `onBeforeAdd`, `onAfterAdd`, `onBeforeUpdate`, `onAfterUpdate`, `onBeforeDelete`, `onAfterDelete`. ## Caching ```php 'cache' => [ 'ttl' => 3600, 'cache_joins' => true, ] ``` - Tablet must return `true` from `isCacheable()` (default is `true` on `DataManager`). - On successful add/update/delete, ORM calls `DataManager::cleanCache()` → managed cache dir `orm_<tableName>` (e.g. `orm_vendor_module_post`). There are **no** fictional `ORM_*` tagged-cache tags for this — invalidation is by managed-cache directory. - TTL can be clamped via global `.settings.php` `cache_flags` keys `<table>_min_ttl` / `<table>_max_ttl` (see `Entity::getCacheTtl`). Details: skill `bitrix-caching`. ```php PostTable::cleanCache(); // after external bulk SQL that bypasses ORM writes ``` ## Security: User Input in Queries Field names in `select` / `order` and expressions in `ExpressionField` / `runtime` / `SqlExpression` are **not** safely escaped as identifiers. Never pass request parameters into them without a whitelist. Prefer bound filter values (`where`, `filter` values). Full patterns: skill `bitrix-security`. ```php // BAD: $order = $_GET['by']; // GOOD: $allowed = ['ID', 'CREATED_AT', 'TITLE']; $order = in_array($userBy, $allowed, true) ? $userBy : 'ID'; PostTable::getList(['order' => [$order => 'DESC']]); ``` ## Checklist - [ ] Tablet class lives in `lib/Model/` and ends in `Table` (extends `DataManager`). - [ ] Primary keys are correctly defined (`configurePrimary`); fluent `configureXxx` for fields. - [ ] New reads use `query()` + `ConditionTree` (not `getList`/array filter by default). - [ ] Fetch shape matches need: objects for entity/relations; arrays for lists/aggregates. - [ ] Object fetch is not used for aggregation / `GROUP BY`. - [ ] Runtime fields registered as field objects; `disableDataDoubling()` only for 1:N filter duplication. - [ ] Writes: `save()` for object/relations; `add`/`update` for simple rows; multi for homogeneous batches. - [ ] `deleteByFilter` / merge / insert-ignore used only with intentional semantics and narrow filters. - [ ] Cache: `isCacheable` + `cleanCache` / `orm_*` dirs understood; no fictional `ORM_*` tags. - [ ] No user input in `select` / `order` / `ExpressionField` / `runtime` without whitelist. - [ ] UF via `getUfId()`, not deprecated `UField`; `orm:annotate` run for IDE. -
reading.md 3.5 KB
# Reading / filters ## Reading Data ### Decision guide (read) | Need | Prefer | | --- | --- | | New query in new code | `DataManager::query()` + fluent API | | Nested / OR / EXISTS / column-to-column filter | `Query::filter()` → `ConditionTree` (`where*`, `logic()`) | | Entity + relations / further mutation | `fetchObject()` / `fetchCollection()` | | Flat list, aggregation, hot path | `fetch()` / `fetchAll()` | | Legacy file already on array API | `getList()` / array `filter` (compatibility only) | | Reuse ORM filter as SQL WHERE | `Query::buildFilterSql($entity, $filter)` | Rules: - Prefer `query()` over `getList()` / `getRow()` for new code (`getList` is a wrapper). - Prefer `ConditionTree` over legacy `'=FIELD' => $value` arrays for new filters. - Do **not** use object fetch for `GROUP BY` / aggregated result shapes. - Register runtime fields as `ExpressionField` objects — not array-expressions in `select`. - `disableDataDoubling()` only when a 1:N back-reference filter duplicates rows — not a default accelerator. - Private fields: call `enablePrivateFields()` explicitly when needed. ### Preferred: `query()` + `ConditionTree` ```php use Bitrix\Main\ORM\Query\Query; $visibility = Query::filter() ->logic('or') ->where('AUTHOR_ID', $userId) ->where('PUBLIC', 'Y'); $posts = PostTable::query() ->setSelect(['ID', 'TITLE', 'AUTHOR_ID']) ->where('ACTIVE', 'Y') ->where($visibility) ->setOrder(['CREATED_AT' => 'DESC']) ->setLimit(20) ->fetchAll(); ``` ### Objects (`fetchObject`, `fetchCollection`) ```php $post = PostTable::query() ->setSelect(['*', 'AUTHOR', 'COMMENTS']) ->where('ID', $id) ->fetchObject(); $title = $post?->getTitle(); $authorName = $post?->getAuthor()?->getName(); $collection = PostTable::query() ->setSelect(['*', 'COMMENTS']) ->where('ACTIVE', 'Y') ->fetchCollection(); ``` ### Query builder (runtime / aggregates) ```php use Bitrix\Main\ORM\Fields\ExpressionField; $query = PostTable::query() ->setSelect(['ID', 'TITLE', new ExpressionField('CNT', 'COUNT(%s)', 'COMMENTS.ID')]) ->registerRuntimeField( new ExpressionField('IS_NEW', 'CASE WHEN %s > NOW() - INTERVAL 7 DAY THEN 1 ELSE 0 END', 'CREATED_AT') ) ->where('ACTIVE', 'Y') ->whereIn('AUTHOR_ID', [1, 2, 3]) ->addOrder('CREATED_AT', 'DESC') ->setLimit(50) ->setGroup(['ID']) ->having('CNT', '>', 0); // Aggregates → fetchAll(), not fetchObject() $result = $query->fetchAll(); ``` ### `buildFilterSql` (bridge to raw / mass ops) ```php $filter = Query::filter() ->where('PROJECT_ID', $projectId) ->whereNotNull('ARCHIVED_AT'); $whereSql = Query::buildFilterSql(PostTable::getEntity(), $filter); ``` ### Legacy: `getList` + array filter ```php $rows = PostTable::getList([ 'select' => ['ID', 'TITLE', 'AUTHOR_NAME' => 'AUTHOR.NAME'], 'filter' => ['=ACTIVE' => 'Y'], 'order' => ['CREATED_AT' => 'DESC'], 'limit' => 20, 'cache' => ['ttl' => 3600], ])->fetchAll(); ``` Use only in existing array-style code or tiny compatibility patches. ## Collections and Annotations After `orm:annotate`, IDE gets types like `EO_Post`, `EO_Post_Collection`, `EO_Post_Query`: ```php /** @var \Vendor\Module\Model\EO_Post $post */ $post = PostTable::getByPrimary($id)->fetchObject(); /** @var \Vendor\Module\Model\EO_Post_Collection $posts */ $posts = PostTable::query()->where('ACTIVE', 'Y')->fetchCollection(); ``` Collection methods: `save()`, `delete()`, `fill()` (eager load relations). Use `fetchCollection()` instead of looping `fetchObject()` to avoid N+1. -
tablet-map.md 3.9 KB
# Tablet map ## Naming: `DataManager` vs `*Table` - `\Bitrix\Main\ORM\Data\DataManager` — **base class** for all tablets (do not put business entities directly on it). - Concrete tablets are named `SomethingTable` and extend `DataManager` (`PostTable`). - The name **without** `Table` is reserved for the entity object class (`Post` / generated `EO_Post`). All project tablets live in `/local/modules/<m>/lib/Model/`. Generation: ```bash php bitrix/bitrix.php make:tablet my_post vendor.module php bitrix/bitrix.php orm:annotate -m vendor.module # IDE annotations ``` ## Tablet Skeleton ```php <?php declare(strict_types=1); namespace Vendor\Module\Model; use Bitrix\Main\ORM\Data\DataManager; use Bitrix\Main\ORM\Fields; use Bitrix\Main\ORM\Fields\Validators; use Bitrix\Main\Localization\Loc; final class PostTable extends DataManager { public static function getTableName(): string { return 'vendor_module_post'; } public static function getUfId(): string { return 'VENDOR_MODULE_POST'; // if user fields are present } public static function isCacheable(): bool { return true; } public static function getMap(): array { return [ (new Fields\IntegerField('ID')) ->configurePrimary() ->configureAutocomplete(), (new Fields\StringField('TITLE')) ->configureRequired() ->configureSize(255) ->addValidator(new Validators\LengthValidator(null, 255)), (new Fields\TextField('BODY')) ->configureNullable(), (new Fields\BooleanField('ACTIVE')) ->configureValues('N', 'Y') ->configureDefaultValue('Y'), (new Fields\DatetimeField('CREATED_AT')) ->configureRequired() ->configureDefaultValue(fn () => new \Bitrix\Main\Type\DateTime()), (new Fields\IntegerField('AUTHOR_ID')) ->configureRequired(), (new Fields\Relations\Reference( 'AUTHOR', \Bitrix\Main\UserTable::class, ['=this.AUTHOR_ID' => 'ref.ID'], ))->configureJoinType('LEFT'), ]; } } ``` **Configuration methods instead of arrays**: `configureRequired`, `configurePrimary`, `configureAutocomplete`, `configureNullable`, `configureSize`, `configureDefaultValue`, `configureColumnName`, `configureTitle`. The old format with array `['primary' => true, 'required' => true]` still works, but prefer fluent API in new code. ## Field Types - `IntegerField`, `FloatField`, `DecimalField` — numeric. - `StringField`, `TextField` — strings/texts. - `BooleanField` — `configureValues('N', 'Y')` stores Y/N. - `DateField`, `DatetimeField` — return `Bitrix\Main\Type\Date`/`DateTime`. - `EnumField` — `configureValues(['draft', 'published'])`. - `ArrayField` — array, with its own serializer. - `CryptoField`, `SecretField` — built-in encryption (see `bitrix-security`). - `ExpressionField('FULL_NAME', 'CONCAT(%s, " ", %s)', ['NAME', 'LAST_NAME'])` — computed field. ## Relations ```php (new Fields\Relations\Reference('AUTHOR', UserTable::class, ['=this.AUTHOR_ID' => 'ref.ID'])) ->configureJoinType('LEFT'), (new Fields\Relations\OneToMany('COMMENTS', CommentTable::class, 'POST')) ->configureJoinType('LEFT'), (new Fields\Relations\ManyToMany('TAGS', TagTable::class)) ->configureTableName('vendor_module_post_tag') ->configureLocalPrimary('ID', 'POST_ID') ->configureRemotePrimary('ID', 'TAG_ID'), ``` ## User Fields (UF) Prefer `getUfId(): string` on the tablet. When non-null, UF fields are attached automatically and usable in `select` / `filter`; on objects use `get('UF_FIELD')` / `set('UF_FIELD', $v)`. `Bitrix\Main\Entity\UField` exists but is **deprecated** (`main/include/deprecated/ufield.php`) — do not use it in new code; rely on `getUfId()` + the user field manager. -
writing.md 3 KB
# Writing / batch / upsert ## Writing ### Decision guide (write) | Need | Prefer | | --- | --- | | One row, array data | `add()` / `update()` / `delete()` | | Object state / relations | `EntityObject::save()` / collection `save()` | | Many homogeneous rows | `addMulti()` / `updateMulti()` | | Mass delete by filter (no per-row lifecycle) | `DeleteByFilterTrait::deleteByFilter()` with a **narrow** filter | | Upsert: update on conflict | `MergeTrait::merge()` or `AddMergeTrait` | | Upsert: ignore duplicate | `AddInsertIgnoreTrait` / `InsertIgnoreByDefaultTrait` | Rules: - Batch methods are not a substitute for Objectify when relations/state matter. - Empty filter for `deleteByFilter()` is forbidden (not a truncate). - `addMerge` / `addInsertIgnore` often skip normal events — use only when that is an intentional contract. - `ignoreEvents` on multi-write is an explicit trade-off, not a default speed hack. - Prefer ORM write APIs that already call `cleanCache()`; do not assume cache clears after raw SQL. ### Arrays ```php $add = PostTable::add(['TITLE' => 'Hi', 'AUTHOR_ID' => 1]); if (!$add->isSuccess()) { $this->addErrors($add->getErrors()); return; } $id = $add->getId(); PostTable::update($id, ['TITLE' => 'Hello']); PostTable::delete($id); ``` ### Objects ```php $post = PostTable::createObject(); $post->setTitle('Title') ->setBody('Body') ->setAuthorId($currentUserId); $save = $post->save(); if (!$save->isSuccess()) { /* ... */ } $loaded = PostTable::getByPrimary(10)->fetchObject(); $loaded->setTitle('Updated'); $loaded->save(); $loaded->delete(); ``` Collections: ```php $collection = PostTable::query()->whereIn('ID', [1, 2])->fetchCollection(); foreach ($collection as $post) { $post->setActive(false); } $collection->save(); ``` ### Batch ```php PostTable::addMulti([ ['TITLE' => 'A', 'AUTHOR_ID' => 1], ['TITLE' => 'B', 'AUTHOR_ID' => 1], ]); PostTable::updateMulti( [['ID' => 100], ['ID' => 101]], ['ACTIVE' => 'N'], ); ``` ### Mass delete (`DeleteByFilterTrait`) ```php use Bitrix\Main\ORM\Data\DataManager; use Bitrix\Main\ORM\Data\Internal\DeleteByFilterTrait; use Bitrix\Main\ORM\Query\Query; final class ArchivedPostTable extends DataManager { use DeleteByFilterTrait; // getTableName / getMap … } ArchivedPostTable::deleteByFilter( Query::filter() ->where('AUTHOR_ID', $authorId) ->where('ACTIVE', 'N') ); ``` ### Merge / InsertIgnore ```php use Bitrix\Main\ORM\Data\Internal\MergeTrait; use Bitrix\Main\ORM\Data\AddStrategy\Trait\AddInsertIgnoreTrait; final class PortalLinkTable extends DataManager { use MergeTrait; } PortalLinkTable::merge( ['PORTAL_ID' => $portalId, 'EXTERNAL_ID' => $externalId, 'TITLE' => $title], ['TITLE' => $title], ); final class SyncMarkerTable extends DataManager { use AddInsertIgnoreTrait; } SyncMarkerTable::addInsertIgnore([ 'ENTITY_ID' => $entityId, 'MARKER' => $marker, ]); ``` Conflict semantics: **ignore** = keep existing row; **merge** = update existing row. Do not treat them as one generic “upsert”.
-
-
SKILL.md 1.4 KB
--- name: bitrix-orm description: D7 ORM tablets, ConditionTree queries, Objectify, batch/merge/deleteByFilter writes. Use for entity design, reads, and persistence. --- # Bitrix D7 ORM Baseline: **main 23.0+**. Features newer than baseline are marked **Since**. Progressive disclosure: open **only** the rule files that match the task. Do not read every `rules/*.md`. ## How to use 1. Identify the layer the task touches. 2. Open the matching `rules/*.md` below. 3. Prefer framework-native Bitrix patterns over custom abstractions. ## Choose a rule file ### When to read `rules/tablet-map.md` Read `rules/tablet-map.md` (`Tablet map`) when the task involves: - Naming: `DataManager` vs `*Table` - Tablet Skeleton - Field Types - Relations - User Fields (UF) ### When to read `rules/reading.md` Read `rules/reading.md` (`Reading / filters`) when the task involves: - Reading Data - Collections and Annotations ### When to read `rules/writing.md` Read `rules/writing.md` (`Writing / batch / upsert`) when the task involves: - Writing ### When to read `rules/events-cache-security.md` Read `rules/events-cache-security.md` (`Events, cache, security`) when the task involves: - Events - Caching - Security: User Input in Queries - Checklist ## Checklist - [ ] Opened only the rule file(s) needed for this task. - [ ] Followed DI / `/local/` / security canons from `AGENTS.md`.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.