Паттерн Repository в Битрикс через RepositoryInterface

26.11.2025

Разработчики Битрикс часто работают напрямую с DataManager, размазывая логику доступа к данным по всему проекту. Это создаёт жёсткую связанность кода с ORM, затрудняет тестирование и нарушает принцип единой ответственности. В модуле main появились интерфейсы RepositoryInterface и SoftDeletableRepositoryInterface, которые позволяют строить чистую архитектуру.

Интерфейс Bitrix\Main\Repository\RepositoryInterface определяет три базовых метода:

interface RepositoryInterface
{
    public function getById(mixed $id): ?EntityInterface;
    public function save(EntityInterface $entity): void;
    public function delete(mixed $id): void;
}

Для реализации репозитория сущность должна имплементировать Bitrix\Main\Entity\EntityInterface:

use Bitrix\Main\Entity\EntityInterface;

class Order implements EntityInterface
{
    public function __construct(
        private ?int $id,
        private int $userId,
        private string $status,
        private float $amount
    ) {}

    public function getId(): mixed
    {
        return $this->id;
    }

    public function setId(int $id): void
    {
        $this->id = $id;
    }

    // Геттеры и сеттеры для остальных свойств...
}

Теперь создаём репозиторий с инкапсулированной логикой работы с БД:

use Bitrix\Main\Repository\RepositoryInterface;
use Bitrix\Main\Repository\SoftDeletableRepositoryInterface;
use Bitrix\Main\Repository\Exception\PersistenceException;
use Bitrix\Main\Entity\EntityInterface;

class OrderRepository implements RepositoryInterface, SoftDeletableRepositoryInterface
{
    public function getById(mixed $id): ?Order
    {
        $row = OrderTable::getById($id)->fetch();
        if (!$row) {
            return null;
        }

        return new Order($row['ID'], $row['USER_ID'], $row['STATUS'], $row['AMOUNT']);
    }

    public function save(EntityInterface $entity): void
    {
        $data = [
            'USER_ID' => $entity->getUserId(),
            'STATUS' => $entity->getStatus(),
            'AMOUNT' => $entity->getAmount(),
        ];

        if ($entity->getId()) {
            $result = OrderTable::update($entity->getId(), $data);
        } else {
            $result = OrderTable::add($data);
            if ($result->isSuccess()) {
                $entity->setId($result->getId());
            }
        }

        if (!$result->isSuccess()) {
            throw new PersistenceException('Ошибка сохранения', errors: $result->getErrors());
        }
    }

    public function delete(mixed $id): void
    {
        $result = OrderTable::delete($id);
        if (!$result->isSuccess()) {
            throw new PersistenceException('Ошибка удаления', errors: $result->getErrors());
        }
    }

    public function softDelete(mixed $id): void
    {
        OrderTable::update($id, ['DELETED' => 'Y']);
    }
}

Использование в сервисном слое:

class OrderService
{
    public function __construct(
        private RepositoryInterface $orderRepository
    ) {}

    public function createOrder(int $userId, float $amount): Order
    {
        $order = new Order(null, $userId, 'NEW', $amount);
        $this->orderRepository->save($order);

        return $order;
    }
}

Репозиторий изолирует бизнес-логику от деталей хранения данных. Код становится тестируемым — достаточно подставить mock-репозиторий. PersistenceException предоставляет унифицированную обработку ошибок через метод getErrors().

Telegram Все советы
Опубликовано 1 неделю назад

Похожие советы

Работа с изображениями через Bitrix\Main\File\Image

Разработчики часто используют CFile::ResizeImageGet() для изменения размеров изображений, не подозревая, что эта функция является обёрткой над современным D7 API. Классы Bitrix\Main\File\Image предоставляют прямой доступ к операциям с изображениями, что даёт больше контроля и гибкости.

Основные операции с Image

use Bitrix\Main\File\Image;
use Bitrix\Main\File\Image\Rectangle;
use Bitrix\Main\File\Image\Mask;

$image = new Image('/path/to/image.jpg');
$image->load();

// Получение информации о изображении
$info = $image->getInfo();
echo $info->getWidth() . 'x' . $info->getHeight(); // размеры
echo $info->getMime(); // MIME-тип
echo $info->getFormat(); // Image::FORMAT_JPEG, FORMAT_PNG, etc.

