DarkRiDDeR10 мин

Bitrix API. Форма с изображением: где заканчивается редактор и начинается файл

BitrixPHPФормы

В форме картинка уже видна, а после сохранения у товара остаётся старая обложка. Цена ошибки не только в пустом поле: оператор уверен, что обновил карточку, а каталог продолжает показывать не тот товар.

В октябрьском проекте я бы не начинал с повторного вызова редактора. Сначала разделил бы четыре состояния: выбор файла в браузере, данные формы, запись файла в Bitrix и ссылка на неё в элементе инфоблока. Пока они названы одним словом «картинка», причина прячется между двумя успешными шагами.

Один экран формы не означает один объект

Контрол \Bitrix\Main\UI\FileInput формирует интерфейс выбора и загрузки. Его show() возвращает разметку и JavaScript для страницы, но сам показ контрола не доказывает, что файл уже связан с нужным элементом. Официальная документация отдельно называет prepareFile() как способ получить файловый массив для дальнейшей обработки. Значит, после интерфейса всё равно остаётся серверный путь.

Для диагностики я записываю не красивый preview, а идентификаторы и границы. У выбранного в браузере файла нет постоянного Bitrix ID. У строки из b_file уже есть ID, но она может быть ни с чем не связана. У поля PREVIEW_PICTURE элемента есть отдельное состояние, и оно изменится только после успешного CIBlockElement::Update().

СостояниеКто им владеетЧто считаю доказательствомСледующий шаг
Файл выбран в диалогебраузер и DOM формывидно имя, размер или preview до отправкине считать это сохранением
Файловый массив в запросеPHP-обработчик$_FILES содержит ожидаемое поле и UPLOAD_ERR_OKпроверить лимит и передать в Bitrix
Файл зарегистрировантаблица b_fileCFile::SaveFile() вернул положительный IDполучить файл по ID и сохранить связь
Изображение карточки измененоэлемент инфоблокаCIBlockElement::Update() вернул trueзапросить элемент заново и открыть карточку
Карта состояний изображения: браузерный File, поле формы, зарегистрированный CFile ID и PREVIEW_PICTURE элемента инфоблока. Между этапами показаны POST, CFile SaveFile и CIBlockElement Update.
Preview нужен пользователю, но подтверждённой считается только связь между ID файла и полем элемента.

Контрол показываю с явными ограничениями

Ниже не универсальный шаблон, а минимальная точка проверки для формы с одной картинкой. Идентификатор текущего файла приходит из уже сохранённой карточки. Поле формы получает осмысленное имя, а ограничения задаются рядом с контролом. Если на старой установке отсутствует этот класс или часть источников FileInput отключена, сначала проверяю версию модуля main и права пользователя, а не копирую настройки вслепую.

<?php
use Bitrix\\Main\\UI\\FileInput;

$currentFileId = (int) $arResult['PREVIEW_PICTURE'];

echo FileInput::createInstance(array(
    'id' => 'catalog_preview',
    'name' => 'CATALOG[PREVIEW_PICTURE]',
    'upload' => true,
    'allowUpload' => FileInput::UPLOAD_IMAGES,
    'medialib' => false,
    'fileDialog' => true,
    'cloud' => false,
    'delete' => true,
    'edit' => true,
    'maxCount' => 1,
    'maxSize' => 5 * 1024 * 1024,
))->show($currentFileId);

Значение maxSize помогает пользователю раньше увидеть предел, но не заменяет серверную проверку. DOM может быть создан старым шаблоном, обновлён Ajax-ом или отправлен вручную. Поэтому имя поля и фактический массив, пришедший на сервер, я сверяю в тестовом запросе. В этом месте удобнее увидеть несовпадение CATALOG[PREVIEW_PICTURE] и обработчика, чем позже искать «потерянный» файл.

Сохраняю файл до привязки и проверяю оба ответа

В учебном обработчике ниже файл приходит из обычного multipart-поля. В проекте с FileInput вместо $_FILES может оказаться результат его подготовки, но контракт одинаковый: на входе — файловый массив, на выходе — подтверждённый ID или понятная ошибка. Я не сохраняю путь из браузера и не записываю имя файла как связь с карточкой.

