Кофе && Код

Утренние советы, best practices и продвинутые техники за чашкой кофе.

Обновление свойств и количества товаров в корзине Bitrix через Sale API

Обновление свойств и количества товаров в корзине Bitrix через Sale API

В современной разработке на платформе 1С-Битрикс любые манипуляции с корзиной покупателя должны выполняться исключительно через объектно-ориентированное API модуля «Интернет-магазин» (sale). Распространенной ошибкой среди начинающих разработчиков является попытка прямого изменения записей в таблице базы данных b_sale_basket или использование устаревших методов глобального класса CSaleBasket. Такой подход крайне опасен, поскольку он не инициирует сложную цепочку внутренних бизнес-процессов ядра: автоматический пересчет правил корзины, применение скидок, расчет налогов и актуализацию стоимости доставки. В результате данные в корзине становятся некорректными, что ведет к финансовым потерям и сбоям при оформлении заказа.

Для правильного и безопасного обновления данных необходимо работать с высокоуровневыми объектами корзины. Ключевым инструментом в данном контексте выступает класс Bitrix\Sale\Basket. Загрузка объектов обычно осуществляется через статический метод loadItemsForFUser, который принимает идентификатор владельца корзины (FUser ID) и идентификатор сайта. Это позволяет получить актуальный объект корзины со всеми вложенными элементами, даже если заказ еще не начал оформляться. После того как корзина загружена, вы можете получить доступ к конкретному элементу Bitrix\Sale\BasketItem, используя его внутренний идентификатор или выполнив поиск по PRODUCT_ID.

Объект BasketItem предоставляет набор методов для управления полями записи. Например, метод setField('QUANTITY', $value) позволяет изменить количество товара, а работа с кастомными характеристиками товара осуществляется через специальную коллекцию свойств, доступную через метод getPropertyCollection(). Это критически важно для товаров с торговыми предложениями или индивидуальными параметрами, которые выбирает пользователь.

Пример реализации кода для обновления параметров товара в корзине:

use Bitrix\Main\Loader;
use Bitrix\Sale\Basket;
use Bitrix\Sale\Fuser;
use Bitrix\Main\Context;

// Обязательная проверка подключения модуля sale
if (Loader::includeModule('sale')) {
    // Получаем внутренний идентификатор корзины текущего пользователя
    $fUserId = Fuser::getId();
    $siteId = Context::getCurrent()->getSite();

    // Загружаем объект корзины со всеми товарами
    $basket = Basket::loadItemsForFUser($fUserId, $siteId);

    // Поиск конкретного элемента корзины по ID товара (PRODUCT_ID)
    // В реальных задачах ID часто передается через AJAX-запрос
    $productId = 201;
    $basketItems = $basket->getExistsItems('catalog', $productId, null); // здесь null - если нам не важны свойства товара, например, размер

    if (!empty($basketItems)) {
        // Берем первый найденный элемент
        $basketItem = current($basketItems);
			
        // 1. Изменение количества товара с автоматической проверкой доступности
        $basketItem->setField('QUANTITY', 5);

        // 2. Управление коллекцией свойств товара (например, цвет или размер)
        $propertyCollection = $basketItem->getPropertyCollection();
        
        // Метод setProperty позволяет массово обновить или добавить свойства
        $propertyCollection->setProperty([
            [
                'NAME' => 'Размер',
                'CODE' => 'SIZE',
                'VALUE' => 'XL',
                'SORT' => 100,
            ],
        ]);

        // 3. Вызов метода save() сохраняет изменения в БД и запускает пересчеты
        $saveResult = $basket->save();

        if (!$saveResult->isSuccess()) {
            // В случае ошибки возвращаем массив описаний проблем
            $errors = $saveResult->getErrorMessages();
            // Логирование ошибок или вывод пользователю
        }
    }
}

Важно учитывать контекст выполнения: если объект корзины уже является частью объекта заказа (Bitrix\Sale\Order), то вызывать метод save() непосредственно у корзины не рекомендуется. В этом случае правильнее вызвать сохранение всего заказа через $order->save(). Это гарантирует, что итоговая сумма заказа, налоги и все связанные сущности (отгрузки, оплаты) будут синхронизированы и пересчитаны в рамках одной транзакции.

