{ "index": 332, "slug": "bitrix-api-add-foto-editor", "title": "Bitrix API: как встроить редактор изображений и сохранить файл в элементе", "excerpt": "FileInput показывает выбранную картинку, но не сохраняет её сам. Разбираем контракт формы, проверку загрузки и привязку файла к PREVIEW_PICTURE в Bitrix.", "contentHtml": "

В форме Bitrix пользователь выбирает новую фотографию и сразу видит preview. После нажатия «Сохранить» страница открывается со старым изображением. Иногда поле остаётся пустым. В файловом хранилище при этом появляются новые записи, которые не привязаны к элементу.

\n

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

\n

Тезис. Bitrix\\Main\\UI\\FileInput решает задачу интерфейса. Он не доказывает, что файл записан в нужное поле сущности. Готовность наступает только после трёх подтверждений: запрос принял ожидаемый файл, Bitrix зарегистрировал его и повторное чтение элемента вернуло этот ID в PREVIEW_PICTURE или другое согласованное свойство.

\n

Где возникает разрыв

\n

У одной картинки есть несколько состояний. Браузер хранит выбранный объект File. HTML-форма передаёт его как multipart-часть. PHP получает массив в $_FILES. Bitrix создаёт запись в b_file и возвращает числовой ID. Элемент инфоблока хранит ссылку на этот ID в поле изображения. Ни один этап не заменяет следующий.

\n

Preview относится к DOM. Он может измениться до отправки формы, после Ajax-перерисовки или даже при ошибочном ответе сервера. Поэтому текст «файл выбран» и URL картинки в интерфейсе нельзя использовать как доказательство сохранения. Сервер должен получить конкретное поле, проверить его и записать связь.

\n
Состояния изображения и границы проверки
СостояниеПричина сбояПроверкаДействие
Preview обновилсяИзменился только DOMОткрыть Network и найти multipart-полеНе считать файл сохранённым
В $_FILES нет поляНеверное name или форма без enctypeПроверить имя input, метод и Request PayloadИсправить контракт формы
Upload завершился ошибкойЛимит PHP, размер или права каталогаПроверить error, size и серверный логВернуть ошибку формы и остановить запись
Есть ID файла, но карточка прежняяНе вызван Update() или передано не то полеПовторно прочитать элемент и поле изображенияСохранить файловый массив в сущность
После удаления картинка вернуласьУдаление выведено из пустого previewПроверить явный флаг удаления в POSTРазвести ветки «оставить», «заменить» и «удалить»
\n
\"Интерфейс
Preview помогает выбрать файл, но итогом считается только связь зарегистрированного ID с элементом Bitrix.
\n

Контракт FileInput

\n

Контрол создают на сервере. Имя поля выбирают вместе с обработчиком. Если шаблон отправляет CATALOG_PREVIEW, PHP не должен ждать picture. Такая ошибка выглядит как «Bitrix не загрузил картинку», хотя файл просто не попал в ожидаемую ветку.

\n

Ниже учебный пример для формы с одной картинкой. Он показывает состав параметров, а не готовую конфигурацию конкретного проекта. Константы, доступность источников и набор опций нужно сверить с версией модуля main и установленными правами.

\n
<?php\nuse Bitrix\\Main\\UI\\FileInput;\n\n$currentFileId = (int) $arResult['PREVIEW_PICTURE'];\n\necho FileInput::createInstance(array(\n    'id' => 'catalog_preview',\n    'name' => 'CATALOG_PREVIEW',\n    'upload' => true,\n    'allowUpload' => FileInput::UPLOAD_IMAGES,\n    'maxCount' => 1,\n    'maxSize' => 5 * 1024 * 1024,\n    'delete' => true,\n))->show($currentFileId);\n?>
\n

name связывает разметку с серверным кодом. upload включает загрузку. allowUpload сужает сценарий до изображений, если это поддерживает установленная версия API. maxCount и maxSize уменьшают число ошибочных действий в интерфейсе. Они не заменяют серверную проверку: запрос можно отправить вручную.

\n

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

\n

Форма должна отправить файл

\n

Обычная HTML-форма с файлом использует method=\"post\" и enctype=\"multipart/form-data\". Для Ajax нужен объект FormData. Вызов jQuery serialize() собирает текстовые поля, но не переносит бинарное содержимое файла. Если заменить multipart-запрос сериализацией, preview останется, а сервер получит только остальную форму.

\n

В legacy-шаблоне частая ошибка появляется после .html(). Старый input удаляют, новый вставляют, а обработчик остаётся привязан к уничтоженному узлу. Повторная инициализация добавляет второй обработчик. Делегируйте событие стабильному контейнеру и снимайте только свой namespace.

\n
(function ($) {\n  function bindPhotoEditor(root) {\n    var $root = $(root);\n\n    $root.off('change.photoEditor', 'input[name=CATALOG_PREVIEW]')\n      .on('change.photoEditor', 'input[name=CATALOG_PREVIEW]', function () {\n        var file = this.files && this.files[0];\n        $root.find('.js-photo-status').text(\n          file ? 'Файл выбран. Сохранение ещё не выполнено.' : 'Файл не выбран.'\n        );\n      });\n  }\n\n  $(function () {\n    bindPhotoEditor(document);\n  });\n}(jQuery));
\n

Этот фрагмент учебный. Он меняет статус в DOM и не создаёт ID файла. Проверить его можно по одному простому признаку: после повторной отрисовки контейнера изменение файла вызывает один статус, а Network показывает один запрос при сохранении. Общий off('change') здесь опасен: он может снять обработчики других компонентов.

\n

Серверный путь: файл, затем связь

\n

Обработчик не должен брать «последний созданный файл» из базы. В форме одновременно работают несколько пользователей. Связь строят из данных конкретного запроса и конкретного элемента.

\n

Сначала проверяют результат загрузки и входные ограничения. Поле type из запроса — это заявление клиента. Нужны серверные проверки размера, расширения, MIME и содержимого изображения. Перечень допустимых форматов зависит от продукта. Не принимайте его только потому, что контрол показал кнопку выбора картинки.

\n
<?php\n\nfunction validateImageUpload(array $upload): array\n{\n    if (($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {\n        throw new RuntimeException('Загрузка изображения не завершена');\n    }\n\n    $size = (int) ($upload['size'] ?? 0);\n    if ($size < 1 || $size > 5 * 1024 * 1024) {\n        throw new RuntimeException('Размер изображения не подходит');\n    }\n\n    if (!isset($upload['tmp_name']) || !is_uploaded_file($upload['tmp_name'])) {\n        throw new RuntimeException('Временный файл не подтверждён PHP');\n    }\n\n    return $upload;\n}\n\n$upload = validateImageUpload($_FILES['CATALOG_PREVIEW'] ?? array());\n$fileId = (int) CFile::SaveFile($upload, 'catalog');\nif ($fileId < 1 || !CFile::GetFileArray($fileId)) {\n    throw new RuntimeException('Bitrix не зарегистрировал файл');\n}
\n

Код выше ограничен учебным примером. Он не знает вашу авторизацию, CSRF-защиту, политику форматов, антивирус, каталог хранения и способ очистки временных файлов. В рабочем обработчике эти условия должны быть явными. Если FileInput в вашей версии сначала загружает файл отдельным запросом, не копируйте ветку с $_FILES: сначала определите, какое значение вернул контрол и кто владеет временным ID.

\n

После регистрации ID нужно превратить в файловый массив для API элемента. Для поля PREVIEW_PICTURE недостаточно положить число в произвольное поле. Старый API Bitrix ожидает структуру файлового значения и возвращает ошибку через результат обновления и LAST_ERROR.

\n
<?php\n\n$element = new CIBlockElement();\n$picture = CFile::MakeFileArray($fileId);\n\nif (!$picture) {\n    throw new RuntimeException('Не удалось собрать файловый массив');\n}\n\n$updated = $element->Update($elementId, array(\n    'PREVIEW_PICTURE' => $picture,\n));\n\nif (!$updated) {\n    throw new RuntimeException($element->LAST_ERROR ?: 'Элемент не обновлён');\n}
\n

Сохранение файла и обновление элемента — разные операции. Первая может завершиться успешно, а вторая — нет из-за прав, неверного ID, ошибки поля или обработчика события. Тогда в b_file останется незакреплённая запись. Политику очистки таких файлов определите отдельно. Не удаляйте старую картинку до успешной привязки новой, если откат не гарантирован.

\n

Три состояния редактирования

\n

Обновление существующей картинки должно различать три команды. Новый файл означает замену после успешной проверки. Отсутствие нового файла и отсутствие флага удаления означает «оставить как есть». Явный флаг удаления означает очистить поле. Новый файл вместе с удалением — конфликт, который лучше отклонить, чем разрешить случайным порядком полей.

\n

Это правило нельзя выводить из пустого preview. DOM может очиститься после ошибки JavaScript или перерисовки формы, хотя пользователь не просил удалять изображение. Смысл операции передаёт серверное поле, которое обработчик проверяет явно.

\n
<?php\n\n$hasNewFile = isset($_FILES['CATALOG_PREVIEW'])\n    && ($_FILES['CATALOG_PREVIEW']['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK;\n$deleteRequested = ($_POST['DELETE_PICTURE'] ?? '') === '1';\n\nif ($hasNewFile && $deleteRequested) {\n    throw new RuntimeException('Выберите новый файл или удаление');\n}\n\nif (!$hasNewFile && !$deleteRequested) {\n    // Старое изображение остаётся. Update() для поля не вызываем.\n    return;\n}\n\n$fields = $deleteRequested\n    ? array('PREVIEW_PICTURE' => array('del' => 'Y'))\n    : array('PREVIEW_PICTURE' => CFile::MakeFileArray($fileId));\n\n$element = new CIBlockElement();\nif (!$element->Update((int) $elementId, $fields)) {\n    throw new RuntimeException($element->LAST_ERROR);\n}
\n

Фрагмент показывает отрицательный путь, но не является готовым endpoint. В реальном коде до него должны выполняться проверка сессии, права на конкретный элемент, CSRF, нормализация входа и проверка принадлежности временного файла пользователю или черновику. Для свойства типа «Файл» формат обновления может отличаться от поля элемента. Сверяйте документацию именно для своего метода.

\n

Проверка после записи

\n

Ответ 200 и положительный ID не закрывают задачу. После Update() нужно прочитать элемент новым запросом. Читайте то же поле, которое использует страница. Старый объект, заполненный до POST, может содержать прежнее значение.

\n

Проверка должна сравнить ожидаемый ID с фактическим. Затем откройте карточку через отдельный HTTP-запрос и проверьте видимое изображение. Этот шаг ловит обработчики событий, кеш, неверный инфоблок и запись не в то свойство.

\n

Порядок действий

\n
  1. Назовите элемент, поле изображения и пользователя, который имеет право его менять.
  2. Зафиксируйте текущий ID картинки и правило для случая без нового файла.
  3. Выведите FileInput с согласованным name, одной картинкой и лимитами интерфейса.
  4. Проверьте HTML формы: multipart/form-data, ожидаемое имя поля и отсутствие дубликатов input после Ajax.
  5. Отправьте небольшой учебный файл и проверьте в Network multipart-часть, код ответа и тело ответа.
  6. На сервере проверьте права, код загрузки, размер, временный путь, MIME и содержимое файла.
  7. Зарегистрируйте файл через согласованный API Bitrix и проверьте положительный ID.
  8. Передайте файловое значение в CIBlockElement::Update() и обработайте LAST_ERROR.
  9. Повторно прочитайте элемент и сравните новое поле изображения с ожидаемым ID.
  10. Повторите тест без файла, с явным удалением и с конфликтом «новый файл плюс удаление».
  11. Только после проверок решите, как очищать незакреплённые файлы и временную диагностику.
\n

Ограничения

\n

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

\n

Лимит в пять мегабайт в коде — учебное число. Он не доказывает подходящий размер для вашей витрины. На результат также влияют upload_max_filesize, post_max_size, права каталога, обратный прокси и антивирусная проверка. Несовпадение этих ограничений проявляется до Update().

\n

Сохранение ID файла не заменяет проверку владельца. Если файл создаётся отдельным Ajax-запросом, временный ID нужно связать с пользователем и элементом или черновиком. Нельзя принять любой ID из скрытого поля и назначить его чужой карточке.

\n

Примеры выше не сообщают production-результаты и не обещают совместимость с конкретной установкой. Они показывают проверяемую последовательность. В рабочем проекте добавьте журнал ошибки без содержимого файла и персональных данных, а также тесты для успешной загрузки, отказа и повторного чтения.

\n

Критерий готовности

\n

Интеграция готова, если после выбора учебного изображения форма делает один ожидаемый multipart-запрос, сервер принимает только разрешённый вход, Bitrix возвращает ID файла, Update() завершается успешно, а свежая страница показывает именно этот файл. При отправке без нового файла старая картинка остаётся. При явном удалении поле очищается. При конфликте действий сервер возвращает понятную ошибку и не меняет элемент.

\n

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

\n" }