Кофе && Код

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

Работа с датами и временем через 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().

Избавляемся от ID инфоблока при работе с разделами

Избавляемся от ID инфоблока при работе с разделами

Проблема

В типичном проекте на 1С-Битрикс код работы с разделами напрямую взаимодействует с ID инфоблока. При каждом запросе к SectionTable или CIBlockSection приходится явно указывать фильтр по инфоблоку. Если нужно изменить ID инфоблока или перенести код на другой проект — начинается ад с поиском и заменой всех вхождений.

Решение

Класс Bitrix\Iblock\Model\Section предоставляет метод compileEntityByIblock(), который создаёт специализированный класс для работы с разделами конкретного инфоблока. Все запросы через этот класс автоматически фильтруются по нужному IBLOCK_ID без явного указания.

Было: магические числа везде

        use Bitrix\Iblock\SectionTable;

// Приходится каждый раз указывать IBLOCK_ID
$sections = SectionTable::getList([
    'select' => ['ID', 'NAME', 'CODE'],
    'filter' => [
        'IBLOCK_ID' => 10, // Что это за инфоблок?
        'ACTIVE' => 'Y'
    ]
])->fetchAll();

// И снова тот же ID в другом месте
$section = SectionTable::getByPrimary(25, [
    'filter' => ['IBLOCK_ID' => 10] // Дублирование!
])->fetch();

    

Стало: чистый код без дублирования

        use Bitrix\Iblock\Model\Section;

// Один раз создаём класс для инфоблока по API_CODE
$catalogSectionEntity = Section::compileEntityByIblock('catalog');

// IBLOCK_ID больше не нужен - всё фильтруется автоматически
$sections = $catalogSectionEntity::getList([
    'select' => ['ID', 'NAME', 'CODE'],
    'filter' => ['ACTIVE' => 'Y']
])->fetchAll();


    

Бонус: автоматическая связь с родителем

Помимо избавления от IBLOCK_ID, получаем ещё и связь PARENT_SECTION для работы с иерархией:

        $catalogSectionEntity = Section::compileEntityByIblock('catalog');

// Получаем раздел с данными родителя одним запросом
$section = $catalogSectionEntity::getList([
    'filter' => ['=CODE' => 'laptops'],
    'select' => ['ID', 'NAME', 'PARENT_SECTION.NAME']
])->fetchObject();

echo $section->getName(); // "Ноутбуки"
echo $section->getParentSection()?->getName(); // "Компьютеры"

    

Практический пример: хлебные крошки без IBLOCK_ID

        use Bitrix\Iblock\Model\Section;

// Инициализируем класс для каталога
$catalogSectionEntity = Section::compileEntityByIblock('catalog');

$section = $catalogSectionEntity::query()
    ->setSelect([
        'ID', 
        'NAME',
        'CODE',
        'PARENT_SECTION.NAME',
        'PARENT_SECTION.PARENT_SECTION.NAME'
    ])
    ->where('CODE', 'gaming-laptops')
    ->exec()
    ->fetchObject();

// Строим хлебные крошки рекурсивно
$breadcrumbs = [];
$current = $section;

while ($current) {
    $breadcrumbs[] = [
        'name' => $current->getName(),
        'code' => $current->getCode()
    ];
    $current = $current->getParentSection();
}

$breadcrumbs = array_reverse($breadcrumbs);

    

Что получаем

  1. Нет магических чисел: IBLOCK_ID указан один раз при создании класса
  2. Переносимость: изменили ID инфоблока — код работает без изменений
  3. Читаемость: не нужно помнить, что 10 — это каталог, а 15 — новости
  4. Меньше ошибок: невозможно забыть добавить фильтр по IBLOCK_ID
  5. Связь с родителем: автоматическая PARENT_SECTION для иерархии
Кастомный тип свойства для JSON-данных с валидацией

Кастомный тип свойства для JSON-данных с валидацией

Проблема

