{ "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 или другое согласованное свойство.
У одной картинки есть несколько состояний. Браузер хранит выбранный объект File. HTML-форма передаёт его как multipart-часть. PHP получает массив в $_FILES. Bitrix создаёт запись в b_file и возвращает числовой ID. Элемент инфоблока хранит ссылку на этот ID в поле изображения. Ни один этап не заменяет следующий.
Preview относится к DOM. Он может измениться до отправки формы, после Ajax-перерисовки или даже при ошибочном ответе сервера. Поэтому текст «файл выбран» и URL картинки в интерфейсе нельзя использовать как доказательство сохранения. Сервер должен получить конкретное поле, проверить его и записать связь.
\n| Состояние | Причина сбоя | Проверка | Действие |
|---|---|---|---|
| Preview обновился | Изменился только DOM | Открыть Network и найти multipart-поле | Не считать файл сохранённым |
В $_FILES нет поля | Неверное name или форма без enctype | Проверить имя input, метод и Request Payload | Исправить контракт формы |
| Upload завершился ошибкой | Лимит PHP, размер или права каталога | Проверить error, size и серверный лог | Вернуть ошибку формы и остановить запись |
| Есть ID файла, но карточка прежняя | Не вызван Update() или передано не то поле | Повторно прочитать элемент и поле изображения | Сохранить файловый массив в сущность |
| После удаления картинка вернулась | Удаление выведено из пустого preview | Проверить явный флаг удаления в POST | Развести ветки «оставить», «заменить» и «удалить» |
Контрол создают на сервере. Имя поля выбирают вместе с обработчиком. Если шаблон отправляет CATALOG_PREVIEW, PHP не должен ждать picture. Такая ошибка выглядит как «Bitrix не загрузил картинку», хотя файл просто не попал в ожидаемую ветку.
Ниже учебный пример для формы с одной картинкой. Он показывает состав параметров, а не готовую конфигурацию конкретного проекта. Константы, доступность источников и набор опций нужно сверить с версией модуля main и установленными правами.
<?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?>\nname связывает разметку с серверным кодом. upload включает загрузку. allowUpload сужает сценарий до изображений, если это поддерживает установленная версия API. maxCount и maxSize уменьшают число ошибочных действий в интерфейсе. Они не заменяют серверную проверку: запрос можно отправить вручную.
delete только даёт пользователю возможность выбрать удаление через контрол. Правило хранения остаётся в обработчике. Для редактирования существующего элемента нужно заранее решить, что означает отсутствие нового файла: обычно сохранить старую картинку. Явное удаление передают отдельным признаком.
Обычная HTML-форма с файлом использует method=\"post\" и enctype=\"multipart/form-data\". Для Ajax нужен объект FormData. Вызов jQuery serialize() собирает текстовые поля, но не переносит бинарное содержимое файла. Если заменить multipart-запрос сериализацией, preview останется, а сервер получит только остальную форму.
В legacy-шаблоне частая ошибка появляется после .html(). Старый input удаляют, новый вставляют, а обработчик остаётся привязан к уничтоженному узлу. Повторная инициализация добавляет второй обработчик. Делегируйте событие стабильному контейнеру и снимайте только свой namespace.
(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Сначала проверяют результат загрузки и входные ограничения. Поле type из запроса — это заявление клиента. Нужны серверные проверки размера, расширения, MIME и содержимого изображения. Перечень допустимых форматов зависит от продукта. Не принимайте его только потому, что контрол показал кнопку выбора картинки.
<?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.
После регистрации ID нужно превратить в файловый массив для API элемента. Для поля PREVIEW_PICTURE недостаточно положить число в произвольное поле. Старый API Bitrix ожидает структуру файлового значения и возвращает ошибку через результат обновления и LAST_ERROR.
<?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Это правило нельзя выводить из пустого 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Ответ 200 и положительный ID не закрывают задачу. После Update() нужно прочитать элемент новым запросом. Читайте то же поле, которое использует страница. Старый объект, заполненный до POST, может содержать прежнее значение.
Проверка должна сравнить ожидаемый ID с фактическим. Затем откройте карточку через отдельный HTTP-запрос и проверьте видимое изображение. Этот шаг ловит обработчики событий, кеш, неверный инфоблок и запись не в то свойство.
\nFileInput с согласованным name, одной картинкой и лимитами интерфейса.multipart/form-data, ожидаемое имя поля и отсутствие дубликатов input после Ajax.CIBlockElement::Update() и обработайте LAST_ERROR.Параметры FileInput, константы разрешённых типов и формат возвращаемого значения зависят от версии Bitrix. Старое ядро может требовать другой способ подготовки файлового массива. Проверяйте установленный API, а не переносите пример по названию метода.
Лимит в пять мегабайт в коде — учебное число. Он не доказывает подходящий размер для вашей витрины. На результат также влияют upload_max_filesize, post_max_size, права каталога, обратный прокси и антивирусная проверка. Несовпадение этих ограничений проявляется до Update().
Сохранение ID файла не заменяет проверку владельца. Если файл создаётся отдельным Ajax-запросом, временный ID нужно связать с пользователем и элементом или черновиком. Нельзя принять любой ID из скрытого поля и назначить его чужой карточке.
\nПримеры выше не сообщают production-результаты и не обещают совместимость с конкретной установкой. Они показывают проверяемую последовательность. В рабочем проекте добавьте журнал ошибки без содержимого файла и персональных данных, а также тесты для успешной загрузки, отказа и повторного чтения.
\nИнтеграция готова, если после выбора учебного изображения форма делает один ожидаемый multipart-запрос, сервер принимает только разрешённый вход, Bitrix возвращает ID файла, Update() завершается успешно, а свежая страница показывает именно этот файл. При отправке без нового файла старая картинка остаётся. При явном удалении поле очищается. При конфликте действий сервер возвращает понятную ошибку и не меняет элемент.
$_FILES и серверные ограничения.