Repository в Битрикс: как перестать дергать CIBlockElement из бизнес-логики
Repository (репозиторий) — паттерн проектирования, который изолирует доступ к данным и скрывает детали хранения.
Кирилл Новожилов
Автор
Содержание
В контексте 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)
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 в компонентах, сервисах и обработчиках.
Похожие статьи