Использование объектной модели BasketItem — это стандарт качественной разработки, который обеспечивает масштабируемость и стабильность вашего решения при любых обновлениях платформы.

Контроль дневных лимитов SMS через Limitation

Контроль дневных лимитов SMS через Limitation

При массовой отправке SMS-сообщений критически важно контролировать количество отправленных сообщений. Превышение лимитов провайдера приводит к блокировке аккаунта или дополнительным расходам. Модуль messageservice предоставляет класс Bitrix\MessageService\Sender\Limitation для управления дневными лимитами.

Установка дневного лимита

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

use Bitrix\MessageService\Sender\Limitation;

// Устанавливаем лимит 1000 SMS в день для sms.ru с номера +79001234567
Limitation::setDailyLimit('smsru', '+79001234567', 1000);

// Лимит для Twilio
Limitation::setDailyLimit('twilio', 'MGXXXXXXXXXX', 500);

После установки лимита все отложенные сообщения автоматически возвращаются в очередь обработки.

Проверка текущего состояния лимитов

Метод getDailyLimits() возвращает полную информацию о лимитах всех провайдеров:

use Bitrix\MessageService\Sender\Limitation;

$limits = Limitation::getDailyLimits();

foreach ($limits as $key => $data)
{
    // $key = 'smsru:+79001234567'
    echo sprintf(
        "Провайдер: %s, Номер: %s, Лимит: %d, Отправлено: %d\n",
        $data['senderId'],
        $data['fromId'],
        $data['limit'],
        $data['current']
    );
}

Проверка лимита перед отправкой

Метод checkDailyLimit() проверяет, доступна ли отправка:

use Bitrix\MessageService\Sender\Limitation;
use Bitrix\Main\Diag\EventLogger;
use Psr\Log\LogLevel;

$senderId = 'smsru';
$fromNumber = '+79001234567';

if (Limitation::checkDailyLimit($senderId, $fromNumber))
{
    // Лимит не превышен — можно отправлять
    $message = SmsManager::createMessage([
        'MESSAGE_TO' => '+79009876543',
        'MESSAGE_BODY' => 'Ваш код подтверждения: 123456',
    ]);
    $message->send();
}
else
{
    // Лимит исчерпан — сообщение будет отложено
    // Можно уведомить администратора
    $logger = new EventLogger('messageservice', 'SMS_LIMIT_EXCEEDED');
    $logger->log(
        LogLevel::WARNING,
        'Дневной лимит SMS исчерпан для ' . $senderId
    );
}

Настройка времени повторной отправки

Если сообщение отложено из-за лимита, оно будет отправлено на следующий день. Время повторной попытки настраивается:

use Bitrix\MessageService\Sender\Limitation;

// Повторная отправка в 9:00 по московскому времени
Limitation::setRetryTime([
    'h' => 9,
    'i' => 0,
    'auto' => false,
    'tz' => 'Europe/Moscow',
]);

// Получение текущих настроек
$retryTime = Limitation::getRetryTime();
// ['h' => 9, 'i' => 0, 'auto' => false, 'tz' => 'Europe/Moscow']

Класс Limitation обеспечивает централизованное управление лимитами отправки SMS для всех провайдеров и защищает от превышения квот.

Работа с датами и временем через Bitrix\Main\Type\DateTime

Работа с датами и временем через Bitrix\Main\Type\DateTime

При работе с датами в 1С-Битрикс критически важно использовать класс Bitrix\Main\Type\DateTime вместо стандартных PHP функций. Это обеспечивает корректную обработку часовых поясов пользователей и унифицированное форматирование дат согласно настройкам сайта.

Проблема

Разработчики часто используют встроенные PHP функции date(), time() или класс DateTime, что приводит к проблемам с часовыми поясами и несоответствием форматов дат в различных частях системы. Особенно это критично при работе с пользовательскими данными и API.

Решение

Класс Bitrix\Main\Type\DateTime предоставляет полный набор методов для работы с датами и временем с учетом настроек Битрикс.

Создание объектов даты

use Bitrix\Main\Type\DateTime;

// Текущая дата и время
$now = new DateTime();

// Из строки с автоопределением формата
$date1 = new DateTime('2024-12-15 15:30:00', 'Y-m-d H:i:s');

// Из timestamp
$date2 = DateTime::createFromTimestamp(time());

