Реклама
Реклама
Реклама
Реклама. Zarub Zarub - зарубежные сервисы, оплата в рублях

Bitrix D7 Web\Json: encode, decode и типичные ошибки

В модулях Битрикс JSON встречается на каждом шагу: ответ REST-контроллера, тело для HttpClient, кеш настроек, очередь задач, обмен с Bitrix24. Писать везде «голый» json_encode/json_decode с разными флагами — быстрый путь к тихим false, кракозябрам в кириллице и двойному экранированию. Нормальный контракт ядра — Bitrix\Main\Web\Json.

Рядом по теме: HttpClient, Application, Context, Request, Result и Error, REST API в D7. Ниже — encode/decode, опции, обработка ошибок и типичные грабли модулей на проде.

Зачем Json, а не json_encode напрямую

json_encode в PHP молча возвращает false при ошибке (невалидный UTF-8, рекурсия, INF/NAN). В логах это выглядит как «пустой ответ», а на фронте — 500 без понятного текста. Bitrix\Main\Web\Json:

  • задаёт единый набор флагов по умолчанию (Unicode без \uXXXX, экранирование опасных символов для HTML-контекста);
  • при ошибке encode/decode бросает исключение, а не отдаёт false/null;
  • одинаково используется в ядре, модулях и вашем local/ — меньше зоопарка флагов.

Для разового скрипта CLI json_encode допустим. В модуле и API — берите Json.

Минимальный encode и decode

Класс статический, отдельный объект не создаёте:

<?php
use Bitrix\Main\Web\Json;
use Bitrix\Main\ArgumentException;

$payload = [
    'orderId' => 10542,
    'status' => 'paid',
    'items' => [
        ['sku' => 'A-100', 'qty' => 2, 'title' => 'Наушники'],
    ],
];

try {
    $json = Json::encode($payload);
    // {"orderId":10542,"status":"paid","items":[{"sku":"A-100","qty":2,"title":"Наушники"}]}
} catch (ArgumentException $e) {
    // невалидные данные / кодировка — не глотайте исключение молча
    throw $e;
}

try {
    $data = Json::decode($json); // ассоциативный массив по умолчанию
    $orderId = (int)$data['orderId'];
} catch (ArgumentException $e) {
    // битый JSON от внешней системы
    AddMessage2Log($e->getMessage(), 'local.json');
}

Второй аргумент decode — $assoc (по умолчанию true). Если нужны объекты stdClass, передайте false. В модулях почти всегда удобнее массив.

Опции encode: дефолт ядра и свои флаги

Если второй аргумент encode не передан, ядро использует свой набор (в актуальных версиях main — комбинация с JSON_UNESCAPED_UNICODE и HEX-экранированием <, &, кавычек). Это безопасно, когда JSON попадает в HTML/шаблон, и читаемо для кириллицы.

Когда нужен «красивый» дамп для отладки или файлового экспорта:

<?php
use Bitrix\Main\Web\Json;

$pretty = Json::encode(
    $payload,
    JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
);

// Компактный вариант без HEX (если JSON уходит только в HTTP-тело, не в HTML):
$compact = Json::encode(
    $payload,
    JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);

Не смешивайте в одном проекте три стиля флагов «как получилось». Выберите контракт модуля (например, всегда Unicode + unescaped slashes для API) и держитесь его. Для ответа браузеру/шаблону оставляйте дефолт ядра или явный HEX.

Ответ HTTP: Content-Type и тело

Encode сам по себе заголовки не ставит. Для ручного endpoint или ajax-обработчика:

<?php
use Bitrix\Main\Application;
use Bitrix\Main\Web\Json;

$app = Application::getInstance();
$response = $app->getContext()->getResponse();

$data = ['ok' => true, 'id' => 42];

$response->addHeader('Content-Type', 'application/json; charset=UTF-8');
$response->flush(Json::encode($data));
// либо в старом стиле ajax:
// header('Content-Type: application/json; charset=UTF-8');
// echo Json::encode($data);
// die();

В контроллерах Engine (REST/маршруты D7) массив из action сериализуется ядром — там не дублируйте echo Json::encode, иначе получите двойную обёртку. Ручной Json::encode нужен, когда вы сами формируете тело (кастомный ajax, вебхук наружу, файл).

Подробнее про Response и заголовки — в статье про Application и Context.

Связка с HttpClient

Исходящий POST JSON — классика. Тело строкой, заголовок явно:

<?php
use Bitrix\Main\Web\HttpClient;
use Bitrix\Main\Web\Json;
use Bitrix\Main\ArgumentException;

$http = new HttpClient([
    'socketTimeout' => 5,
    'streamTimeout' => 15,
]);
$http->setHeader('Content-Type', 'application/json; charset=UTF-8');
$http->setHeader('Accept', 'application/json');

$payload = ['event' => 'order.paid', 'orderId' => 10542];

