Files
progcode/editorial/agent-rewrites/331.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
17 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 331,
"slug": "editorial-2018-10-field-image-workflow",
"title": "Bitrix: как не потерять изображение между preview и сохранением формы",
"excerpt": "Preview показывает выбранный файл, но не доказывает, что Bitrix получил его и записал в PREVIEW_PICTURE. Разбираем контракт legacy-формы, multipart-запрос, серверные ветки замены и удаления и проверки после обновления элемента.",
"contentHtml": "<p>Редактор показывает новую фотографию, пользователь нажимает «Сохранить», а после перезагрузки карточка снова открывается со старой. Иногда поле изображения становится пустым. В файловом хранилище при этом остаются лишние загрузки без связи с элементом. Оператор повторяет действие, а разработчик видит только успешный preview и ответ HTTP 200. Ошибка стоит времени пользователя, мусора в хранилище и риска опубликовать карточку не с тем изображением.</p>\n<p>Тезис простой: preview — состояние DOM, а не подтверждение данных. Для сохранения нужны три отдельные границы: браузер должен отправить файл как multipart-часть, сервер должен принять и проверить его, а Bitrix должен успешно обновить поле элемента. Если одна граница скрыта за jQuery или FileInput, интерфейс может выглядеть исправным при потерянном результате.</p>\n<h2>Механизм: у одного изображения несколько состояний</h2>\n<p>До отправки браузер владеет выбранным объектом <code>File</code>. Preview владеет только картинкой на экране и текстом статуса. После загрузки сервер регистрирует файл и получает ID. Только затем обработчик может передать файл или файловый массив в <code>CIBlockElement::Update()</code>. Поле <code>PREVIEW_PICTURE</code> принадлежит элементу инфоблока, поэтому его значение нужно проверить отдельным чтением после обновления.</p>\n<p>В legacy-шаблоне часто живут обычный <code>input type=file</code>, скрытый ID, HTML редактора и Ajax-перерисовка одного контейнера. У каждого поля должна быть одна роль. Скрытый <code>PHOTO_ID</code> не превращает выбранный в браузере файл в сохранённый. Если контрол загружает файл отдельным запросом, его временный ID нужно связать с конкретным пользователем и элементом. Нельзя брать «последний файл» из базы: параллельный запрос может изменить этот результат.</p>\n<table><thead><tr><th>Поле или сигнал</th><th>Кто владеет</th><th>Что означает</th></tr></thead><tbody><tr><td><code>CATALOG_PREVIEW</code></td><td>браузер и multipart POST</td><td>кандидат на новую картинку</td></tr><tr><td><code>DELETE_PICTURE=1</code></td><td>явное действие пользователя</td><td>запросить удаление текущей картинки</td></tr><tr><td><code>.js-photo-state</code></td><td>DOM и jQuery</td><td>только текст статуса, не ID файла</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=\"Контракт legacy-формы с изображением\" /><figcaption>Контракт формы: файл приходит в multipart POST, preview остаётся сигналом DOM, а результатом становится значение PREVIEW_PICTURE после успешного обновления.</figcaption></figure>\n<h2>Конкретный пример формы и обработчика</h2>\n<p>Ниже учебный пример для элемента инфоблока с одним изображением. Он показывает границы данных, но не заменяет политику доступа и проверки конкретного проекта. Форма должна явно указать <code>multipart/form-data</code>, а имя поля должно совпасть с ключом в <code>$_FILES</code>.</p>\n<pre><code>&lt;?php\nuse Bitrix\\Main\\UI\\FileInput;\n\n$currentId = (int) $arResult['PREVIEW_PICTURE'];\n?&gt;\n&lt;form method=\"post\" enctype=\"multipart/form-data\" id=\"catalog-photo-form\"&gt;\n &lt;?php\n echo FileInput::createInstance(array(\n 'id' =&gt; 'catalog_preview',\n 'name' =&gt; 'CATALOG_PREVIEW',\n 'upload' =&gt; true,\n 'allowUpload' =&gt; FileInput::UPLOAD_IMAGES,\n 'maxCount' =&gt; 1,\n 'delete' =&gt; true,\n ))-&gt;show($currentId);\n ?&gt;\n &lt;label&gt;&lt;input type=\"checkbox\" name=\"DELETE_PICTURE\" value=\"1\"&gt; удалить&lt;/label&gt;\n &lt;p class=\"js-photo-state\" aria-live=\"polite\"&gt;&lt;/p&gt;\n &lt;button type=\"submit\"&gt;Сохранить&lt;/button&gt;\n&lt;/form&gt;</code></pre>\n<p>Если форма отправляется Ajax-ом, собирайте её через <code>FormData</code>. Не вызывайте <code>serialize()</code> для передачи файла. Не задавайте вручную заголовок <code>Content-Type: multipart/form-data</code>: браузер должен добавить boundary. После отправки смотрите в Network имя file-поля, размер части, код ответа и тело ответа. Статус 200 означает только, что сервер ответил, а не то, что элемент обновился.</p>\n<pre><code>var form = document.getElementById('catalog-photo-form');\n\nform.addEventListener('submit', function (event) {\n event.preventDefault();\n var data = new FormData(form);\n\n fetch('/admin/catalog/photo.php', {\n method: 'POST',\n body: data\n });\n});</code></pre>\n<p>jQuery нужен для поведения интерфейса, но не для хранения результата. При Ajax-перерисовке обработчик поля может исчезнуть. Делегирование от стабильного контейнера сохраняет событие, а namespace позволяет снять только свой обработчик:</p>\n<pre><code>(function ($) {\n var root = document;\n\n $(root).off('change.photoWorkflow', 'input[name=CATALOG_PREVIEW]')\n .on('change.photoWorkflow', 'input[name=CATALOG_PREVIEW]', function () {\n var file = this.files &amp;&amp; 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>Для редактирования изображения нужны три нормальные операции: оставить старое, заменить новым или удалить. Новый файл и удаление в одном запросе — конфликт, который лучше отклонить. Пустой <code>input type=file</code> не означает удаление: пользователь мог не менять картинку, а DOM мог перерисоваться.</p>\n<pre><code>function updatePreviewPicture($elementId, array $post, array $files)\n{\n $upload = $files['CATALOG_PREVIEW'] ?? array();\n $hasNewFile = ($upload['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK;\n $delete = ($post['DELETE_PICTURE'] ?? '') === '1';\n\n if ($hasNewFile &amp;&amp; $delete) {\n throw new RuntimeException('Выберите замену или удаление');\n }\n if (!$hasNewFile &amp;&amp; !$delete) {\n return; // оставить текущее значение\n }\n\n $fields = array(\n 'PREVIEW_PICTURE' =&gt; $hasNewFile\n ? $upload\n : array('del' =&gt; 'Y'),\n );\n $element = new CIBlockElement();\n\n if (!$element-&gt;Update((int) $elementId, $fields)) {\n throw new RuntimeException($element-&gt;LAST_ERROR);\n }\n}</code></pre>\n<p>Для старых версий Bitrix формат входного файла и политика удаления могут отличаться. Если файл уже зарегистрирован отдельно, используйте подтверждённый ID и соберите файловый массив через <code>CFile::MakeFileArray()</code>. После <code>Update()</code> проверяйте не только возврат метода, но и <code>LAST_ERROR</code>. Для файлового свойства, а не поля <code>PREVIEW_PICTURE</code>, формат <code>PROPERTY_VALUES</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 или обработчик не вызвал Update</td><td>Network: multipart-часть и серверный лог ID элемента</td><td>исправить <code>enctype</code>, имя поля или ветку сохранения</td></tr><tr><td>Ответ 200, поле пустое</td><td>ошибка скрыта за ответом или неверный формат поля</td><td>проверить JSON/HTML ответа, <code>Update()</code> и <code>LAST_ERROR</code></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>сравнить явный флаг удаления с состоянием DOM</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></ol>\n<h2>Ограничения</h2>\n<p>Пример не задаёт универсальные MIME-типы, размеры, права и антивирусную проверку. Эти правила зависят от версии Bitrix, настроек PHP, роли пользователя и требований каталога. Проверка расширения в браузере не защищает сервер. На сервере нужно проверять размер, фактический тип, ошибки загрузки, доступ к элементу и допустимость операции для пользователя.</p>\n<p>Если FileInput загружает файл отдельным запросом, путь через <code>$_FILES</code> неприменим без адаптации. Сначала определите, какое значение возвращает контрол и где хранится временный файл. При отмене формы не оставляйте такой ID без владельца. Для небольшой синхронной формы не нужны очереди, но нужна явная политика очистки незавершённых загрузок.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Интеграция готова, если форма делает один понятный запрос, новый файл проходит серверные проверки, <code>Update()</code> возвращает успех, а свежая страница показывает новое изображение. Сохранение без нового файла оставляет старое значение. Явное удаление меняет поле только по флагу пользователя. Конфликт «новый файл плюс удаление» даёт ошибку. В браузере после Ajax-перерисовки нет двойного обработчика. Эти условия проверяются на тестовой записи; они не являются заявлением о результате production.</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://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects\" target=\"_blank\" rel=\"noopener\">MDN: Using FormData Objects</a></li><li><a href=\"https://api.jquery.com/on/\" target=\"_blank\" rel=\"noopener\">Официальная документация jQuery: .on()</a></li></ul>"
}