// Из PHP DateTime
$phpDate = new \DateTime();
$date3 = DateTime::createFromPhp($phpDate);

Работа с часовыми поясами

// Преобразование в пользовательское время
$serverDate = new DateTime('2025-12-15 12:00:00', 'Y-m-d H:i:s');
$serverDate->toUserTime(); // Учитывает настройки часового пояса пользователя

Форматирование и сравнение

// Форматирование с учетом формата сайта
$date = new DateTime();
echo $date->toString(); // Использует формат сайта из настроек

// Пользовательский формат
echo $date->format('d.m.Y H:i:s');

// Сравнение дат
$date1 = new DateTime('2025-12-15 10:00:00', 'Y-m-d H:i:s');
$date2 = new DateTime('2025-12-15 15:00:00', 'Y-m-d H:i:s');

if ($date1 < $date2) {
    echo 'date1 раньше date2';
}

Модификация дат

$date = new DateTime();

// Добавление/вычитание интервалов
$date->add('+1 day');
$date->add('+2 hours');
$date->add('-3 months');

// Установка конкретных значений
$date->setTime(14, 30, 0); // Установить время 14:30:00
$date->setDate(2024, 12, 31); // Установить дату

Итог

Использование Bitrix\Main\Type\DateTime обеспечивает корректную работу с датами в контексте Битрикс, автоматически учитывает часовые пояса пользователей и настройки форматирования. Это особенно важно при разработке API, работе с базой данных и выводе дат в интерфейсе.

Программная фильтрация данных через Security Filter Request

Программная фильтрация данных через Security Filter Request

Проблема

При интеграции с внешними системами данные поступают не через стандартные суперглобальные переменные, а через API, webhooks или файлы. Встроенный WAF Битрикса автоматически фильтрует только $_GET, $_POST и $_COOKIE, оставляя кастомные источники без защиты от XSS и SQL-инъекций.

Решение

Класс Bitrix\Security\Filter\Request позволяет программно применять те же фильтры безопасности к любым данным. Вы можете подключить нужные аудиторы и выбрать стратегию обработки опасных значений.

use Bitrix\Main\Loader;
use Bitrix\Main\Diag\EventLogger;
use Bitrix\Security\Filter\Request;
use Bitrix\Security\Filter\Auditor;
use Psr\Log\LogLevel;

Loader::includeModule('security');

// Данные из внешнего источника (API, webhook, файл)
$externalData = [
    'name' => 'Иван Петров',
    'comment' => '<script>alert("xss")</script>Текст комментария',
    'query' => "'; DROP TABLE users; --",
];

// Создаём фильтр с нужным действием
$filter = new Request([
    'action' => 'filter', // filter|clear|none
    'log' => 'Y',         // логировать срабатывания в EventLog
]);

// Подключаем аудиторы для проверки
$filter->setAuditors([
    'XSS' => new Auditor\Xss(),
    'SQL' => new Auditor\Sql(),
]);

// Фильтруем данные (ключ 'data' для произвольных массивов)
$result = $filter->filter(['data' => $externalData]);

// Получаем очищенные данные
$cleanData = $result['data'] ?? $externalData;

Параметр action определяет поведение при обнаружении угрозы:

  • filter — удаляет опасные конструкции, сохраняя остальной текст
  • clear — полностью очищает значение при срабатывании
  • none — только логирует, не изменяя данные

Для анализа того, что было отфильтровано, используйте методы класса:

// Проверяем, сработал ли какой-либо аудитор
if ($filter->isAuditorsTriggered()) {
    // Добавляем свой формат записи в лог
    $logger = new EventLogger('mymodule', 'EXTERNAL_DATA_FILTERED');
    
    // Получаем список изменённых переменных
    foreach ($filter->getChangedVars() as $varName => $originalValue) {
        $logger->log(
            LogLevel::WARNING,
            'Отфильтровано опасное значение в {variable}',
            [
                'variable' => $varName,
                'original' => $originalValue,
            ]
        );
    }
}

Доступные аудиторы находятся в пространстве Bitrix\Security\Filter\Auditor:

  • Xss — обнаружение XSS-атак (скрипты, события, опасные теги)
  • Sql — обнаружение SQL-инъекций (UNION, SELECT, DROP)
  • Path — обнаружение path traversal (../, доступ к файлам)

