Кофе && Код

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

Избавляемся от 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 редактирования. Подход масштабируется для любых специфических форматов данных.

InsertIgnore стратегия для безопасной вставки без дублирования

InsertIgnore стратегия для безопасной вставки без дублирования

Проблема дублирования записей

При массовой вставке данных в базу через ORM часто возникает проблема дублирования по уникальным полям. Стандартный метод add() генерирует ошибку при попытке вставить запись с существующим первичным ключом или уникальным индексом. Приходится вручную проверять существование записи через getList() перед вставкой, что неэффективно при массовых операциях.

Стратегия InsertIgnore

С версии 22.0 в Bitrix ORM появилась стратегия InsertIgnore, которая использует SQL-конструкцию INSERT IGNORE. Если запись с таким ключом существует, она игнорируется без ошибки. Это атомарная операция на уровне СУБД, что обеспечивает корректность даже при конкурентных запросах.

Переопределение стратегии в Table-классе

Самый простой способ — переопределить метод getAddStrategy() в вашем Table-классе:

namespace Mycompany\MyModule;

use Bitrix\Main\ORM\Data\DataManager;
use Bitrix\Main\ORM\Data\AddStrategy;

class UserTokenTable extends DataManager
{
    public static function getTableName()
    {
        return 'b_user_token';
    }
    
    public static function getMap()
    {
        return [
            'ID' => ['data_type' => 'integer', 'primary' => true, 'autocomplete' => true],
            'USER_ID' => ['data_type' => 'integer', 'required' => true],
            'TOKEN' => ['data_type' => 'string', 'required' => true],
        ];
    }
    
    // Переопределяем стратегию для всех операций add()
    protected static function getAddStrategy(): AddStrategy\Contract\AddStrategy
    {
        return new AddStrategy\InsertIgnore(static::getEntity());
    }
}

// Теперь add() использует INSERT IGNORE автоматически
$result = UserTokenTable::add([
    'ID' => 1,
	  'USER' => 1,
    'TOKEN' => 'abc123'
]);

// Если запись существует - игнорируется без ошибки
// $result->isSuccess() вернёт true
// $result->getId() вернёт ID существующей записи

Массовая вставка с InsertIgnore

Для массовой вставки данных используйте метод addMulti():

use Bitrix\MyModule\UserTokenTable;

$tokens = [
    ['ID' => 10, 'USER_ID' => 1, 'TOKEN' => 'token1'],
    ['ID' => 11, 'USER_ID' => 2, 'TOKEN' => 'token2'],
    ['ID' => 12, 'USER_ID' => 3, 'TOKEN' => 'token3'],
    ['ID' => 10, 'USER_ID' => 1, 'TOKEN' => 'token1'], // дубликат - будет проигнорирован
];

// Все записи вставляются одним запросом
$result = UserTokenTable::addMulti($tokens);

// Проверка успешности
if ($result->isSuccess()) {
    // Операция выполнена, дубликаты проигнорированы
}

Указание уникальных полей

По умолчанию InsertIgnore проверяет первичный ключ. Если нужно проверять другие поля, передайте их в конструктор:

protected static function getAddStrategy(): AddStrategy\Contract\AddStrategy
{
    // Проверять уникальность по полям USER_ID и TOKEN
    return new AddStrategy\InsertIgnore(
        static::getEntity(),
        ['USER_ID', 'TOKEN']
    );
}

Проверка изменений базы

Метод isDBChanged() в результате показывает, была ли реально изменена база:

$result = UserTokenTable::add(['USER_ID' => 1, 'TOKEN' => 'abc']);

if ($result->isSuccess()) {
    if ($result->getData()['isDBChanged']) {
        // Запись была вставлена
    } else {
        // Запись уже существовала и была проигнорирована
    }
}

Важные ограничения

InsertIgnore работает только с таблицами, имеющими первичный ключ или уникальный индекс. Стратегия не поддерживает таблицы без ограничений уникальности. При попытке использовать InsertIgnore на несовместимой таблице будет выброшено исключение NotSupportedException.

Когда использовать InsertIgnore

Применяйте InsertIgnore для импорта данных, синхронизации справочников, сохранения логов без дублей, кэширования токенов. Для случаев, когда нужно обновить существующую запись, используйте стратегию Merge вместо InsertIgnore.

Выбор между send и sendImmediate для почтовых событий

Выбор между send и sendImmediate для почтовых событий

При отправке почтовых событий в 1С-Битрикс разработчики часто используют старый API CEvent::Send(), не зная о современном классе Bitrix\Main\Mail\Event и его двух принципиально разных методах отправки. Неправильный выбор метода приводит к блокировке выполнения скрипта при проблемах с почтовым сервером или избыточной нагрузке на базу данных.

