Files
progcode/editorial/agent-rewrites/326.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
18 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": 326,
"slug": "editorial-2018-12-mechanism-legacy-refactoring",
"title": "Bitrix: как безопасно вынести запись из смешанного save.php",
"excerpt": "Старый обработчик формы может изменить инфоблок, отправить уведомление и показать HTML в одном проходе. Разбираем, как отделить запись элемента, увидеть отрицательный путь и проверить локальный рефакторинг без ложного сообщения об успехе.",
"contentHtml": "<p>Форма сообщает «ошибка», но название товара уже изменилось. Пользователь нажимает «Сохранить» ещё раз и повторно запускает письмо или внешний вызов. Цена ошибки — дублирование побочного эффекта, потеря причины и спор о том, какое состояние считать правильным.</p>\n<p>Так происходит, когда старый <code>save.php</code> читает <code>$_POST</code>, вызывает <code>CIBlockElement::Update</code>, отправляет уведомление и выводит HTML в одной функции. Первый рефакторинг такого файла должен разделить не все слои системы, а один наблюдаемый переход. Сначала отделяем запись элемента от формы и последующих действий. Затем проверяем фактическое значение повторным чтением.</p>\n<h2>Симптом начинается раньше сообщения</h2>\n<p>Один HTTP-запрос может оставить несколько следов. Входные поля приходят из формы. Инфоблок хранит изменённое поле. Почтовый или сетевой вызов оставляет след во внешней системе. Браузер получает только последний ответ обработчика. Если поздний вызов падает, этот ответ не рассказывает, что произошло с инфоблоком несколькими строками выше.</p>\n<p>Длина файла здесь ничего не решает. Небольшая функция тоже опасна, если она сама читает глобальный массив, меняет данные и решает, какой HTML вывести. Для диагностики разделите три вопроса: вход допустим, запись состоялась, следующий побочный эффект выполнен. Один текст «сохранено» не может честно отвечать на все три.</p>\n<figure><img src=\"/assets/editorial/2018/bitrix-mixed-responsibility-2018.svg\" alt=\"Смешанный обработчик save.php читает POST, меняет CODE в инфоблоке, формирует HTML и запускает внешний эффект; поздняя ошибка не отменяет раннюю запись\" loading=\"lazy\" /><figcaption>Смешанный обработчик скрывает границу между записью и последующим действием. Поздний сбой не доказывает, что ранняя запись не произошла.</figcaption></figure>\n<h2>Механизм: Update и ответ формы живут на разных границах</h2>\n<p><code>CIBlockElement::Update</code> возвращает логический результат и принимает массив полей. До записи Bitrix вызывает обработчики, которые могут изменить параметры или отменить операцию. После попытки обновления работают обработчики другого этапа. Поэтому результат метода и текст, который позже печатает форма, нельзя считать одним событием.</p>\n<p>Если <code>Update</code> вернул ошибку, обработчик может показать её и остановиться. Если он вернул успех, это подтверждает результат вызова метода, но не успешную отправку письма и не согласованность внешнего каталога. Если после успеха возникло исключение, поле уже могло измениться. Повтор всей формы в таком состоянии опаснее, чем повтор чтения.</p>\n<table><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>Браузер показал ошибку, но поле изменилось</td><td>Сбой произошёл после <code>Update</code></td><td>Снова прочитать элемент по ID и сравнить поле</td><td>Не повторять POST; разобрать поздний вызов</td></tr><tr><td>Пустой ID выглядит как ошибка Bitrix</td><td>Глобальный вход привели к целому числу поздно</td><td>Проверить ID и обязательные поля до записи</td><td>Вернуть ошибку валидации без записи</td></tr><tr><td>Значение отличается от расчёта</td><td>Обработчик изменил вход или другой процесс записал поле</td><td>Проверить обработчики и повторную выборку</td><td>Остановить расширение шва</td></tr><tr><td>Повтор формы отправил два уведомления</td><td>Внешний эффект не имеет отдельного статуса</td><td>Развести результат записи и уведомления</td><td>Определить политику повтора</td></tr><tr><td>Затронуты лишние свойства</td><td>В <code>Update</code> передали широкий массив</td><td>Сравнить ключи с контрактом</td><td>Передавать только нужное поле</td></tr></tbody></table>\n<h2>Учебный пример смешанного обработчика</h2>\n<p>Следующий фрагмент — учебный пример, ограниченный объяснением механизма. Он не взят из конкретного production-проекта и не доказывает наличие функции <code>sendPartnerNotice</code> в вашем сайте. В нём намеренно оставлены типичные границы старого файла.</p>\n<pre><code>&lt;?php&#10;if ($_SERVER['REQUEST_METHOD'] === 'POST') {&#10; $elementId = (int) $_POST['ID'];&#10; $name = trim((string) $_POST['NAME']);&#10;&#10; if ($elementId &lt;= 0 || $name === '') {&#10; echo 'Заполните ID и название.';&#10; return;&#10; }&#10;&#10; CModule::IncludeModule('iblock');&#10; $element = new CIBlockElement();&#10; if (!$element-&gt;Update($elementId, array('NAME' =&gt; $name))) {&#10; echo $element-&gt;LAST_ERROR;&#10; return;&#10; }&#10;&#10; sendPartnerNotice($elementId, $name);&#10; echo 'Сохранено';&#10;}</code></pre>\n<p>У примера три отрицательных пути. Валидация останавливает запрос до записи. Bitrix может вернуть отказ. Внешний вызов может завершиться ошибкой после успешной записи. Последний путь нельзя исправить строкой «повторить Update»: она не делает внешнюю операцию безопасной и скрывает, что поле уже изменилось.</p>\n<h2>Первый шов: функция возвращает только результат записи</h2>\n<p>Вынесенная функция получает нормализованные значения аргументами. Она не знает о форме, не печатает HTML и не вызывает интеграцию. Её контракт узкий: вернуть ID после успешного обновления или остановить путь исключением. Это не новая архитектура. Это точка проверки входа, API и ошибки.</p>\n<pre><code>&lt;?php&#10;function saveProductName($elementId, $name)&#10;{&#10; if (!CModule::IncludeModule('iblock')) {&#10; throw new RuntimeException('Module iblock is unavailable');&#10; }&#10;&#10; $elementId = (int) $elementId;&#10; $name = trim((string) $name);&#10; if ($elementId &lt;= 0 || $name === '') {&#10; throw new InvalidArgumentException('ID and NAME are required');&#10; }&#10;&#10; $element = new CIBlockElement();&#10; if (!$element-&gt;Update($elementId, array('NAME' =&gt; $name))) {&#10; throw new RuntimeException($element-&gt;LAST_ERROR ?: 'Element update failed');&#10; }&#10;&#10; return $elementId;&#10;}&#10;&#10;try {&#10; $savedId = saveProductName($_POST['ID'], $_POST['NAME']);&#10; $message = 'Карточка сохранена: ' . $savedId;&#10;} catch (InvalidArgumentException $error) {&#10; $message = $error-&gt;getMessage();&#10;} catch (RuntimeException $error) {&#10; $message = $error-&gt;getMessage();&#10;}</code></pre>\n<p>Фрагмент остаётся учебным. Он не заменяет правила доступа, CSRF-защиту, журналирование и обработку конкретной версии Bitrix. Перед переносом сохраните контракт формы и проверьте зарегистрированные обработчики.</p>\n<p>После успешной записи следующий эффект вызывают явно. Если уведомление допустимо только для изменённого элемента, его место видно после <code>saveProductName</code>. Если уведомление упало, сообщение должно назвать именно эту проблему. Нельзя превращать её в «элемент не сохранён», если повторное чтение показывает обратное.</p>\n<h2>Обработчики и набор полей</h2>\n<p><code>OnBeforeIBlockElementUpdate</code> получает параметры до изменения и может их переопределить или отменить операцию. Обработчик после попытки обновления может сработать и при неудаче, поэтому в нём нужно смотреть результат, а не только факт вызова. Эти события объясняют расхождение между переданным массивом и состоянием элемента.</p>\n<p>Передавайте в <code>Update</code> только поля текущей задачи. Широкий массив повышает цену ошибки: обработчик видит лишнее, а свойства могут получить нежелательное значение. Узкий массив не гарантирует атомарность, но сокращает поверхность изменения. Если старый код обновляет имя, не добавляйте туда свойства и разделы «для полноты».</p>\n<p>Проверяйте не только ответ метода. После разрешённого тестового вызова снова выберите тот же элемент по ID и сравните фактическое поле с ожидаемым. Это не производственный результат и не доказательство безопасности всех вызовов. Это локальная проверка одного входа, инфоблока и поля.</p>\n<h2>Порядок локальной переделки</h2>\n<ol><li>Запишите один симптом: например, ошибка формы не отвечает, изменилось ли имя.</li><li>Перечислите побочные эффекты старого обработчика в фактическом порядке: запись, уведомление, кеш, HTML и внешние вызовы.</li><li>Выберите один эффект для первого шва и зафиксируйте ID, инфоблок, поле, успех и отказ.</li><li>Проверьте зарегистрированные обработчики Bitrix и все места вызова участка.</li><li>Вынесите запись в функцию с явными аргументами. Уберите из неё <code>$_POST</code>, <code>echo</code> и сторонние вызовы.</li><li>Подключите функцию одним вызовом, оставив неисследованные действия в прежнем порядке.</li><li>На разрешённом тестовом элементе сохраните исходное значение, выполните один вызов и прочитайте поле обратно.</li><li>Проверьте пустой вход, неверный ID, отказ обработчика и ошибку позднего эффекта.</li><li>Если значение расходится с ожидаемым, остановите расширение. Не добавляйте повторную запись наугад.</li></ol>\n<h2>Отрицательный путь важнее зелёного сообщения</h2>\n<p>Нужно различать четыре состояния: вход отклонён, запись отклонена, запись подтверждена, поздний эффект завершился отдельно. Первые два не должны менять элемент. Третье подтверждается повторным чтением. Четвёртое сообщает о частичном результате и не заставляет пользователя повторять весь запрос.</p>\n<p>Откат маршрута и откат данных — разные действия. Вернуть старую функцию для следующих запросов можно быстро. Восстановить старое поле безопасно только после проверки, что его не изменил другой процесс. Сохранённое до теста значение не даёт права перезаписать карточку поверх работы редактора или импорта.</p>\n<h2>Ограничения метода</h2>\n<p>Этот шов не создаёт транзакцию между инфоблоком и внешним API. Он не делает повтор сетевого запроса идемпотентным. Он не устраняет гонки, кеш, права доступа и все обработчики проекта. Если операция требует согласованного изменения нескольких систем, нужен отдельный контракт: ключ операции, допустимый повтор, состояние частичного выполнения и владелец восстановления.</p>\n<p>Не нужно выносить каждую строку PHP в класс. Если один вызов уже имеет ясный вход и не скрывает побочные эффекты, функции достаточно. Шов оправдан там, где смешение мешает ответить на вопрос о состоянии данных.</p>\n<h2>Критерий готовности</h2>\n<p>Локальный рефакторинг готов, когда для одного выбранного элемента выполнены все условия: вход проходит отдельную проверку; функция меняет только заявленное поле; результат <code>Update</code> обработан явно; обработчики просмотрены; фактическое значение подтверждено повторной выборкой; поздний эффект имеет отдельный результат; отрицательный путь не запускает повторную запись; при расхождении есть действие остановки или возврата.</p>\n<p>Если хотя бы один пункт не проверен, готов только шов к следующему исследованию. Это лучше зелёного сообщения, которое скрывает уже изменённые данные.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://www.dev.1c-bitrix.ru/api_help/iblock/classes/ciblockelement/update.php?print=Y\" target=\"_blank\" rel=\"noopener\">Официальная документация 1С-Битрикс: CIBlockElement::Update</a></li><li><a href=\"https://www.dev.1c-bitrix.ru/api_help/iblock/events/onbeforeiblockelementupdate.php\" target=\"_blank\" rel=\"noopener\">Официальная документация 1С-Битрикс: OnBeforeIBlockElementUpdate</a></li><li><a href=\"https://www.php.net/manual/en/class.runtimeexception.php\" target=\"_blank\" rel=\"noopener\">PHP Manual: RuntimeException</a></li></ul>"
}