8 lines
17 KiB
JSON
8 lines
17 KiB
JSON
{
|
||
"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><?php\nuse Bitrix\\Main\\UI\\FileInput;\n\n$currentId = (int) $arResult['PREVIEW_PICTURE'];\n?>\n<form method=\"post\" enctype=\"multipart/form-data\" id=\"catalog-photo-form\">\n <?php\n echo FileInput::createInstance(array(\n 'id' => 'catalog_preview',\n 'name' => 'CATALOG_PREVIEW',\n 'upload' => true,\n 'allowUpload' => FileInput::UPLOAD_IMAGES,\n 'maxCount' => 1,\n 'delete' => true,\n ))->show($currentId);\n ?>\n <label><input type=\"checkbox\" name=\"DELETE_PICTURE\" value=\"1\"> удалить</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: 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 && 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 && $delete) {\n throw new RuntimeException('Выберите замену или удаление');\n }\n if (!$hasNewFile && !$delete) {\n return; // оставить текущее значение\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}</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>"
|
||
}
|