14.01.2026 15 мин чтения

Repository в Битрикс: как перестать дергать CIBlockElement из бизнес-логики

Repository (репозиторий) — паттерн проектирования, который изолирует доступ к данным и скрывает детали хранения.

Кирилл Новожилов

Кирилл Новожилов

Автор

Repository в Битрикс: как перестать дергать CIBlockElement из бизнес-логики

В контексте 1С-Битрикс это особенно ценно: вместо того чтобы размазывать по проекту CIBlockElement, Option::get, ORM-таблицы и фильтры, вы получаете единый слой, который возвращает понятные объекты (DTO) и даёт стабильный контракт для сервиса.

Репозиторий — один из ключевых «строительных блоков» для соблюдения принципа SRP (Single Responsibility Principle): он выносит инфраструктуру (инфоблок/ORM/SQL) из компонента и из бизнес-логики.

Проблема: доступ к данным размазан по всему проекту

Типичный Bitrix-код со временем превращается в смесь:

  • в компоненте дергаем CIBlockElement::GetList,
  • в сервисе — \Bitrix\Iblock\ElementTable,
  • в обработчике события — ещё раз CIBlockElement, но уже с другим select,
  • где-то рядом — Option::get за IBLOCK_ID, кэш руками, и внезапно формирование ответа для фронта.
        // Типичная ситуация: бизнес-логика знает слишком много про Bitrix API
function canUserBuyProduct(int $userId, int $productId): bool
{
    \Bitrix\Main\Loader::includeModule('iblock');

    $rs = \CIBlockElement::GetList(
        [],
        ['IBLOCK_ID' => 12, 'ID' => $productId, 'ACTIVE' => 'Y'],
        false,
        false,
        ['ID', 'NAME', 'PROPERTY_AVAILABLE', 'PROPERTY_MIN_QTY']
    );

    $el = $rs->GetNext();
    if (!$el) {
        return false;
    }

    $available = (string)($el['PROPERTY_AVAILABLE_VALUE'] ?? 'N');
    $minQty = (int)($el['PROPERTY_MIN_QTY_VALUE'] ?? 1);

    // ... ещё 80 строк бизнес-правил ...
    return $available === 'Y' && $minQty <= 3;
}

    

Почему это проблема

1. Нарушается SRP

Бизнес-логика начинает отвечать сразу за несколько вещей: правила предметной области + инфраструктура Bitrix (модули, инфоблоки, фильтры, поля, свойства, особенности API).

2. Невозможно безопасно менять хранение

Хотите заменить инфоблок на HL-блок/таблицу/внешний сервис? Придётся искать «кусочки доступа к данным» по всему проекту.

3. Контракты неявные

Функции начинают принимать и возвращать массивы «как получится». В одном месте PROPERTY_AVAILABLE_VALUE, в другом — AVAILABLE, в третьем — UF_AVAILABLE.

4. Кэш, выборки и фильтры плодятся

В разных местах появляются разные select, разные фильтры ACTIVE, разные правила сортировки. Итог — рассинхрон и баги «почему на странице одно, а в API другое».

Решение: Repository как единая точка доступа к данным

Repository — это класс, который:

  • инкапсулирует работу с хранилищем (инфоблоки / ORM / SQL / API),
  • предоставляет методы чтения/сохранения в терминах предметной области,
  • возвращает стабильные объекты (обычно DTO), а не битриксовые массивы «как вышло».
        interface ProductRepositoryInterface
{
    public function findById(int $id): ?ProductDto;
    public function findByCode(string $code): ?ProductDto;
}

    

Теперь сервис не знает о CIBlockElement и структуре полей. Он знает только контракт репозитория.

Ключевые преимущества репозитория

1. Локализация изменений

Поменялась структура инфоблока? Поле переименовали? Мигрировали на D7? Меняется репозиторий (и/или маппер), а не весь проект.

2. Чистые границы слоёв

Компоненты/контроллеры → сервисы → репозитории. Каждый слой отвечает за своё.

3. Единая политика выборок

Один репозиторий — один источник правды про select, filter, сортировку, правила ACTIVE/SECTION_ID/доступность.

4. Типобезопасность и понятные контракты

Вместо «массив с ключами, о которых нужно догадаться» — DTO с явными полями.

