В форме картинка уже видна, а после сохранения у товара остаётся старая обложка. Цена ошибки не только в пустом поле: оператор уверен, что обновил карточку, а каталог продолжает показывать не тот товар.
В октябрьском проекте я бы не начинал с повторного вызова редактора. Сначала разделил бы четыре состояния: выбор файла в браузере, данные формы, запись файла в 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_file | CFile::SaveFile() вернул положительный ID | получить файл по ID и сохранить связь |
| Изображение карточки изменено | элемент инфоблока | CIBlockElement::Update() вернул true | запросить элемент заново и открыть карточку |
Контрол показываю с явными ограничениями
Ниже не универсальный шаблон, а минимальная точка проверки для формы с одной картинкой. Идентификатор текущего файла приходит из уже сохранённой карточки. Поле формы получает осмысленное имя, а ограничения задаются рядом с контролом. Если на старой установке отсутствует этот класс или часть источников 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 не делает обновление элемента успешным сам по себе.
Проверяю путь в том порядке, в котором он ломается
- Открываю карточку с известным текущим ID картинки и отмечаю его до изменения.
- Выбираю небольшой тестовый JPEG и в браузерной сетевой вкладке сверяю имя поля и ответ отправки формы.
- На сервере временно фиксирую только код upload-ошибки, ID элемента и ID созданного файла.
- После
Update()повторно читаюPREVIEW_PICTUREу этого же элемента, а не доверяю старому$arResult. - Открываю карточку новым запросом в браузере и проверяю, что URL изображения указывает на ожидаемый файл.
- Только после этого удаляю временную диагностику или оставляю безопасный лог ошибки для следующего случая.
Не путаю замену с удалением
Самый неприятный крайний случай — форма отправлена без нового файла. Для одной карточки это может означать «сохранить старую картинку», а флаг удаления означает противоположное. Нельзя получать это решение из пустого 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; источник нужен для границы временного файла