Метод send(): отложенная отправка

Метод send() добавляет событие в очередь отправки (таблица b_event). Агент Битрикс обработает событие позже:

use Bitrix\Main\Mail\Event;

// Добавление в очередь с автоматическим выбором шаблона
$result = Event::send([
    'EVENT_NAME' => 'ORDER_CONFIRM',
    'LID' => 's1',
    'C_FIELDS' => [
        'ORDER_ID' => $orderId,
        'USER_EMAIL' => $email,
        'ORDER_LIST' => $orderHtml,
    ],
]);

// Проверка результата
if ($result->isSuccess()) {
    $eventId = $result->getId();
} else {
    $errors = $result->getErrorMessages();
}

Метод send() возвращает объект Result с методами проверки успешности и получения ошибок, что упрощает обработку по сравнению со старым API.

Отправка конкретного почтового шаблона

Для отправки определённого шаблона используйте параметр MESSAGE_ID:

// Отправка конкретного шаблона (ID из таблицы b_event_message)
$result = Event::send([
    'EVENT_NAME' => 'ORDER_CONFIRM',
    'LID' => 's1',
    'MESSAGE_ID' => 5, // ID конкретного шаблона
    'C_FIELDS' => [
        'ORDER_ID' => $orderId,
        'USER_EMAIL' => $email,
    ],
]);

Без MESSAGE_ID система выберет все активные шаблоны по EVENT_NAME и LID. С MESSAGE_ID будет использован только указанный шаблон, даже если есть другие для этого типа события.

Метод sendImmediate(): немедленная отправка

Метод sendImmediate() отправляет письмо синхронно, минуя очередь:

use Bitrix\Main\Mail\Event;

// Немедленная отправка с конкретным шаблоном
$sendResult = Event::sendImmediate([
    'EVENT_NAME' => 'CRITICAL_ALERT',
    'LID' => 's1',
    'MESSAGE_ID' => 12, // Опционально: конкретный шаблон
    'C_FIELDS' => [
        'MESSAGE' => 'Критическая ошибка в системе',
        'TIME' => date('Y-m-d H:i:s'),
    ],
]);

// Результат - строковая константа
if ($sendResult === Event::SEND_RESULT_SUCCESS) {
    // Отправлено успешно
} elseif ($sendResult === Event::SEND_RESULT_ERROR) {
    // Ошибка отправки
} elseif ($sendResult === Event::SEND_RESULT_TEMPLATE_NOT_FOUND) {
    // Шаблон не найден
}

Используйте sendImmediate() для критических уведомлений в фоновых задачах (агенты, cron), когда письмо должно уйти немедленно без сохранения в очередь.

Метод sendImmediate() возвращает строку, в отличие от send(), а не объект Result.

Обработка вложений

Оба метода поддерживают массив вложений в параметре FILE:

$result = Event::send([
    'EVENT_NAME' => 'DOCUMENT_READY',
    'LID' => ['s1', 's2'], // Множественные сайты
    'C_FIELDS' => ['DOC_NUMBER' => '123'],
    'FILE' => [
        456, // ID из b_file (не копируется)
        '/path/to/new.pdf', // Новый файл (будет скопирован)
    ],
]);

Система автоматически определяет тип вложения: числовые значения считаются ID файлов, строки - путями к файлам для копирования.

Итог

Используйте send() для пользовательских сценариев и sendImmediate() для фоновых задач. Параметр MESSAGE_ID позволяет точно контролировать, какой шаблон будет использован.

Безопасная работа с JSON через Bitrix\Main\Web\Json

Безопасная работа с JSON через Bitrix\Main\Web\Json

Проблема

При работе с JSON в 1С-Битрикс многие разработчики используют нативные функции json_encode() и json_decode() без должной обработки ошибок. Это приводит к silent-ошибкам: когда данные не кодируются корректно, функция возвращает false или null, но код продолжает выполняться. В результате возникают сложно диагностируемые проблемы с невалидными данными.

Решение

Класс Bitrix\Main\Web\Json предоставляет безопасные методы для работы с JSON, автоматически выбрасывая исключения при ошибках кодирования/декодирования.

Базовое использование

use Bitrix\Main\Web\Json;
use Bitrix\Main\ArgumentException;

// Безопасное кодирование
try {
    $jsonString = Json::encode($data);
} catch (ArgumentException $e) {
    // Обработка ошибки кодирования
}

// Безопасное декодирование
try {
    $array = Json::decode($jsonString);
} catch (ArgumentException $e) {
    // Обработка ошибки декодирования
}

Преимущества класса

