8 lines
20 KiB
JSON
8 lines
20 KiB
JSON
{
|
||
"index": 331,
|
||
"slug": "editorial-2018-10-field-image-workflow",
|
||
"title": "Bitrix: как не потерять изображение между preview и сохранением формы",
|
||
"excerpt": "Preview показывает выбранный файл, но не доказывает, что Bitrix получил его и записал в PREVIEW_PICTURE. Разделяем нативный multipart-контракт и AJAX-режим FileInput, разбираем ветки замены и удаления и проверяем результат после обновления элемента.",
|
||
"contentHtml": "<p>Редактор показывает новую фотографию, пользователь нажимает «Сохранить», а после перезагрузки карточка снова открывается со старой. Иногда поле изображения становится пустым. В файловом хранилище при этом остаются лишние загрузки без связи с элементом. Оператор повторяет действие, а разработчик видит только удачный preview и ответ HTTP 200. Ошибка стоит времени пользователя, мусора в хранилище и риска опубликовать карточку не с тем изображением.</p>\n<p>Preview подтверждает только состояние интерфейса. Надёжное сохранение проходит три границы: браузер отправляет файл, сервер принимает и проверяет его, Bitrix обновляет поле элемента. Важно выбрать один контракт загрузки: нативная форма передаёт файл в <code>$_FILES</code>, а AJAX-контрол Bitrix сначала создаёт своё значение. Если смешать эти пути, экран может быть правильным, а <code>PREVIEW_PICTURE</code> — прежним.</p>\n<h2>У изображения несколько состояний</h2>\n<p>До отправки браузер владеет выбранным объектом <code>File</code>. Preview владеет картинкой на экране и текстом статуса. После обычной отправки файл появляется в <code>$_FILES</code>; после отдельной AJAX-загрузки контрол возвращает путь или хеш в своём формате. Только серверный обработчик решает, какое значение можно передать в <code>CIBlockElement::Update()</code>. Поле <code>PREVIEW_PICTURE</code> принадлежит элементу инфоблока, поэтому его нужно перечитать после обновления.</p>\n<p>В legacy-шаблоне рядом могут жить поле выбора, скрытый идентификатор, HTML-редактор и Ajax-перерисовка контейнера. У каждого значения должна быть одна роль. Скрытый <code>PHOTO_ID</code> не превращает локальный файл в сохранённый. Идентификатор отдельной загрузки нужно связать с пользователем и элементом; брать «последний файл» из базы нельзя, потому что параллельный запрос может изменить результат.</p>\n<table><thead><tr><th>Сигнал</th><th>Владелец</th><th>Что он доказывает</th></tr></thead><tbody><tr><td><code>CATALOG_PREVIEW</code> в <code>$_FILES</code></td><td>браузер и multipart POST</td><td>файл дошёл до PHP</td></tr><tr><td><code>DELETE_PICTURE=1</code></td><td>явное действие пользователя</td><td>запрошено удаление</td></tr><tr><td><code>.js-photo-state</code> и preview</td><td>DOM и jQuery</td><td>только обратная связь</td></tr><tr><td><code>PREVIEW_PICTURE</code></td><td>элемент инфоблока</td><td>сохранённая привязка файла</td></tr></tbody></table>\n<figure><img src=\"/assets/editorial/2018/legacy-photo-form-contract-2018.svg\" alt=\"Схема контракта формы: preview, multipart POST, ветка Bitrix и повторное чтение\" /><figcaption>Preview — сигнал браузера. Результат появляется только после проверки multipart-запроса, успешного обновления элемента и нового чтения карточки.</figcaption></figure>\n<h2>Нативная форма: самый короткий контракт</h2>\n<p>Для простого legacy-сценария достаточно обычного поля файла. У формы есть <code>enctype=\"multipart/form-data\"</code>, у поля — имя, которое сервер ищет в <code>$_FILES</code>. Этот пример намеренно не использует <code>FileInput</code>: так легче увидеть, какой запрос должен прийти обработчику.</p>\n<pre><code><form method=\"post\" enctype=\"multipart/form-data\" id=\"catalog-photo-form\">\n <input type=\"file\" name=\"CATALOG_PREVIEW\" accept=\"image/*\">\n <label>\n <input type=\"checkbox\" name=\"DELETE_PICTURE\" value=\"1\">\n удалить текущее изображение\n </label>\n <p class=\"js-photo-state\" aria-live=\"polite\"></p>\n <button type=\"submit\">Сохранить</button>\n</form></code></pre>\n<p>Если форма отправляется Ajax-ом, соберите её через <code>FormData</code>. Метод <code>serialize()</code> не передаёт содержимое поля файла. Заголовок <code>Content-Type</code> вручную не задавайте: браузер добавляет boundary, по которому сервер разделяет части запроса. В Network проверяйте имя поля, размер части, код ответа и тело ответа. HTTP 200 означает ответ сервера, но не успешное обновление элемента.</p>\n<pre><code>var form = document.getElementById('catalog-photo-form');\n\nform.addEventListener('submit', function (event) {\n event.preventDefault();\n\n var data = new FormData(form);\n fetch('/admin/catalog/photo.php', {\n method: 'POST',\n body: data\n })\n .then(function (response) {\n if (!response.ok) {\n throw new Error('HTTP ' + response.status);\n }\n return response.json();\n })\n .then(function (result) {\n if (!result.ok) {\n throw new Error(result.error || 'Изображение не сохранено');\n }\n });\n});</code></pre>\n<p>Для статуса выбора можно использовать делегированный обработчик jQuery. Он остаётся рабочим после замены дочернего HTML-контейнера. Namespace позволяет снять только обработчик этого сценария:</p>\n<pre><code>(function ($) {\n $(document)\n .off('change.photoWorkflow', 'input[name=CATALOG_PREVIEW]')\n .on('change.photoWorkflow', 'input[name=CATALOG_PREVIEW]', function () {\n var file = this.files && this.files[0];\n $('.js-photo-state').text(file\n ? 'Выбран файл: ' + file.name + '. Сохранение ещё не выполнено.'\n : 'Новый файл не выбран.');\n });\n}(jQuery));</code></pre>\n<p>Текст намеренно говорит о выборе, а не о сохранении. Имя файла, data URL и размер полезны для экрана, но не могут заменять серверный ID. После ответа сервера статус нужно менять только по явному полю результата, а не по факту отправки формы.</p>\n<h2>Сервер выбирает одну ветку</h2>\n<p>Для редактирования изображения нужны три операции: оставить старое, заменить новым или удалить. Запрет на одновременные замену и удаление — это политика данного обработчика, а не обещание самого Bitrix. Она убирает зависимость от порядка полей в запросе. Пустой <code>input type=\"file\"</code> означает «нового файла нет», но не означает «удалить старый».</p>\n<pre><code>function updatePreviewPicture($elementId, array $post, array $files)\n{\n $upload = $files['CATALOG_PREVIEW'] ?? array();\n $error = $upload['error'] ?? UPLOAD_ERR_NO_FILE;\n $delete = ($post['DELETE_PICTURE'] ?? '') === '1';\n\n if ($error !== UPLOAD_ERR_NO_FILE && $error !== UPLOAD_ERR_OK) {\n throw new RuntimeException('Загрузка файла завершилась ошибкой: ' . $error);\n }\n\n $hasNewFile = $error === UPLOAD_ERR_OK;\n if ($hasNewFile && (empty($upload['tmp_name']) || !is_uploaded_file($upload['tmp_name']))) {\n throw new RuntimeException('Файл не найден во временном хранилище');\n }\n\n if ($hasNewFile && $delete) {\n throw new RuntimeException('Выберите замену или удаление');\n }\n if (!$hasNewFile && !$delete) {\n return false; // оставить текущее значение\n }\n\n $fields = array(\n 'PREVIEW_PICTURE' => $hasNewFile\n ? $upload\n : array('del' => 'Y'),\n );\n $element = new CIBlockElement();\n\n if (!$element->Update((int) $elementId, $fields)) {\n throw new RuntimeException($element->LAST_ERROR);\n }\n\n return true;\n}</code></pre>\n<p>Код показывает минимальную развилку, а не готовую политику приёма файлов. Перед <code>Update()</code> сервер должен ограничить размер, проверить фактический тип изображения, права пользователя и принадлежность элемента. Значения <code>UPLOAD_ERR_*</code> нельзя превращать в «файла нет»: ошибка лимита или сбой временного хранилища должны стать ошибкой формы.</p>\n<p>Документация Bitrix описывает для <code>CIBlockElement::Update()</code> массив полей и сообщает, что при <code>false</code> текст причины находится в <code>LAST_ERROR</code>. Для уже существующего серверного файла нужен подтверждённый путь и файловый массив, например через <code>CFile::MakeFileArray()</code>. Для файлового свойства, а не поля <code>PREVIEW_PICTURE</code>, формат <code>PROPERTY_VALUES</code> проверяйте отдельно.</p>\n<h2>FileInput: другой способ передачи</h2>\n<p><code>\\Bitrix\\Main\\UI\\FileInput</code> удобен, когда нужен готовый контрол выбора и загрузки. Но параметр <code>upload => true</code> включает отдельную AJAX-загрузку. В документации у него есть <code>uploadType</code> со значениями <code>path</code> и <code>hash</code>; результат такого контрола не следует автоматически искать в <code>$_FILES['CATALOG_PREVIEW']</code>.</p>\n<pre><code><?php\nuse Bitrix\\Main\\UI\\FileInput;\n\necho FileInput::createInstance(array(\n 'id' => 'catalog_preview',\n 'name' => 'CATALOG_PREVIEW',\n 'upload' => true,\n 'uploadType' => 'hash',\n 'allowUpload' => FileInput::UPLOAD_IMAGES,\n 'maxCount' => 1,\n 'delete' => true,\n))->show($files);</code></pre>\n<p>Для этого варианта сначала определите реальный ответ контрол-эндпойнта и способ, которым проект вызывает <code>prepareFile()</code>. Затем сервер должен проверить подпись или идентификатор, владельца временного файла, элемент и срок жизни загрузки, после чего передать подготовленный файловый массив в обновление. Если нужна одна обычная отправка формы, не включайте AJAX-загрузку FileInput: используйте нативное поле и обрабатывайте <code>$_FILES</code> по первому контракту.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><thead><tr><th>Симптом</th><th>Вероятная причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>Preview новый, карточка старая</td><td>файл не попал в POST или не вызван <code>Update()</code></td><td>Network: multipart-часть, ответ обработчика и свежая выборка</td><td>исправить имя поля или ветку сохранения</td></tr><tr><td>Ответ 200, поле пустое</td><td>ошибка скрыта в теле ответа или неверен формат поля</td><td>проверить JSON, <code>LAST_ERROR</code> и ID элемента</td><td>вернуть ошибку формы и перечитать элемент</td></tr><tr><td>Ajax работает со второго раза</td><td><code>serialize()</code> не передаёт файл или обработчик дублируется</td><td>проверить Request Payload и число срабатываний <code>change</code></td><td>использовать <code>FormData</code> и namespace</td></tr><tr><td>Удаление срабатывает само</td><td>пустой preview ошибочно принят за команду удаления</td><td>сравнить явный флаг с данными формы</td><td>передавать отдельный флаг удаления</td></tr><tr><td>Появляются сиротские файлы</td><td>отдельная загрузка завершилась без обновления элемента</td><td>сопоставить временный ID с пользователем и элементом</td><td>ввести владельца и очистку незавершённых загрузок</td></tr></tbody></table>\n<h2>Проверка по порядку отказа</h2>\n<ol><li>Открываю карточку с известным текущим ID изображения и фиксирую его до изменения.</li><li>Отправляю форму без нового файла и без удаления. После свежего чтения ID должен остаться прежним.</li><li>Выбираю небольшой учебный JPEG и проверяю в Network имя поля, multipart-часть и размер файла.</li><li>Проверяю ответ обработчика, результат <code>Update()</code> и <code>LAST_ERROR</code>, если метод вернул <code>false</code>.</li><li>Открываю карточку новым HTTP-запросом и сравниваю URL изображения с ожидаемым файлом.</li><li>Отправляю явное удаление без нового файла и проверяю, что сработала только ветка удаления.</li><li>Отправляю новый файл вместе с удалением. Ожидаю понятную ошибку формы.</li><li>Перерисовываю контейнер Ajax-ом и меняю файл ещё раз. Событие должно обработаться один раз.</li><li>Повторяю сценарий с превышенным размером и повреждённым изображением. Обработчик должен вернуть ошибку до изменения элемента.</li></ol>\n<h2>Ограничения</h2>\n<p>Пример не задаёт универсальные размеры, MIME-типы, права и антивирусную проверку. Эти правила зависят от версии Bitrix, настроек PHP, роли пользователя и требований каталога. Проверка расширения в браузере не защищает сервер. Поле <code>PREVIEW_PICTURE</code> и файловое свойство используют разные контракты, поэтому их нельзя менять одним предположением.</p>\n<p>Если FileInput загружает файл отдельным запросом, путь через <code>$_FILES</code> неприменим без адаптации. Сначала определите ответ контролла и место хранения временного файла. При отмене формы не оставляйте такой идентификатор без владельца. Для небольшой синхронной формы не нужны очереди, но нужна понятная очистка незавершённых загрузок.</p>\n<h2>Критерий готовности</h2>\n<p>Сценарий готов, если форма делает один понятный запрос, сервер принимает только допустимый файл, <code>Update()</code> возвращает успех, а свежая страница показывает новое изображение. Отправка без нового файла оставляет старое значение. Явное удаление меняет поле только по флагу пользователя. Конфликт «новый файл плюс удаление» даёт ошибку. После Ajax-перерисовки нет двойного обработчика. Эти условия проверяются на тестовой записи и не являются заявлением о результате рабочей среды.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://dev.1c-bitrix.ru/api_help/iblock/classes/ciblockelement/update.php?print=Y\" target=\"_blank\" rel=\"noopener\">Официальная документация Bitrix: CIBlockElement::Update</a></li><li><a href=\"https://dev.1c-bitrix.ru/api_d7/bitrix/main/ui/fileinput/index.php\" target=\"_blank\" rel=\"noopener\">Официальная документация Bitrix: FileInput</a></li><li><a href=\"https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects\" target=\"_blank\" rel=\"noopener\">MDN: FormData, multipart и граница Content-Type</a></li><li><a href=\"https://api.jquery.com/on/\" target=\"_blank\" rel=\"noopener\">Официальная документация jQuery: делегирование и namespace</a></li></ul>"
|
||
}
|