8 lines
23 KiB
JSON
8 lines
23 KiB
JSON
{
|
||
"index": 332,
|
||
"slug": "bitrix-api-add-foto-editor",
|
||
"title": "Bitrix API: как встроить редактор изображений и сохранить файл в элементе",
|
||
"excerpt": "FileInput показывает выбранную картинку, но не сохраняет её сам. Разбираем контракт формы, проверку загрузки и привязку файла к PREVIEW_PICTURE в Bitrix.",
|
||
"contentHtml": "<p>При редактировании карточки товара пользователь с ролью менеджера выбирает новую фотографию в форме Bitrix и сразу видит preview. Сначала он заметил: URL картинки в DOM сменился. После нажатия «Сохранить» страница открывается со старым изображением, а поле иногда остаётся пустым.</p>\n<p>Цена ошибки выше, чем у сломанной кнопки. Менеджер считает карточку обновлённой, а каталог продолжает показывать старую обложку. Повторная загрузка создаёт мусорные файлы. При массовом редактировании ошибка превращается в неверные фотографии, ручную сверку и восстановление данных.</p>\n<p><strong>Тезис.</strong> <code>Bitrix\\Main\\UI\\FileInput</code> формирует интерфейс выбора и загрузки. Он не доказывает, что файл записан в нужное поле сущности. Готовность наступает только после трёх подтверждений: запрос принял ожидаемое значение, Bitrix зарегистрировал разрешённый файл и повторное чтение элемента вернуло этот ID в <code>PREVIEW_PICTURE</code> или другое согласованное свойство.</p>\n<h2>Поворот проверки: preview не равен сохранению</h2>\n<p>Первое предположение менеджера естественно: раз preview изменился, браузер получил файл. Проверка Network меняет картину. Нужно увидеть фактический запрос, его поле, код ответа и значение, которое обработчик передал в Bitrix. Если обновился только DOM, сервер ещё ничего не знает о выборе.</p>\n<p>У изображения несколько независимых состояний: браузер хранит объект <code>File</code>, HTML-форма передаёт multipart-часть, PHP собирает данные в <code>$_FILES</code>, Bitrix регистрирует файл в <code>b_file</code>, а элемент хранит ссылку на ID. При асинхронной загрузке вместо сырого файла может прийти подготовленное значение или ID. Контракт подтверждают по конкретному запросу, а не по названию input.</p>\n<table><caption>Состояния изображения и границы проверки</caption><thead><tr><th scope=\"col\">Состояние</th><th scope=\"col\">Причина сбоя</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Preview обновился</td><td>Изменился только DOM</td><td>Открыть Network и найти значение поля</td><td>Не считать файл сохранённым</td></tr><tr><td>В <code>$_FILES</code> нет поля</td><td>Неверное <code>name</code>, метод или <code>enctype</code></td><td>Сверить Request Payload с обработчиком</td><td>Исправить контракт или ветку чтения</td></tr><tr><td>Есть ID файла, но карточка прежняя</td><td>Не вызван <code>Update()</code> или передано не то поле</td><td>Повторно прочитать элемент</td><td>Передать файловое значение в сущность</td></tr><tr><td>После удаления картинка вернулась</td><td>Удаление выведено из пустого preview</td><td>Проверить явный флаг удаления</td><td>Развести «оставить», «заменить» и «удалить»</td></tr></tbody></table>\n<figure><img src=\"/assets/illustrations/bitrix-photo-editor-ui.svg\" alt=\"Интерфейс редактора изображений Bitrix с выбором файла и предварительным просмотром\" /><figcaption>Preview помогает выбрать файл, но результатом считается только связь зарегистрированного ID с элементом Bitrix.</figcaption></figure>\n<h2>Контракт FileInput</h2>\n<p>Контрол создают через <code>createInstance()</code>, а разметку и JavaScript выводят вызовом <code>show()</code>. Имя поля выбирают вместе с серверным обработчиком. Явный <code>id</code> помогает отличить экземпляр контрола от другого поля на странице.</p>\n<pre><code><?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?></code></pre>\n<p>В примере <code>UPLOAD_IMAGES</code> разрешает изображения, <code>maxCount</code> ограничивает количество, а <code>maxSize</code> задаёт ограничение контрола. Документация допускает для <code>show()</code> текущие значения как массив, ID или строку и возвращает HTML с JavaScript. Передача ID существующего файла допустима, но не означает, что новый файл уже привязан к элементу.</p>\n<p>У <code>FileInput</code> есть собственный JavaScript-контрол и дополнительные источники выбора. В одной конфигурации обработчик получает обычный multipart-файл, в другой — значение, подготовленное асинхронной загрузкой. Сначала сохраните один учебный файл, откройте Network и зафиксируйте точное поле. Ветка с <code>$_FILES['CATALOG_PREVIEW']</code> ниже относится только к подтверждённому multipart-контракту.</p>\n<h2>Форма должна отправить файл</h2>\n<p>Обычная HTML-форма использует <code>method="post"</code> и <code>enctype="multipart/form-data"</code>. Для Ajax нужен объект <code>FormData</code>. Вызов jQuery <code>serialize()</code> собирает текстовые поля, но не переносит бинарное содержимое файла. Если заменить multipart-запрос сериализацией, preview останется, а сервер получит только остальную форму.</p>\n<p>После Ajax-перерисовки старый input может быть уничтожен вместе с обработчиком. Делегируйте событие стабильному контейнеру и снимайте только собственный namespace. Иначе повторная инициализация даст два обработчика или удалит подписки соседнего компонента.</p>\n<pre><code>(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));</code></pre>\n<p>Этот фрагмент меняет только статус в DOM и не создаёт ID файла. После повторной отрисовки изменение должно дать один статус, а сохранение — один ожидаемый запрос. Общий <code>off('change')</code> здесь опасен: он может снять обработчики других компонентов.</p>\n<h2>Проверяем вход до регистрации</h2>\n<p>Поле <code>type</code> из запроса сообщает мнение клиента, а не доказанный тип файла. Сервер сначала проверяет код загрузки, размер и временный путь, затем определяет MIME по содержимому. В учебном примере разрешены три расширения; этот список должен совпадать с политикой проекта.</p>\n<pre><code><?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());</code></pre>\n<p>Проверка MIME по содержимому и <code>getimagesize()</code> не заменяют права, CSRF-защиту, антивирус и безопасное хранение. Они закрывают только границу входного файла. Не используйте имя файла или присланный MIME как единственное доказательство. Ошибку нужно вернуть до создания записи и до обновления элемента.</p>\n<h2>Сначала файл, затем связь</h2>\n<p>Обработчик не должен искать «последний созданный файл» в базе: одновременно работают несколько пользователей. Связь строят из данных конкретного запроса и конкретного элемента. <code>CFile::SaveFile()</code> регистрирует файл и возвращает числовой ID, но этот ID ещё не меняет карточку.</p>\n<pre><code><?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}</code></pre>\n<p>Для поля изображения старый API ожидает файловое значение, а не произвольное число. <code>MakeFileArray()</code> готовит массив для методов работы с файлами; <code>Update()</code> возвращает <code>true</code> при успехе и <code>false</code> при ошибке, текст которой доступен в <code>LAST_ERROR</code>. Если регистрация прошла, а обновление нет, в <code>b_file</code> может остаться незакреплённая запись. Старую картинку не удаляйте до успешной привязки новой, если откат не гарантирован.</p>\n<h2>Три команды редактирования</h2>\n<p>Отсутствие нового файла не должно случайно означать удаление. Для существующей карточки нужны три команды: оставить старое изображение, заменить его после проверки или удалить по явному признаку. Новый файл вместе с удалением безопаснее отклонить.</p>\n<pre><code><?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}</code></pre>\n<p>Этот фрагмент предполагает, что авторизация, права на конкретный элемент, CSRF и нормализация входа уже выполнены. Для свойства инфоблока с типом «Файл» формат обновления может отличаться от поля <code>PREVIEW_PICTURE</code>. Сверяйте именно свой тип поля и реальный контракт FileInput.</p>\n<h2>Проверяем запись новым чтением</h2>\n<p>Ответ <code>200</code>, положительный ID и обновлённый preview не закрывают задачу. После <code>Update()</code> нужно получить элемент новым запросом, прочитать то же поле и сравнить его с ожидаемым ID. Старый объект, заполненный до POST, способен показать прежнее значение.</p>\n<pre><code><?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}</code></pre>\n<p>Затем откройте карточку отдельным HTTP-запросом и проверьте видимое изображение. Такой шаг ловит обработчик событий, кеш, неверный инфоблок и запись не в то поле. Только после него можно считать исходный сценарий менеджера закрытым.</p>\n<h2>Порядок проверки</h2>\n<ol><li>Назовите элемент, поле изображения и пользователя, который имеет право его менять.</li><li>Зафиксируйте текущий ID картинки и правило для отправки без нового файла.</li><li>Выведите <code>FileInput</code> с согласованными <code>name</code>, <code>id</code> и лимитами.</li><li>Проверьте форму и Network: метод, <code>enctype</code>, имя поля и фактическое значение запроса.</li><li>Отправьте небольшой учебный файл и проверьте код ответа и тело ответа.</li><li>На сервере проверьте права, код загрузки, размер, путь, MIME и содержимое.</li><li>Зарегистрируйте файл через согласованный API Bitrix и проверьте положительный ID.</li><li>Передайте файловое значение в <code>CIBlockElement::Update()</code> и обработайте <code>LAST_ERROR</code>.</li><li>Новым чтением сравните поле изображения с ожидаемым ID, затем проверьте карточку по HTTP.</li><li>Повторите тест без файла, с явным удалением и с конфликтом «новый файл плюс удаление».</li><li>Только после проверок определите политику очистки незакреплённых файлов и журналирования ошибок.</li></ol>\n<h2>Ограничения</h2>\n<p>Параметры <code>FileInput</code>, доступные константы и формат возвращаемого значения зависят от версии Bitrix и подключённых модулей. Старое ядро может требовать другой способ подготовки файла. Проверяйте установленный API, а не переносите пример только по названию метода.</p>\n<p>Пять мегабайт в примере — учебное число. Реальный предел задают также <code>upload_max_filesize</code>, <code>post_max_size</code>, права каталога, обратный прокси и антивирус. Для асинхронной загрузки временный ID нужно связать с пользователем и элементом или черновиком; нельзя принять любой ID из скрытого поля.</p>\n<p>Примеры не являются готовым endpoint: в них опущены авторизация, CSRF, транзакционная политика, обработка дублей и очистка сиротских файлов. Они показывают воспроизводимую последовательность проверки. Добавьте тесты для успешной загрузки, отказа, оставления старого файла, удаления и повторного чтения.</p>\n<h2>Критерий готовности</h2>\n<p>Интеграция готова, если после выбора учебного изображения запрос содержит ожидаемое значение, сервер принимает только разрешённый вход, Bitrix возвращает ID, <code>Update()</code> завершается успешно, новое чтение возвращает тот же ID, а свежая страница показывает файл. При отправке без нового файла старая картинка остаётся. При явном удалении поле очищается. При конфликте действий сервер возвращает понятную ошибку и не меняет элемент. В этот момент менеджер видит не просто новый preview, а подтверждённый результат записи.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://dev.1c-bitrix.ru/api_d7/bitrix/main/ui/fileinput/index.php\" target=\"_blank\" rel=\"noopener noreferrer\">1С-Битрикс: FileInput</a> — класс, константы и параметры контрола.</li><li><a href=\"https://dev.1c-bitrix.ru/api_d7/bitrix/main/ui/fileinput/show.php\" target=\"_blank\" rel=\"noopener noreferrer\">1С-Битрикс: FileInput::show</a> — текущие значения и вывод HTML/JavaScript.</li><li><a href=\"https://dev.1c-bitrix.ru/api_help/main/reference/cfile/savefile.php?print=Y\" target=\"_blank\" rel=\"noopener noreferrer\">1С-Битрикс: CFile::SaveFile</a> — регистрация файла и возвращаемый ID.</li><li><a href=\"https://dev.1c-bitrix.ru/api_help/main/reference/cfile/makefilearray.php\" target=\"_blank\" rel=\"noopener noreferrer\">1С-Битрикс: CFile::MakeFileArray</a> — подготовка файлового массива.</li><li><a href=\"https://dev.1c-bitrix.ru/api_help/iblock/classes/ciblockelement/update.php\" target=\"_blank\" rel=\"noopener noreferrer\">1С-Битрикс: CIBlockElement::Update</a> — обновление элемента и файловых свойств.</li><li><a href=\"https://www.php.net/manual/en/features.file-upload.php\" target=\"_blank\" rel=\"noopener noreferrer\">PHP Manual: Handling file uploads</a> — multipart-запрос, <code>$_FILES</code>, коды ошибок и проверка входного файла.</li></ul>"
|
||
}
|