Итог

Класс Request из модуля security позволяет защитить любые входные данные теми же механизмами, что использует встроенный WAF Битрикса. Используйте его при обработке данных из API, webhooks, импортируемых файлов и других внешних источников.

Signer и TimeSigner для защиты данных от подделки

Signer и TimeSigner для защиты данных от подделки

При передаче данных через URL, формы или AJAX необходимо гарантировать их целостность. Стандартные проверки легко обойти, а хранение данных в сессии усложняет архитектуру. Классы Bitrix\Main\Security\Sign\Signer и TimeSigner решают эту задачу криптографической подписью.

Базовая подпись через Signer

Класс Signer создаёт подпись данных на основе HMAC и системного ключа:

use Bitrix\Main\Security\Sign\Signer;
use Bitrix\Main\Security\Sign\BadSignatureException;

$signer = new Signer();

// Подписываем данные
$data = 'user_id=15&action=delete';
$signed = $signer->sign($data);
// Результат: "user_id=15&action=delete.a1b2c3d4e5..."

// Проверяем и извлекаем данные
try {
    $original = $signer->unsign($signed);
    // $original === 'user_id=15&action=delete'
} catch (BadSignatureException $e) {
    // Данные были изменены
}

Использование salt для изоляции контекстов

Параметр salt позволяет создавать независимые подписи для разных сценариев:

use Bitrix\Main\Security\Sign\Signer;

$signer = new Signer();

// Подписи с разными salt несовместимы
$tokenA = $signer->sign('data', 'context_a');
$tokenB = $signer->sign('data', 'context_b');

// Проверка требует тот же salt
$signer->unsign($tokenA, 'context_a'); // OK
$signer->unsign($tokenA, 'context_b'); // BadSignatureException

Подпись с ограничением времени через TimeSigner

TimeSigner добавляет срок действия подписи — критически важно для одноразовых ссылок:

use Bitrix\Main\Security\Sign\TimeSigner;

$timeSigner = new TimeSigner();

// Ссылка для сброса пароля, действует 1 час
$token = $timeSigner->sign('user_id=42', '+1 hour');

// Подтверждение email, действует 24 часа
$emailToken = $timeSigner->sign('email=test@example.com', '+1 day');

// Можно указать точный timestamp
$exactToken = $timeSigner->sign('data', strtotime('2025-12-31 23:59:59'));

При проверке автоматически контролируется срок:

try {
    $data = $timeSigner->unsign($token);
} catch (BadSignatureException $e) {
    // "Signature timestamp expired" — время истекло
    // или "Signature does not match" — данные изменены
}

Практический пример: защищённая ссылка отписки

use Bitrix\Main\Security\Sign\BadSignatureException;
use Bitrix\Main\Security\Sign\TimeSigner;
use Bitrix\Main\Web\Json;

// Генерация ссылки
$timeSigner = new TimeSigner();
$payload = Json::encode(['user_id' => 15, 'mailing_id' => 7]);
$token = $timeSigner->sign($payload, '+7 days', 'unsubscribe');

$url = '/unsubscribe/?token=' . urlencode($token);

$request = \Bitrix\Main\Context::getCurrent()->getRequest();
// Обработка перехода
try {
    $data = Json::decode(
        $timeSigner->unsign($request->get('token'), 'unsubscribe')
    );
    // Безопасно отписываем пользователя
} catch (BadSignatureException $e) {
    // Ссылка недействительна или истекла
}

Классы Signer и TimeSigner используют системный ключ из настроек и не требуют дополнительной конфигурации. Это надёжный способ защитить любые передаваемые данные от модификации.

Визуальное сравнение текстов через Bitrix\Main\Text\Diff

Визуальное сравнение текстов через Bitrix\Main\Text\Diff

При работе с инфоблоками в режиме документооборота накапливается история версий элементов. Стандартный интерфейс Битрикс показывает список версий, но не позволяет увидеть конкретные изменения между ними. Класс Bitrix\Main\Text\Diff решает эту задачу.

Класс реализует алгоритм Майерса и предоставляет метод getDiffHtml(), который принимает две версии текста и возвращает HTML с подсветкой: удалённые фрагменты выделены красным перечёркнутым текстом, добавленные — зелёным жирным.