try {
    $body = $http->post(
        'https://erp.example/api/hooks',
        Json::encode($payload)
    );
} catch (ArgumentException $e) {
    // не смогли даже сериализовать — внешний вызов не начинаем
    throw $e;
}

if ($body === false) {
    throw new \RuntimeException('HTTP transport: ' . implode('; ', $http->getError()));
}

$status = $http->getStatus();
if ($status < 200 || $status >= 300) {
    throw new \RuntimeException('HTTP status=' . $status . ' body=' . mb_substr((string)$body, 0, 500));
}

try {
    $result = Json::decode((string)$body);
} catch (ArgumentException $e) {
    throw new \RuntimeException('Invalid JSON from ERP: ' . $e->getMessage());
}

Не передавайте PHP-массив в post() при Content-Type: application/json — клиент уйдёт в form-urlencoded. Полный разбор таймаутов и статусов — в гайде по HttpClient.

UTF-8 и «кракозябры»

JSON по спецификации — Unicode. Типичные проблемы на проектах Битрикс:

  • строка из БД/файла в windows-1251 попала в encode — PHP 8+ чаще падает с ошибкой, на старых версиях мог получиться false;
  • двойной encode: уже JSON-строку снова прогнали через Json::encode — в теле появляются \" и лишний уровень кавычек;
  • ответ отдали с Content-Type: text/html без charset — браузер «угадал» кодировку;
  • в лог писали сырой binary/emoji без truncate — журнал раздулся.

Практичный хелпер перед encode, если источник «грязный»:

<?php
use Bitrix\Main\Web\Json;
use Bitrix\Main\Text\Encoding;

function toUtf8Deep($value)
{
    if (is_array($value)) {
        foreach ($value as $k => $v) {
            $value[$k] = toUtf8Deep($v);
        }
        return $value;
    }
    if (is_string($value) && !mb_check_encoding($value, 'UTF-8')) {
        return Encoding::convertEncoding($value, SITE_CHARSET, 'UTF-8');
    }
    return $value;
}

$json = Json::encode(toUtf8Deep($payload));

На чистом UTF-8 проекте (современный Битрикс) хелпер обычно не нужен — важнее не смешивать charset при чтении внешних CSV/XML.

Ошибки encode/decode: не глотать false

Антипример, который до сих пор копируют из старых сниппетов:

<?php
// Плохо: тихий провал
$json = json_encode($data);
file_put_contents($path, $json); // может записать пустую строку/false

$data = json_decode($raw, true);
if (!$data) {
    // пустой массив [] тоже «ложный» — логика ломается
}

Нормальный путь с Json + явным контрактом ошибок (удобно стыковать с Result/Error):

<?php
use Bitrix\Main\Result;
use Bitrix\Main\Error;
use Bitrix\Main\Web\Json;
use Bitrix\Main\ArgumentException;

function decodeApiBody(string $raw): Result
{
    $result = new Result();
    if ($raw === '') {
        $result->addError(new Error('Empty body'));
        return $result;
    }
    try {
        $data = Json::decode($raw);
        $result->setData(is_array($data) ? $data : ['value' => $data]);
    } catch (ArgumentException $e) {
        $result->addError(new Error('JSON: ' . $e->getMessage()));
    }
    return $result;
}

Пустой объект {} и пустой массив [] — валидный JSON. Не проверяйте успех через if (!$data).

Большие payload и память

Json::encode/decode держат всё в памяти. Для выгрузки каталога на десятки тысяч позиций:

  • не собирайте один гигантский массив «все товары» — отдавайте чанками / NDJSON / потоковую запись;
  • лимит memory_limit на агентах часто ниже, чем на хитах FPM — тестируйте на проде-подобных объёмах;
  • в лог никогда не пишите полный JSON заказа с ПДн: mb_substr($json, 0, 500) или только id/status;
  • если JSON кладёте в Option или кеш — следите за размером: Option не файловое хранилище.

Тело запроса: получить JSON из Request

Входящий webhook или свой ajax часто присылает raw JSON. Не читайте php://input вразнобой — через Context это предсказуемее:

<?php
use Bitrix\Main\Application;
use Bitrix\Main\Web\Json;
use Bitrix\Main\ArgumentException;

$request = Application::getInstance()->getContext()->getRequest();
$raw = $request->getInput(); // тело как строка

if ($raw === '' || $raw === null) {
    // иногда клиент шлёт form — тогда смотрите getPostList / getJsonList в вашей версии
    http_response_code(400);
    echo Json::encode(['error' => 'empty_body']);
    return;
}

try {
    $data = Json::decode($raw);
} catch (ArgumentException $e) {
    http_response_code(400);
    echo Json::encode(['error' => 'invalid_json', 'message' => $e->getMessage()]);
    return;
}

// дальше валидация полей, не «доверие к массиву»
$event = (string)($data['event'] ?? '');
$orderId = (int)($data['orderId'] ?? 0);