// Изменение размера (пропорционально)
$source = $image->getDimensions();
$destination = new Rectangle(800, 600);
$source->resize($destination, Image::RESIZE_PROPORTIONAL);
$image->resize($source, $destination);

// Сохранение с качеством 85%
$image->save(85);

Аналог unsharpmask из CFile::ResizeImageGet

// CFile::ResizeImageGet по умолчанию применяет sharpen с precision=15
$mask = Mask::createSharpen(15);
$image->filter($mask);

Режимы изменения размера

// RESIZE_PROPORTIONAL - сохраняет пропорции, вписывает в указанный прямоугольник
$source->resize($destination, Image::RESIZE_PROPORTIONAL);

// RESIZE_EXACT - кадрирует изображение по центру до точных размеров
$source->resize($destination, Image::RESIZE_EXACT);

// RESIZE_PROPORTIONAL_ALT - учитывает ориентацию (портрет/ландшафт)
$source->resize($destination, Image::RESIZE_PROPORTIONAL_ALT);

Расширенные возможности

// Поворот и отражение
$image->rotate(90); // поворот на 90°
$image->flipHorizontal(); // зеркальное отражение
$image->autoRotate($exifOrientation); // автокоррекция по EXIF

// Размытие
$image->blur(10); // sigma от 1 до 100

// Водяной знак (изображение)
use Bitrix\Main\File\Image\ImageWatermark;

$watermark = new ImageWatermark('/path/to/watermark.png');
$watermark->setAlignment('right', 'bottom')
    ->setPadding(20)
    ->setAlpha(0.7);
$image->drawWatermark($watermark);

// Сохранение в другой формат
$image->saveAs('/path/to/output.webp', 85, Image::FORMAT_WEBP);

Выбор движка: GD или Imagick

// По умолчанию используется GD
// Для Imagick зарегистрируйте сервис:
use Bitrix\Main\DI\ServiceLocator;
use Bitrix\Main\File\Image\Imagick;

$serviceLocator = ServiceLocator::getInstance();
$serviceLocator->registerByCreator('main.imageEngine', fn() => new Imagick());

Классы Bitrix\Main\File\Image полностью покрывают функциональность CFile::ResizeImageGet(), включая proportional resize, exact crop, sharpen-фильтр и водяные знаки. Для типовых задач resizing проще использовать CFile::ResizeImageGet(), а для сложных сценариев с цепочкой операций — классы напрямую.

Параллельные HTTP-запросы через асинхронный API HttpClient

При интеграции с внешними сервисами часто требуется выполнить несколько HTTP-запросов. Последовательное выполнение приводит к суммированию времени ожидания каждого запроса. Если три API отвечают по 500мс, общее время составит 1.5 секунды. Класс Bitrix\Main\Web\HttpClient поддерживает асинхронное выполнение запросов через curl_multi, что позволяет выполнять их параллельно.

Для асинхронных запросов используется метод sendAsyncRequest(), который возвращает объект Promise. Promise реализует интерфейс Http\Promise\Promise и поддерживает цепочки обработчиков через метод then().

use Bitrix\Main\Web\HttpClient;
use Bitrix\Main\Web\Http\Request;
use Bitrix\Main\Web\Uri;

// Важно: для асинхронных запросов требуется CURL
$client = new HttpClient(['useCurl' => true]);

// Список URL для параллельных запросов
$urls = [
    'products' => 'https://api.example.com/products',
    'categories' => 'https://api.example.com/categories',
    'prices' => 'https://api.example.com/prices',
];

$promises = [];

foreach ($urls as $key => $url)
{
    // Создаём PSR-7 совместимый Request
    $request = new Request('GET', new Uri($url));
    
    // sendAsyncRequest() не блокирует выполнение
    $promises[$key] = $client->sendAsyncRequest($request);
}

// wait() блокирует до завершения всех запросов
$responses = $client->wait();

// Обрабатываем результаты
foreach ($promises as $key => $promise)
{
    try
    {
        // wait() на конкретном promise возвращает Response
        $response = $promise->wait();
        $data[$key] = json_decode((string)$response->getBody(), true);
    }
    catch (\Bitrix\Main\Web\Http\ClientException $e)
    {
        // Обработка ошибок сети
        $data[$key] = ['error' => $e->getMessage()];
    }
}

Promise поддерживает callback-функции для обработки успешных и неуспешных запросов:

