Claude Skill

bitrix-orm

D7 ORM tablets, ConditionTree queries, Objectify, batch/merge/deleteByFilter writes. Use for entity design, reads, and persistence.

LLM Mart · 0 points · 11 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download bxmaximum-bitrix-framework-skills-skills_bitrix-orm-66c40e0.zip · 7 KB
Part of bxmaximum/bitrix-framework-skills — 38 skills

Install

skills CLI npx skills add https://github.com/bxmaximum/bitrix-framework-skills/tree/main/skills/bitrix-orm
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install bxmaximum-bitrix-framework-skills@llmmart
Git 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

  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.
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.

No comments yet.

Reviews (0)

No reviews yet.

Related