Когда использовать Repository

Repository полезен не всегда. Его цель — стабильный доступ к данным, когда проект растёт и появляются повторяющиеся сценарии чтения/сохранения.

Используйте Repository когда:

  • данные читаются/пишутся в нескольких местах,
  • есть бизнес-логика, которая не должна знать про Bitrix API,
  • вы хотите постепенно прийти к SOLID (особенно SRP и DIP),
  • планируется миграция (старое ядро → D7, инфоблок → HL/таблица),
  • важно унифицировать выборки и кэширование.

Можно обойтись без Repository когда:

  • это одноразовая маленькая выборка в админке,
  • сценарий локальный и не повторяется,
  • вы делаете быстрый прототип (но тогда примите будущий техдолг осознанно).

Анатомия Repository в Bitrix

Базовая связка: DTO + Mapper + Repository

В реальном Bitrix-проекте репозиторий почти всегда работает в паре с маппером:

  • Mapper знает, как превратить битриксовый массив полей/свойств в DTO и обратно.
  • Repository знает, как достать/сохранить эти поля в конкретном хранилище.
        final class ProductDto
{
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly string $code,
        public readonly bool $active,
    ) {}
}

    
        final class ProductMapper
{
    public function fromBitrixElement(array $fields): ProductDto
    {
        return new ProductDto(
            id: (int)$fields['ID'],
            name: (string)$fields['NAME'],
            code: (string)$fields['CODE'],
            active: ((string)($fields['ACTIVE'] ?? 'N')) === 'Y',
        );
    }
}

    

Репозиторий на старом ядре (CIBlockElement)

Важно Старое ядро до сих пор встречается в большинстве проектов. Если у вас D7 ORM (Bitrix\Iblock\Elements), принцип тот же — меняется реализация.
        final class ProductRepository implements ProductRepositoryInterface
{
    public function __construct(
        private readonly int $iblockId,
        private readonly ProductMapper $mapper,
    ) {}

    public function findById(int $id): ?ProductDto
    {
        \Bitrix\Main\Loader::includeModule('iblock');

        $rs = \CIBlockElement::GetList(
            [],
            ['IBLOCK_ID' => $this->iblockId, 'ID' => $id],
            false,
            false,
            ['ID', 'NAME', 'CODE', 'ACTIVE']
        );

        $row = $rs->GetNext();
        if (!$row) {
            return null;
        }

        return $this->mapper->fromBitrixElement($row);
    }

    public function findByCode(string $code): ?ProductDto
    {
        \Bitrix\Main\Loader::includeModule('iblock');

        $rs = \CIBlockElement::GetList(
            [],
            ['IBLOCK_ID' => $this->iblockId, '=CODE' => $code],
            false,
            false,
            ['ID', 'NAME', 'CODE', 'ACTIVE']
        );

        $row = $rs->GetNext();
        return $row ? $this->mapper->fromBitrixElement($row) : null;
    }
}

    

Сервис, который использует репозиторий, теперь не знает про Loader::includeModule, GetList, select и ACTIVE.

Репозиторий на D7 (общая идея)

Если вы используете D7, репозиторий остаётся тем же по смыслу: он всё равно скрывает инфраструктуру и возвращает DTO.

Пример (упрощённо, без конкретного компилируемого класса инфоблока):

        final class ProductRepositoryD7 implements ProductRepositoryInterface
{
    public function __construct(
        private readonly ProductMapper $mapper,
    ) {}

    public function findById(int $id): ?ProductDto
    {
        // Здесь будет ваш D7-запрос (например, через компилируемый класс инфоблока).
        // Идея: repo возвращает DTO, а не Bitrix-объекты/массивы наружу.

        $row = null; // результат запроса как массив полей
        if (!$row) {
            return null;
        }

        return $this->mapper->fromBitrixElement($row);
    }

    public function findByCode(string $code): ?ProductDto
    {
        $row = null;
        return $row ? $this->mapper->fromBitrixElement($row) : null;
    }
}

    

Важно не «каким API вы делаете запрос», а то, что внешний код не зависит от этого API.

Архитектура с Repository (как это помогает SRP)

Repository красиво вписывается в слоистую архитектуру (как и DTO):

        ┌─────────────────────────────────────────┐