При работе со сложными структурированными данными в инфоблоках часто приходится хранить JSON в обычных строковых свойствах. Это создает проблемы: нет валидации при сохранении, неудобный интерфейс редактирования и риск повреждения данных. Стандартные типы свойств Битрикс не предоставляют встроенных механизмов для работы с JSON-структурами.

Решение

Создание кастомного типа свойства позволяет реализовать специализированный интерфейс с валидацией и удобным редактором. Регистрируем новый тип свойства через событие.

Регистрация типа свойства:

        <?php
// /local/php_interface/init.php
$eventManager = \Bitrix\Main\EventManager::getInstance();

$eventManager->addEventHandler(
    'iblock',
    'OnIBlockPropertyBuildList',
    ['CustomPropertyJson', 'GetUserTypeDescription']
);

class CustomPropertyJson
{
    public static function GetUserTypeDescription(): array
    {
        return [
            'PROPERTY_TYPE' => 'S',
            'USER_TYPE' => 'json_data',
            'DESCRIPTION' => 'JSON данные',
            'GetPropertyFieldHtml' => [__CLASS__, 'GetPropertyFieldHtml'],
            'ConvertToDB' => [__CLASS__, 'ConvertToDB'],
            'ConvertFromDB' => [__CLASS__, 'ConvertFromDB'],
        ];
    }

    // Валидация и сохранение
    public static function ConvertToDB($property, $value)
    {
        if (empty($value['VALUE'])) {
            return $value;
        }

        // Проверяем валидность JSON
        $decoded = json_decode($value['VALUE'], true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            global $APPLICATION;
            $APPLICATION->ThrowException('Невалидный JSON: ' . json_last_error_msg());
            return false;
        }

        // Дополнительная валидация структуры
        if (isset($property['USER_TYPE_SETTINGS']['REQUIRED_FIELDS'])) {
            $required = explode(',', $property['USER_TYPE_SETTINGS']['REQUIRED_FIELDS']);
            foreach ($required as $field) {
                if (!isset($decoded[trim($field)])) {
                    global $APPLICATION;
                    $APPLICATION->ThrowException("Отсутствует обязательное поле: {$field}");
                    return false;
                }
            }
        }

        return $value;
    }

    // Форматирование при выводе
    public static function ConvertFromDB($property, $value)
    {
        $value['VALUE'] = htmlspecialcharsback($value['VALUE']);
        return $value;
    }

    // Интерфейс редактирования
    public static function GetPropertyFieldHtml($property, $value, $strHTMLControlName)
    {
        $currentValue = $value['VALUE'];

        return '<textarea 
    name="' . $strHTMLControlName['VALUE'] . '" 
    rows="10" 
    cols="50" 
    class="json-editor"
    placeholder=\'{"key": "value"}\'
    style="font-family: monospace; font-size: 14px; line-height: 1.4; padding: 8px; border: 1px solid #ccc; border-radius: 4px; width: 100%;">' .
            htmlspecialchars($currentValue) .
            '</textarea>
<script>
    BX.ready(function() {
        const textarea = document.querySelector(\'textarea[name="' . $strHTMLControlName['VALUE'] . '"]\');
        
        // Автоформатирование при потере фокуса
        textarea.addEventListener("blur", function() {
            try {
                if (this.value.trim()) {
                    const obj = JSON.parse(this.value);
                    this.value = JSON.stringify(obj, null, 2);
                    this.style.borderColor = "#28a745";
                }
            } catch(e) {
                this.style.borderColor = "#dc3545";
                console.error("JSON error:", e);
            }
        });

        // Проверка при вводе
        textarea.addEventListener("input", function() {
            try {
                if (this.value.trim()) {
                    JSON.parse(this.value);
                    this.style.borderColor = "#28a745";
                } else {
                    this.style.borderColor = "#ccc";
                }
            } catch(e) {
                this.style.borderColor = "#dc3545";
            }
        });

        // Инициализация проверки
        textarea.dispatchEvent(new Event("input"));
    });
</script>';
    }
}

    

Итог

Кастомный тип свойства обеспечивает типобезопасность, валидацию данных на уровне сохранения и улучшает UX редактирования. Подход масштабируется для любых специфических форматов данных.

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