function saveCatalogImage(array $upload)
{
    if (($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
        throw new RuntimeException('Image upload did not finish');
    }

    if ((int) ($upload['size'] ?? 0) < 1 || (int) $upload['size'] > 5 * 1024 * 1024) {
        throw new RuntimeException('Image size is outside the form limit');
    }

    $upload['MODULE_ID'] = 'catalog';
    $fileId = (int) CFile::SaveFile($upload, 'catalog');

    if ($fileId < 1 || !CFile::GetFileArray($fileId)) {
        throw new RuntimeException('Bitrix did not register the uploaded file');
    }

    return $fileId;
}

$fileId = saveCatalogImage($_FILES['CATALOG_PREVIEW']);

Метод CFile::SaveFile() регистрирует файл в b_file и возвращает числовой ID. Проверка через CFile::GetFileArray() здесь не украшение: она отделяет случай «обработчик получил форму» от случая «у нас есть объект, на который можно ссылаться». На старом проекте я также сохраняю в закрытый лог ID элемента, ID файла и текст LAST_ERROR, но не складываю в журнал сам файл или персональные поля формы.

$element = new CIBlockElement();
$picture = CFile::MakeFileArray($fileId);

$updated = $element->Update($elementId, array(
    'PREVIEW_PICTURE' => $picture,
));

if (!$updated) {
    throw new RuntimeException($element->LAST_ERROR);
}

Для обновления изображения инфоблока нужен файловый массив. CFile::MakeFileArray() умеет собрать его по существующему ID, а CIBlockElement::Update() возвращает false и оставляет текст ошибки в LAST_ERROR. Это две разные проверки; положительный $fileId не делает обновление элемента успешным сам по себе.

Проверяю путь в том порядке, в котором он ломается

  1. Открываю карточку с известным текущим ID картинки и отмечаю его до изменения.
  2. Выбираю небольшой тестовый JPEG и в браузерной сетевой вкладке сверяю имя поля и ответ отправки формы.
  3. На сервере временно фиксирую только код upload-ошибки, ID элемента и ID созданного файла.
  4. После Update() повторно читаю PREVIEW_PICTURE у этого же элемента, а не доверяю старому $arResult.
  5. Открываю карточку новым запросом в браузере и проверяю, что URL изображения указывает на ожидаемый файл.
  6. Только после этого удаляю временную диагностику или оставляю безопасный лог ошибки для следующего случая.

Не путаю замену с удалением

Самый неприятный крайний случай — форма отправлена без нового файла. Для одной карточки это может означать «сохранить старую картинку», а флаг удаления означает противоположное. Нельзя получать это решение из пустого preview в DOM: пустой preview может появиться из-за перерисовки формы. Политику формулирую явно: нет нового файла и нет флага удаления — поле остаётся как было; новый файл — заменяем после успешного сохранения; удаление — передаём в Bitrix отдельным согласованным полем.

Если загрузка и редактирование сделаны в два HTTP-запроса, появляется ещё одна граница. Пользователь может закрыть вкладку после первого запроса. Тогда созданный ID не должен автоматически становиться картинкой чужого элемента. В старой системе достаточно хранить ID в сессии или в черновике, сверять владельца при финальном сохранении и отдельно убирать неиспользованные файлы по согласованному регламенту. Это не повод усложнять маленькую форму очередями; это повод не считать временный файл завершённым результатом.

Ограничения примера

Здесь не задан общий список допустимых MIME-типов и размеров картинки: он зависит от каталога, старой версии Bitrix и требований редакторов. Ограничение в контроле не защищает сервер, поэтому реальные проверки типа, размера, прав и пределов PHP надо добавлять в обработчик. Также не стоит переносить пример в свойство типа файл без проверки формата PROPERTY_VALUES: документация CIBlockElement::Update() отдельно оговаривает работу с файловыми свойствами.

Итог

Редактор отвечает за выбор и preview, CFile — за зарегистрированный файл, а элемент инфоблока — за ссылку на него. Если проверить каждую передачу отдельно, «картинка была в форме» перестаёт быть ложным признаком готовности. В следующей правке достаточно повторить один тестовый POST и сравнить ID файла с PREVIEW_PICTURE после свежего чтения элемента.

Проверяемые источники

  • 1С-Битрикс: FileInput — описание контрола, его методов createInstance, prepareFile и show, а также параметров загрузки
  • 1С-Битрикс: CFile::SaveFile — метод сохраняет файловый массив и регистрирует его в b_file, возвращая числовой идентификатор
  • 1С-Битрикс: CFile::MakeFileArray — формирует файловый массив, в том числе по ID существующего файла
  • 1С-Битрикс: CIBlockElement::Update — Update принимает массив полей, возвращает true или false, а текст ошибки остаётся в LAST_ERROR
  • PHP Manual: $_FILES — структура данных, переданных HTTP POST с файлом
  • PHP Manual: move_uploaded_file — функция работает только с файлом, который PHP признал HTTP POST upload; источник нужен для границы временного файла