Паттерн Repository в Битрикс через RepositoryInterface
Разработчики Битрикс часто работают напрямую с 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().
Похожие советы
Работа с изображениями через 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(). Код станет переносимым между окружениями без ручных правок, а централизованные константы обеспечат контроль над используемыми справочниками.