$promise = $client->sendAsyncRequest($request);

// Регистрируем обработчики до вызова wait()
$promise->then(
    function ($response) {
        // Вызывается при успешном ответе
        // Можно модифицировать и вернуть response
        return $response;
    },
    function ($exception) {
        // Вызывается при ошибке
        // Логируем или обрабатываем исключение
        return $exception;
    }
);

// Запускаем выполнение
$client->wait();

Для POST-запросов с телом используйте Http\FormStream:

use Bitrix\Main\Web\Http\FormStream;

$body = new FormStream(['param1' => 'value1', 'param2' => 'value2']);
$request = new Request('POST', new Uri($url), ['Content-Type' => 'application/x-www-form-urlencoded'], $body);

$promise = $client->sendAsyncRequest($request);

Асинхронный API HttpClient использует curl_multi_exec() под капотом, что обеспечивает истинную параллельность на уровне сетевых операций. Три запроса по 500мс выполнятся примерно за 500мс вместо 1.5 секунд.

Работа с Highload-блоками по имени вместо ID

Продолжаем тему избавления кодовой базы от ID при работе с сущностями Битрикс.

Проблема

Типичный код работы с Highload-блоками содержит жёстко прописанные ID:

<?php
// Антипаттерн: ID зашит в код
$hlblock = HighloadBlockTable::getById(5)->fetch();
$entity = HighloadBlockTable::compileEntity($hlblock);

ID Highload-блока различается между окружениями: на dev-сервере это 5, на production — 12. При переносе кода приходится менять значения вручную или использовать конфигурационные файлы. Ситуация усугубляется, когда ID разбросаны по десяткам файлов проекта.

Решение

Метод HighloadBlockTable::resolveHighloadblock() принимает символьное имя HL-блока вместо числового ID. Имя задаётся при создании блока и остаётся неизменным при переносе между окружениями.

<?php
use Bitrix\Highloadblock\HighloadBlockTable;
use Bitrix\Main\Loader;

Loader::includeModule('highloadblock');

// Правильно: используем символьное имя
$hlblock = HighloadBlockTable::resolveHighloadblock('Cities');

if ($hlblock !== null) {
    $entity = HighloadBlockTable::compileEntity($hlblock);
    $entityClass = $entity->getDataClass();
    
    $items = $entityClass::getList([
        'filter' => ['=UF_ACTIVE' => 1]
    ])->fetchAll();
}

Метод compileEntity также поддерживает имя

Символьное имя можно передавать напрямую в compileEntity() — метод внутри вызывает resolveHighloadblock():

<?php
// Компиляция entity по имени — без промежуточных вызовов
$entity = HighloadBlockTable::compileEntity('Cities');
$entityClass = $entity->getDataClass();

$cities = $entityClass::getList()->fetchAll();

Константы для имён HL-блоков

Для централизованного управления именами создайте класс с константами:

<?php
namespace App\Reference;

class HLBlock
{
    public const CITIES = 'Cities';
    public const REGIONS = 'Regions';
    public const COLORS = 'ProductColors';
    public const SIZES = 'ProductSizes';
}

Использование в коде:

<?php
use App\Reference\HLBlock;

$entity = HighloadBlockTable::compileEntity(HLBlock::CITIES);
$entityClass = $entity->getDataClass();

При таком подходе IDE подсказывает доступные HL-блоки, а опечатки выявляются на этапе статического анализа.

Встроенное кеширование

Метод resolveHighloadblock() кеширует результат запроса на 24 часа. Повторные вызовы с тем же именем не обращаются к базе данных:

<?php
// Первый вызов — запрос к БД, результат кешируется
$hlblock1 = HighloadBlockTable::resolveHighloadblock('Cities');

// Повторный вызов — данные из кеша ORM
$hlblock2 = HighloadBlockTable::resolveHighloadblock('Cities');

Валидация имени

Метод проверяет корректность символьного имени регулярным выражением /^[a-z0-9_]+$/i. При несуществующем или некорректном имени возвращается null:

<?php
$hlblock = HighloadBlockTable::resolveHighloadblock('NonExistent');
// $hlblock === null

Итог

Замените числовые ID на символьные имена HL-блоков через resolveHighloadblock(). Код станет переносимым между окружениями без ручных правок, а централизованные константы обеспечат контроль над используемыми справочниками.