Сравнение версий элемента инфоблока

use Bitrix\Main\Text\Diff;

/**
 * Получает историю версий элемента через скомпилированный ORM класс
 */
function getElementHistory(int $elementId)
{
    return \Bitrix\Iblock\Elements\ElementNewsTable::getList([
        'select' => [
            'ID',
            'NAME',
            'PREVIEW_TEXT',
            'DETAIL_TEXT',
            'TIMESTAMP_X',
            'WF_COMMENTS',
            'WF_STATUS_ID',
            'WF_PARENT_ELEMENT_ID',
            'MODIFIER_' => 'MODIFIED_BY_USER',
        ],
        'filter' => [
            'WF_PARENT_ELEMENT_ID' => $elementId,
        ],
        'order' => ['ID' => 'ASC'],
    ])->fetchCollection();
}


$elementId = 37; // ID элемента в режиме документооборота
$history = getElementHistory($elementId);

if ($history->count() >= 2) {
    $items = $history->getAll();
    $prev = $items[count($items) - 2];
    $curr = $items[count($items) - 1];

    $diff = new Diff();

    echo '<div class="version-diff">';
    echo '<p>Версия #' . $curr->getId() . ' от ' . $curr->getTimestampX() . '</p>';
    echo '<p>Автор: ' . $curr->getModifiedByUser()->getLogin() . '</p>';

    if ($curr->getWfComments()) {
        echo '<p>Комментарий: ' . htmlspecialchars($curr->getWfComments()) . '</p>';
    }

    echo '<h4>Название:</h4>';
    echo $diff->getDiffHtml($prev->getName(), $curr->getName());

    echo '<h4>Описание:</h4>';
    echo $diff->getDiffHtml(
        $prev->getDetailText() ?? '',
        $curr->getDetailText() ?? ''
    );
    echo '</div>';
}

Вывод diff для всей истории элемента

// Формируем полную историю изменений с diff
$history = getElementHistory($elementId);
$diff = new Diff();
$items = $history->getAll();

echo '<div class="history-timeline">';
for ($i = 1; $i < count($items); $i++) {
    $prev = $items[$i - 1];
    $curr = $items[$i];

    printf(
        '<div class="history-item">
            <div class="meta">%s | %s | Статус: %d</div>
            <div class="comment">%s</div>
            <div class="changes">%s</div>
        </div>',
        $curr->getTimestampX()->format('d.m.Y H:i'),
        $curr->getModifiedByUser()?->getLogin() ?? 'N/A',
        $curr->getWfStatusId(),
        htmlspecialchars($curr->getWfComments() ?? ''),
        $diff->getDiffHtml(
            $prev->getDetailText() ?? '',
            $curr->getDetailText() ?? ''
        )
    );
}
echo '</div>';

Для программного анализа используйте метод getDiffScript(), который возвращает массив операций с ключами startA, startB, deletedA, insertedB.

Управление конфигурацией приложения через Configuration

Управление конфигурацией приложения через Configuration

Проблема

Разработчики часто хранят конфигурационные параметры приложения в собственных файлах или таблицах БД. При этом в ядре 1С-Битрикс существует готовый механизм Bitrix\Main\Config\Configuration, который предоставляет единообразный интерфейс для работы с настройками через файлы .settings.php.

Решение

Класс Configuration работает с файлом .settings.php в директории /bitrix/ и поддерживает дополнительный файл .settings_extra.php для переопределения значений. Это позволяет разделять конфигурацию для разных окружений.

Чтение настроек

use Bitrix\Main\Config\Configuration;

// Получение значения через статический метод
$cacheSettings = Configuration::getValue('cache_flags');

// Получение через экземпляр с доступом как к массиву
$config = Configuration::getInstance();
$connections = $config['connections'];
$defaultConnection = $connections['value']['default']['host'] ?? null;

Запись настроек

use Bitrix\Main\Config\Configuration;

// Простая установка значения
Configuration::setValue('my_app_settings', [
    'api_endpoint' => 'https://api.example.com',
    'timeout' => 30,
    'debug' => false,
]);

// Через экземпляр с контролем readonly
$config = Configuration::getInstance();
$config->add('feature_flags', [
    'new_catalog' => true,
    'beta_checkout' => false,
]);
$config->saveConfiguration();