Проверяйте Content-Type на входе, если endpoint публичный: неожиданный multipart/form-data при ожидании JSON — повод вернуть 415, а не пытаться «угадать» парсер. CSRF для браузерных сессий — отдельно (bitrix_sessid / Signed); для server-to-server webhook обычно токен в заголовке.

JSON в файле и в Option

Иногда конфиг или снимок состояния кладут в файл. Encode + атомарная запись:

<?php
use Bitrix\Main\Web\Json;
use Bitrix\Main\IO\File;
use Bitrix\Main\IO\Directory;

$dir = $_SERVER['DOCUMENT_ROOT'] . '/upload/local_export/';
if (!Directory::isDirectoryExists($dir)) {
    Directory::createDirectory($dir);
}

$path = $dir . 'orders-snapshot.json';
$tmp  = $path . '.tmp';

$json = Json::encode($snapshot, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
File::putFileContents($tmp, $json);
rename($tmp, $path); // атомарнее, чем писать сразу в целевой файл

Чтение обратно — File::getFileContents + Json::decode в try/catch. Для небольших настроек модуля чаще достаточно Option::set со строкой JSON, но помните лимиты размера Option и то, что это не очередь и не файловое хранилище логов. Крупные структуры лучше в файл, highload-блок или свою таблицу.

Связанный разбор работы с путями — в статье про Main\IO; настройки модуля — в Option и Configuration.

Типичные ошибки модулей на проде

  • Тихий false. Остались на json_encode без проверки json_last_error(). Переходите на Json::encode и ловите исключение.
  • Двойной encode. Сначала Json::encode($arr), потом эту строку кладут в массив и encode снова — API получает строку вместо объекта.
  • Двойной decode. Engine уже отдал массив, разработчик ещё раз Json::decode «на всякий».
  • Неверный Content-Type. Тело JSON, а заголовок text/html или form-urlencoded — клиенты и CDN ведут себя сюрпризно.
  • Массив в HttpClient при JSON Content-Type. Уходит не то тело.
  • Проверка if (!$data) после decode. Ломает валидные [] и 0/false в JSON.
  • Разные флаги Unicode в соседних сервисах — то кириллица читаемая, то \u0410, то сломанный diff в git.
  • Логируют секреты. JSON с token/password целиком в AddMessage2Log.

PHP json_encode с JSON_THROW_ON_ERROR — чем отличается

В PHP 7.3+ можно писать json_encode($data, JSON_THROW_ON_ERROR) и ловить JsonException. Это лучше тихого false, но в проекте на Битрикс всё равно предпочтительнее Bitrix\Main\Web\Json: те же исключения в стиле ядра (ArgumentException), единые дефолтные флаги и меньше сюрпризов при code review («почему тут throw-флаг, а тремя файлами ниже — нет»). Оставляйте нативный json_* для изолированных CLI-утилит вне runtime Битрикс.

Быстрая отладка

<?php
use Bitrix\Main\Web\Json;
use Bitrix\Main\ArgumentException;

function debugJson(string $label, $value): void
{
    try {
        if (is_string($value)) {
            $decoded = Json::decode($value);
            AddMessage2Log([
                'label' => $label,
                'kind' => 'string-json',
                'preview' => mb_substr($value, 0, 400),
                'keys' => is_array($decoded) ? array_keys($decoded) : gettype($decoded),
            ], 'local.json');
        } else {
            $json = Json::encode($value);
            AddMessage2Log([
                'label' => $label,
                'kind' => 'php-value',
                'preview' => mb_substr($json, 0, 400),
            ], 'local.json');
        }
    } catch (ArgumentException $e) {
        AddMessage2Log(['label' => $label, 'error' => $e->getMessage()], 'local.json');
    }
}

На стейдже этого достаточно. На проде после разбора инцидента отключайте — журнал /upload/ быстро растёт.

Краткий чеклист

  1. В модуле — Bitrix\Main\Web\Json, не россыпь json_encode с разными флагами.
  2. Ошибки encode/decode обрабатывайте через try/catch или Result, не через if (!$data).
  3. Для HTTP: явный Content-Type: application/json; charset=UTF-8 + строка из Json::encode.
  4. Исходящие вызовы — через HttpClient с таймаутами.
  5. Не логируйте полные payload с ПДн и токенами; режьте preview.
  6. Большие выгрузки — чанками, не одним encode на весь каталог.

Вывод

Bitrix\Main\Web\Json — тонкая, но важная обёртка: единые флаги, исключения вместо тихого false, понятный контракт для модулей. Связка «Json сериализует → HttpClient отправляет → Result описывает исход» закрывает большинство интеграционных кейсов без сюрпризов на проде. Адреса собирайте через Uri, HTTP-контекст берите из Application/Context, ошибки API оформляйте через Result/Error.