Кофе && Код

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

Получение DETAIL_PAGE_URL для элемента инфоблока в D7

Получение DETAIL_PAGE_URL для элемента инфоблока в D7

Проблема/контекст

В новом ядре D7 (таблица Bitrix\Iblock\ElementTable) нет готового поля DETAIL_PAGE_URL. Это связано с тем, что ссылка на детальную страницу — это динамический шаблон (например, /catalog/#SECTION_CODE#/#ELEMENT_CODE#/), который хранится в настройках инфоблока и требует подстановки реальных данных элемента для вычисления.

Решение с кодом

Чтобы получить корректную ссылку, необходимо выбрать данные элемента вместе с шаблоном URL из связанной таблицы инфоблока. Рекомендуется использовать Element API (если у инфоблока задан API_CODE), но способ также работает и с базовым ElementTable.

        <?php
declare(strict_types=1);

use Bitrix\Iblock\Elements\ElementCatalogTable; // Где 'Catalog' — это API_CODE вашего инфоблока
use Bitrix\Main\Loader;

Loader::includeModule('iblock');

$elementId = 123;

// Если у инфоблока нет API_CODE, используйте \Bitrix\Iblock\ElementTable
$element = ElementCatalogTable::getList([
    'select' => [
        'ID',
        'CODE',
        'IBLOCK_SECTION_ID',
        // Получаем шаблон URL из настроек инфоблока через связь IBLOCK
        'DETAIL_PAGE_URL_TEMPLATE' => 'IBLOCK.DETAIL_PAGE_URL'
    ],
    'filter' => ['=ID' => $elementId]
])->fetch();

if ($element)
{
    // Метод ReplaceDetailUrl заменит плейсхолдеры типа #ID#, #CODE# на реальные значения из массива $element
    $detailUrl = \CIBlock::ReplaceDetailUrl(
        url: $element['DETAIL_PAGE_URL_TEMPLATE'],
        arr: $element,
        server_name: true,
        arrType: 'E' // Тип "E" означает Element
    );

    echo $detailUrl;
}

    

Пример с использованием fetchObject()

Если вы предпочитаете работать с объектами (EO_Element), данные для замены масок нужно подготовить через метод collectValues():

        $elementObject = ElementCatalogTable::getList([
    'select' => ['ID', 'CODE', 'IBLOCK_SECTION_ID', 'IBLOCK.DETAIL_PAGE_URL'],
    'filter' => ['=ID' => $elementId]
])->fetchObject();

if ($elementObject)
{
    // ReplaceDetailUrl ожидает массив, поэтому используем collectValues()
    $detailUrl = \CIBlock::ReplaceDetailUrl(
        url: $elementObject->getIblock()->getDetailPageUrl(),
        arr: $elementObject->collectValues(),
        server_name: true,
        arrType: 'E'
    );
}

    

Параметры метода ReplaceDetailUrl

  1. $url (string) — Шаблон URL из настроек инфоблока (например, /catalog/#SECTION_CODE#/#ELEMENT_CODE#/).
  2. $arr (array) — Массив данных элемента. Ключи должны совпадать с масками (ID для #ID#, CODE для #CODE# и т.д.).
  3. $server_name (bool) — Если true, метод подставит SITE_DIR и домен сайта. Полезно для генерации полных ссылок (в письмах или RSS).
  4. $arrType (string) — Тип сущности: "E" для элементов, "S" для разделов. Это определяет, какие маски будут обрабатываться (например, #SECTION_CODE_PATH#).

Практические детали:

  • Обязательные поля: В select запроса getList нужно обязательно включать все поля, которые используются в шаблоне ссылки (обычно это ID, CODE, IBLOCK_SECTION_ID). Если в шаблоне есть #SECTION_CODE#, вам придется добавить в select связь с секцией, например 'SECTION_CODE' => 'IBLOCK_SECTION.CODE'.
  • Element API vs ElementTable: Использование ElementCatalogTable предпочтительнее для новых проектов, так как это дает типизацию и упрощенную работу со свойствами. Но если вы пишете универсальный код или API_CODE не задан, \Bitrix\Iblock\ElementTable работает по тому же принципу.
  • Тип сущности: При вызове ReplaceDetailUrl последний параметр 'E' указывает, что мы работаем с элементом. Для разделов используется 'S'.

Итог

Для получения ссылки в D7 используйте выборку шаблона через связь IBLOCK.DETAIL_PAGE_URL и стандартный метод \CIBlock::ReplaceDetailUrl для подстановки данных. Это наиболее производительный и правильный способ в рамках Bitrix Framework.

Использование локальных баз MaxMind в GeoIp Bitrix для высокой точности

Использование локальных баз MaxMind в GeoIp Bitrix для высокой точности

Стандартные GeoIP-обработчики в 1С-Битрикс часто полагаются на внешние HTTP-сервисы, что вносит задержки в генерацию страницы и создает риски из-за блокировок API. Для проектов, ориентированных на высокую производительность, оптимальным решением является использование локального бинарного файла. Отечественного, к сожалению, актуального не нашёл, но MaxMind со своей бинарной БД (.mmdb) вполне подходит.

Контекст и проблема

При использовании облачных GeoIP-сервисов каждый запрос к сайту может инициировать внешнее соединение, что увеличивает TTFB (Time to First Byte). Кроме того, сторонние API могут быть ограничены для запросов из определенных регионов. Использование локальной базы данных позволяет обрабатывать запросы на стороне сервера со скоростью чтения из памяти, обеспечивая при этом актуальность данных для российских IP-адресов (но проверяйте страну для новых регионов, т.к. MaxMind - зарубежный сервис).

Решение

Для реализации необходимо скачать базу данных (например, бесплатную GeoLite2 City) в формате .mmdb и разместить её на сервере. В административной панели (Настройки -> Настройки продукта -> Геолокация) нужно активировать обработчик «MaxMind (бинарный файл)» и указать путь к файлу. Для скачивания БД с официального сайта MaxMind потребуется регистрация - она бесплатна, но можно поискать зеркала.

Программное взаимодействие осуществляется через Bitrix\Main\Service\GeoIp\Manager. Рекомендуется явно проверять наличие данных на русском языке, так как базы MaxMind по умолчанию содержат мультиязычные записи.

        declare(strict_types=1);

namespace Local\Services;

use Bitrix\Main\Service\GeoIp\Manager;
use Bitrix\Main\Service\GeoIp\MaxMind;
use Bitrix\Main\Error;

final class GeoService
{
    /**
     * Получает детальную информацию о местоположении через локальный MaxMind
     */
    static public function getCityInfo(string $ip = ''): ?array
    {
        if ($ip === '') {
            $ip = Manager::getRealIp();
        }

        // Можно принудительно использовать только локальный обработчик MaxMind
        // Класс обработчика: \Bitrix\Main\Service\GeoIp\GeoIP2
        // Важно не использовать здесь конструкцию \Bitrix\Main\Service\GeoIp\GeoIP2::class
        // т.к. она генерирует строку без ведущего слеша Bitrix\Main\Service\GeoIp\GeoIP2 , а в методе
        // getHandlerByClassName() проверка идёт строго с ведущим (facepalm)
        $handler = Manager::getHandlerByClassName('\\Bitrix\\Main\\Service\\GeoIp\\GeoIP2');

        if (!$handler || !$handler->isActive()) {
            return null;
        }

        // Запрашиваем данные с приоритетом на русском языке
        $result = $handler->getDataResult($ip, 'ru');

        if ($result && $result->isSuccess()) {
            $geoData = $result->getGeoData();

            return [
                'CITY' => $geoData->cityName,
                'REGION_CODE' => $geoData->regionCode,
                'COUNTRY' => $geoData->countryName,
                'IS_IN_RF' => ($geoData->countryCode === 'RU'),
            ];
        }

        return null;
    }
}

// Пример вызова сервиса для получения данных о геолокации
/*
$geoData = GeoService::getCityInfo();
if ($geoData) {
    echo "Ваш город: " . $geoData['CITY'];
}
*/

    

Важно:

При обновлении базы MaxMind вручную (через замену файла), Битрикс подхватит изменения мгновенно, так как менеджер инициализирует обработчик при каждом запросе. Для автоматизации обновления можно использовать кастомный скрипт, скачивающий базу, например, с зеркала и перезаписывающий файл в /local/share/geoip/.

Итог

Использование локального MaxMind через интерфейс GeoIp\Manager — это "золотой стандарт" для Битрикс-разработки. Вы получаете максимальную скорость работы, отсутствие внешних зависимостей и полную совместимость с ядром D7.

Блокировка заказа в Sale для защиты от гонок

Блокировка заказа в Sale для защиты от гонок

Проблема/контекст

В sale заказ одновременно могут менять несколько процессов: админка, AJAX-обработчик, обмен, агент. Без синхронизации вы получаете потерю изменений: один запрос перезаписывает поля/коллекции, которые уже изменил другой.

Решение с кодом

Используйте встроенную блокировку Bitrix\Sale\Order::lock()/unlock() и всегда снимайте её в finally. Перед блокировкой проверьте статус через getLockedStatus(): если заказ уже заблокирован другим пользователем — корректно завершайте операцию (HTTP 409/ошибка).

        <?php
declare(strict_types=1);

use Bitrix\Main\Context;
use Bitrix\Main\Loader;
use Bitrix\Sale\Order;

Loader::includeModule('sale');

$request = Context::getCurrent()->getRequest();

$orderId = (int)$request->getPost('orderId');
if ($orderId <= 0)
{
    throw new \Bitrix\Main\ArgumentException('Invalid orderId');
}

// 1) Проверяем, нет ли чужой блокировки
$lockStatus = Order::getLockedStatus($orderId)->getData(); // LOCK_STATUS, LOCKED_BY, DATE_LOCK
if (($lockStatus['LOCK_STATUS'] ?? null) === Order::SALE_ORDER_LOCK_STATUS_RED)
{
    // Здесь лучше вернуть 409 Conflict и показать, кем/когда заблокировано
    throw new \RuntimeException('Order is locked by another user');
}

$lockResult = Order::lock($orderId);
if (!$lockResult->isSuccess())
{
    throw new \RuntimeException('Failed to lock order: ' . implode('; ', $lockResult->getErrorMessages()));
}

try {
    // 2) Загружаем и меняем заказ строго внутри блокировки
    $order = Order::load($orderId);
    if (!$order)
    {
        throw new \RuntimeException('Order not found');
    }

    // Пример: безопасное изменение статуса и сохранение
    $r = $order->setField('STATUS_ID', 'P'); // ваш код статуса
    if (!$r->isSuccess())
    {
        throw new \RuntimeException('Status update failed: ' . implode('; ', $r->getErrorMessages()));
    }

    $save = $order->save();
    if (!$save->isSuccess())
    {
        throw new \RuntimeException('Order save failed: ' . implode('; ', $save->getErrorMessages()));
    }
} finally {
    // 3) Снимаем блокировку всегда, даже при исключениях
    Order::unlock($orderId);
}

    

Практические детали:

  • Блокируйте как можно ближе к месту изменения и держите блокировку минимальное время.
  • Не делайте внутри блокировки долгие операции (внешние API, генерация файлов, длительные расчёты). Сначала подготовьте данные, затем коротко: lock → load → change → save → unlock.
  • Если операция может выполняться без веб-пользователя (агент/CLI), заранее продумайте контекст прав: unlock() проверяет права текущего пользователя, поэтому тип выполнения важен.

Итог

Order::lock()/unlock() — простой способ избежать потери изменений при параллельных запросах. Ключевое правило — проверка статуса и снятие блокировки в finally.

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

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