Настройки модулей

Каждый модуль может иметь собственный файл .settings.php. Доступ к ним осуществляется через передачу ID модуля:

use Bitrix\Main\Config\Configuration;

// Чтение конфигурации модуля iblock
$iblockConfig = Configuration::getInstance('iblock');
$apiSettings = $iblockConfig->get('api');

// Итерация по всем настройкам модуля
$moduleConfig = Configuration::getInstance('sale');
foreach ($moduleConfig as $key => $value) {
    // Обработка каждого параметра
}

Защита от перезаписи

Параметры можно защитить флагом readonly:

// В файле .settings.php
return [
    'license' => [
        'value' => 'XXXX-XXXX-XXXX',
        'readonly' => true, // Нельзя изменить через Configuration::setValue
    ],
    'debug' => [
        'value' => true,
        'readonly' => false,
    ],
];

Разделение окружений

Файл .settings_extra.php автоматически мержится с основным. Это удобно для переопределения настроек на dev/prod:

// .settings_extra.php (не коммитится в репозиторий)
return [
    'connections' => [
        'value' => [
            'default' => [
                'host' => 'localhost',
                'database' => 'dev_db',
            ],
        ],
    ],
];

Итог

Класс Configuration предоставляет типизированный доступ к настройкам приложения с поддержкой защиты от изменений и разделения окружений. Используйте его вместо собственных решений для централизованного управления конфигурацией.

Настройка OpenSearch для полнотекстового поиска в Битрикс

Настройка OpenSearch для полнотекстового поиска в Битрикс

Стандартный поиск Битрикс использует таблицы MySQL для хранения поискового индекса. На проектах с большим объёмом контента это приводит к медленным запросам и высокой нагрузке на базу данных. Начиная с версии 25.0 модуля поиска, Битрикс поддерживает OpenSearch (форк Elasticsearch) — полнотекстовый поисковый движок с морфологией и горизонтальным масштабированием.

Настройка через административный раздел

Подключение OpenSearch выполняется в настройках модуля «Поиск» без написания кода:

  1. Перейдите в раздел Настройки → Настройки продукта → Настройки модулей → Поиск
  2. Откройте вкладку Морфология
  3. В поле Полнотекстовый поиск с помощью выберите OpenSearch
  4. Заполните параметры подключения:
    • Сервер OpenSearch — адрес в формате https://host:9200
    • Пользователь — имя пользователя для авторизации
    • Пароль — пароль пользователя
    • Название индекса — произвольное имя (латиница, цифры, дефис, подчёркивание)
  5. Для каждого сайта выберите Анализатор поисковых запросов (russian, english и др.)
  6. Сохраните настройки

После сохранения система проверит подключение. При успешном соединении появится уведомление о необходимости переиндексации.

Переиндексация контента

После смены поискового движка выполните полную переиндексацию:

  1. Перейдите в Настройки → Поиск → Переиндексация
  2. Установите флаг Очистить индекс
  3. Нажмите Начать и дождитесь завершения

Все существующие данные будут перенесены в OpenSearch. Новый контент индексируется автоматически.

Как это работает

После настройки класс CSearchFullText::getInstance() автоматически возвращает экземпляр CSearchOpenSearch вместо стандартного движка. Все компоненты поиска (bitrix:search.page, bitrix:search.title) начинают работать через OpenSearch без изменения кода.

Система создаёт отдельный индекс для каждого сайта с суффиксом: {index_name}-{site_id}. Шаблоны индексов обновляются автоматически при изменении версии модуля.

Поддерживаемые языковые анализаторы

OpenSearch поддерживает морфологический анализ для 30 языков. Основные:

Анализатор Язык
russian Русский
english Английский
german Немецкий
french Французский
spanish Испанский

Преимущества OpenSearch

  • Быстрый полнотекстовый поиск на больших объёмах данных
  • Встроенная морфология для множества языков
  • Горизонтальное масштабирование через кластеризацию
  • Снижение нагрузки на основную базу данных
  • Релевантное ранжирование результатов

Переход на OpenSearch рекомендуется для проектов с количеством индексируемых документов свыше 50 000 или при высокой частоте поисковых запросов.

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

Работа с изображениями через 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 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

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

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

Паттерн 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().