Кофе && Код

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

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

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) используют этот подход.

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