│            Controller / Component       │
│  Принимает запрос, вызывает сервис      │
└─────────────────┬───────────────────────┘
                  │ DTO / primitives
                  ▼
┌─────────────────────────────────────────┐
│               Service                   │
│  Бизнес-логика, координация сценария    │
└─────────────────┬───────────────────────┘
                  │ calls
                  ▼
┌─────────────────────────────────────────┐
│             Repository                  │
│  Доступ к данным, возвращает DTO        │
└─────────────────┬───────────────────────┘
                  │ uses
                  ▼
┌─────────────────────────────────────────┐
│         Bitrix storage (Iblock/ORM)     │
└─────────────────────────────────────────┘

    

Пример: сервис с репозиторием

        final class ProductAvailabilityService
{
    public function __construct(
        private readonly ProductRepositoryInterface $products,
    ) {}

    public function canBuyById(int $productId): bool
    {
        $product = $this->products->findById($productId);
        if ($product === null) {
            return false;
        }

        // бизнес-логика работает с понятным DTO
        return $product->active;
    }
}

    

Типичные ошибки

1. Репозиторий превращается в «сервис на всё»

        // Плохо: репозиторий начинает заниматься бизнес-логикой
class ProductRepository
{
    public function canBuy(int $productId, int $userId): bool
    {
        // ... 200 строк правил, скидок, прав доступа ...
    }
}

    

Хорошо: репозиторий отвечает за доступ к данным, а бизнес-правила живут в сервисах.

2. Репозиторий возвращает битриксовые массивы «как есть»

        // Плохо: наружу течёт инфраструктура
public function findById(int $id): ?array
{
    return \CIBlockElement::GetList(...)->GetNext() ?: null;
}

    

Хорошо: репозиторий возвращает DTO (или доменные объекты), а не формат Bitrix API.

3. Репозиторий протекает деталями хранения

        // Плохо: внешний код начинает знать про PROPERTY_*
$row = $repo->findRawById($id);
if ($row['PROPERTY_AVAILABLE_VALUE'] === 'Y') { ... }

    

Если где-то в коде появились PROPERTY_* — значит границы нарушены и SRP снова ломается.

4. Слишком «умные» универсальные методы

Попытка сделать «один метод на все случаи» часто заканчивается репозиторием-лапшой:

        // Плохо: универсальный findList с десятком параметров
public function findList(
    array $filter,
    array $select,
    array $order,
    int $limit,
    int $offset,
): array {}

    

Такой API быстро становится неудобным и снова тащит наружу детали хранения.

Лучше: делайте методы под реальные сценарии или вводите отдельные объект-фильтры (Filter DTO).

Рекомендации

1. Дайте репозиторию стабильный контракт

Начните с интерфейса (ProductRepositoryInterface) и пары методов (findById, findByCode). Расширяйте по мере появления реальных сценариев.

2. Возвращайте DTO, а не массивы

Если DTO пока нет — даже простой final class на 3–5 полей уже даст огромный выигрыш в читабельности.

3. Маппинг держите отдельно

Mapper — это «переводчик» между Bitrix-данными и вашим контрактом. Он помогает держать репозиторий компактным и облегчает миграции.

4. Не мешайте бизнес-логику в репозиторий

Репозиторий отвечает за «достать/сохранить». Всё, что про правила, сценарии, условия — в сервис.

5. Начинайте с одной зоны

Не нужно «внедрить репозитории везде». Возьмите один сценарий (например, товары или заявки) и доведите до рабочего состояния. Эффект станет заметен быстро.

Заключение

Repository — это не «архитектурная роскошь». В Bitrix он решает очень практичную боль: избавляет бизнес-логику от зависимости на CIBlockElement/ORM и делает изменения локальными.

В связке с DTO и мапперами вы получаете:

  • понятные контракты между слоями,
  • меньше регресса при изменениях,
  • быстрее рефакторинг и миграции,
  • реальный SRP в компонентах, сервисах и обработчиках.
Опубликовано 2 недели назад

Похожие статьи

Мы используем файлы cookie для улучшения работы сайта. Продолжая использовать сайт, вы соглашаетесь с нашей политикой конфиденциальности.