Класс автоматически использует оптимальные опции по умолчанию:

  • JSON_HEX_TAG, JSON_HEX_AMP, JSON_HEX_APOS, JSON_HEX_QUOT — защита от XSS
  • JSON_UNESCAPED_UNICODE — корректная работа с кириллицей
  • JSON_INVALID_UTF8_SUBSTITUTE — замена невалидных символов UTF-8
  • JSON_THROW_ON_ERROR — автоматический выброс исключений

Валидация JSON

Класс предоставляет метод для проверки валидности JSON-строки:

use Bitrix\Main\Web\Json;
use Bitrix\Main\Application;

$request = Application::getInstance()->getContext()->getRequest();
$jsonString = $request->getPost('json_data') ?? '';

if (!Json::validate($jsonString)) {
    throw new \Bitrix\Main\ArgumentException(
        "Невалидный JSON в параметре json_data"
    );
}

$data = Json::decode($jsonString);

Кастомные опции кодирования

Вы можете переопределить опции при необходимости:

use Bitrix\Main\Web\Json;

// Форматированный вывод с отступами
$prettyJson = Json::encode(
    $data, 
    Json::DEFAULT_OPTIONS | JSON_PRETTY_PRINT
);

// Без экранирования слешей для URL
$jsonForApi = Json::encode(
    ['url' => 'https://example.com/path'],
    Json::DEFAULT_OPTIONS | JSON_UNESCAPED_SLASHES
);

Итог

Использование Bitrix\Main\Web\Json вместо нативных функций обеспечивает явную обработку ошибок и автоматическую защиту от XSS. Метод validate() позволяет безопасно проверять пользовательский ввод перед декодированием.

Правильная работа с Bitrix\Main\Result для обработки ошибок

Правильная работа с Bitrix\Main\Result для обработки ошибок

Проблема

Многие разработчики до сих пор возвращают булевы значения или массивы из своих методов, игнорируя стандартный класс Bitrix\Main\Result. Это приводит к неединообразной обработке ошибок, усложняет отладку и делает код несовместимым с современным API Битрикс.

Решение

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

Базовое использование

use Bitrix\Main\Result;
use Bitrix\Main\Error;

function processOrder(int $orderId): Result
{
    $result = new Result();
    
    // Проверка существования заказа
    if (!$order = getOrder($orderId)) {
        $result->addError(new Error('Заказ не найден', 'ORDER_NOT_FOUND'));
        return $result;
    }
    
    // Проверка статуса
    if ($order['STATUS'] === 'PAID') {
        $result->addError(new Error('Заказ уже оплачен', 'ORDER_ALREADY_PAID'));
        return $result;
    }
    
    // Успешная обработка
    $result->setData([
        'order_id' => $orderId,
        'amount' => $order['PRICE'],
        'timestamp' => time()
    ]);
    
    return $result;
}

Проверка результата

$result = processOrder(123);

if ($result->isSuccess()) {
    $data = $result->getData();
    echo "Заказ обработан: " . $data['order_id'];
} else {
    // Получение всех ошибок
    foreach ($result->getErrors() as $error) {
        echo $error->getCode() . ': ' . $error->getMessage();
    }
    
    // Или только первой ошибки
    $error = $result->getError();
    echo $error->getMessage();
    
    // Или всех сообщений одним массивом
    $messages = $result->getErrorMessages();
}

Добавление множественных ошибок

function validateOrderData(array $data): Result
{
    $result = new Result();
    $errors = [];
    
    if (empty($data['email'])) {
        $errors[] = new Error('Email обязателен', 'EMAIL_REQUIRED');
    }
    
    if (empty($data['phone'])) {
        $errors[] = new Error('Телефон обязателен', 'PHONE_REQUIRED');
    }
    
    if (!empty($errors)) {
        $result->addErrors($errors);
    }
    
    return $result;
}

Передача ошибок между методами

function createUser(array $userData): Result
{
    $result = new Result();
    
    // Валидация
    $validationResult = validateUserData($userData);
    if (!$validationResult->isSuccess()) {
        $result->addErrors($validationResult->getErrors());
        return $result;
    }
    
    // Создание пользователя
    // ...
    
    return $result;
}

Дополнительные данные в ошибках

$error = new Error(
    'Недостаточно товара на складе',
    'INSUFFICIENT_STOCK',
    ['available' => 5, 'requested' => 10] // customData
);

// Получение дополнительных данных
$customData = $error->getCustomData();
echo "Доступно: " . $customData['available'];

Итог

Использование Result делает код предсказуемым, упрощает обработку ошибок и соответствует стандартам Битрикс. Все современные модули (Sale, Catalog, CRM) используют этот подход.