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

При редактировании карточки товара пользователь с ролью менеджера выбирает новую фотографию в форме Bitrix и сразу видит preview. Сначала он заметил: URL картинки в DOM сменился. После нажатия «Сохранить» страница открывается со старым изображением, а поле иногда остаётся пустым.

\n

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

\n

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

\n

Поворот проверки: preview не равен сохранению

\n

Первое предположение менеджера естественно: раз preview изменился, браузер получил файл. Проверка Network меняет картину. Нужно увидеть фактический запрос, его поле, код ответа и значение, которое обработчик передал в Bitrix. Если обновился только DOM, сервер ещё ничего не знает о выборе.

\n

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

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

Контракт FileInput

\n

Контрол создают через createInstance(), а разметку и JavaScript выводят вызовом show(). Имя поля выбирают вместе с серверным обработчиком. Явный id помогает отличить экземпляр контрола от другого поля на странице.

\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

В примере UPLOAD_IMAGES разрешает изображения, maxCount ограничивает количество, а maxSize задаёт ограничение контрола. Документация допускает для show() текущие значения как массив, ID или строку и возвращает HTML с JavaScript. Передача ID существующего файла допустима, но не означает, что новый файл уже привязан к элементу.

\n

У FileInput есть собственный JavaScript-контрол и дополнительные источники выбора. В одной конфигурации обработчик получает обычный multipart-файл, в другой — значение, подготовленное асинхронной загрузкой. Сначала сохраните один учебный файл, откройте Network и зафиксируйте точное поле. Ветка с $_FILES['CATALOG_PREVIEW'] ниже относится только к подтверждённому multipart-контракту.

\n

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

\n

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

\n

После Ajax-перерисовки старый 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 файла. После повторной отрисовки изменение должно дать один статус, а сохранение — один ожидаемый запрос. Общий off('change') здесь опасен: он может снять обработчики других компонентов.

\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    $tmpName = $upload['tmp_name'] ?? '';\n    if ($tmpName === '' || !is_uploaded_file($tmpName)) {\n        throw new RuntimeException('Временный файл не подтверждён PHP');\n    }\n\n    $allowed = array(\n        'jpg' => 'image/jpeg',\n        'jpeg' => 'image/jpeg',\n        'png' => 'image/png',\n    );\n    $extension = strtolower(pathinfo($upload['name'] ?? '', PATHINFO_EXTENSION));\n    $mime = (new finfo(FILEINFO_MIME_TYPE))->file($tmpName);\n    if (!isset($allowed[$extension]) || $allowed[$extension] !== $mime\n        || @getimagesize($tmpName) === false) {\n        throw new RuntimeException('Файл не является разрешённым изображением');\n    }\n\n    $upload['type'] = $mime;\n    return $upload;\n}\n\n$upload = validateImageUpload($_FILES['CATALOG_PREVIEW'] ?? array());
\n

Проверка MIME по содержимому и getimagesize() не заменяют права, CSRF-защиту, антивирус и безопасное хранение. Они закрывают только границу входного файла. Не используйте имя файла или присланный MIME как единственное доказательство. Ошибку нужно вернуть до создания записи и до обновления элемента.

\n

Сначала файл, затем связь

\n

Обработчик не должен искать «последний созданный файл» в базе: одновременно работают несколько пользователей. Связь строят из данных конкретного запроса и конкретного элемента. CFile::SaveFile() регистрирует файл и возвращает числовой ID, но этот ID ещё не меняет карточку.

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

Для поля изображения старый API ожидает файловое значение, а не произвольное число. MakeFileArray() готовит массив для методов работы с файлами; Update() возвращает true при успехе и false при ошибке, текст которой доступен в LAST_ERROR. Если регистрация прошла, а обновление нет, в b_file может остаться незакреплённая запись. Старую картинку не удаляйте до успешной привязки новой, если откат не гарантирован.

\n

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

\n

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

\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

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

\n

Проверяем запись новым чтением

\n

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

\n
<?php\n\n$result = CIBlockElement::GetList(\n    array(),\n    array('ID' => (int) $elementId),\n    false,\n    false,\n    array('ID', 'PREVIEW_PICTURE')\n);\n$row = $result->GetNext();\n\nif (!$row || (int) $row['PREVIEW_PICTURE'] !== $fileId) {\n    throw new RuntimeException('Повторное чтение вернуло другой файл');\n}
\n

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

\n

Порядок проверки

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

Ограничения

\n

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

\n

Пять мегабайт в примере — учебное число. Реальный предел задают также upload_max_filesize, post_max_size, права каталога, обратный прокси и антивирус. Для асинхронной загрузки временный ID нужно связать с пользователем и элементом или черновиком; нельзя принять любой ID из скрытого поля.

\n

Примеры не являются готовым endpoint: в них опущены авторизация, CSRF, транзакционная политика, обработка дублей и очистка сиротских файлов. Они показывают воспроизводимую последовательность проверки. Добавьте тесты для успешной загрузки, отказа, оставления старого файла, удаления и повторного чтения.

\n

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

\n

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

\n

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

\n" }