Files
progcode/editorial/agent-rewrites/326.json
T

8 lines
19 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, меняет NAME в инфоблоке, формирует HTML и запускает внешний эффект; поздняя ошибка не отменяет раннюю запись\" loading=\"lazy\" /><figcaption>Смешанный обработчик скрывает границу между записью и последующим действием. Поздний сбой не доказывает, что ранняя запись не произошла.</figcaption></figure>\n<h2>Механизм: Update и ответ формы живут на разных границах</h2>\n<p><code>CIBlockElement::Update</code> принимает ID и массив полей. Официальная документация фиксирует его контракт: метод возвращает <code>true</code> при успехе и <code>false</code> при ошибке, а текст ошибки доступен в <code>LAST_ERROR</code>. До записи обработчик <code>OnBeforeIBlockElementUpdate</code> может изменить параметры или отменить операцию. После попытки обновления может сработать обработчик другого этапа.</p>\n<p>Поэтому результат метода и текст, который позже печатает форма, нельзя считать одним событием. Успешный <code>Update</code> подтверждает результат записи, но не успешную отправку письма и не согласованность внешнего каталога. Если после успеха возникло исключение, поле уже могло измениться. Повтор всей формы в таком состоянии опаснее, чем повторное чтение.</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>Браузер показал ошибку, но поле изменилось</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>Следующий фрагмент — учебный пример механизма, а не отчёт о конкретном проекте. Функция <code>sendPartnerNotice</code> здесь условна: перед переносом нужно найти реальный внешний вызов и определить его собственный результат.</p>\n<pre><code>&lt;?php\nif ($_SERVER['REQUEST_METHOD'] === 'POST') {\n $elementId = (int) $_POST['ID'];\n $name = trim((string) $_POST['NAME']);\n\n if ($elementId &lt;= 0 || $name === '') {\n echo 'Заполните ID и название.';\n return;\n }\n\n CModule::IncludeModule('iblock');\n $element = new CIBlockElement();\n if (!$element-&gt;Update($elementId, array('NAME' =&gt; $name))) {\n echo $element-&gt;LAST_ERROR;\n return;\n }\n\n sendPartnerNotice($elementId, $name);\n echo 'Сохранено';\n}</code></pre>\n<p>У примера три отрицательных пути. Валидация останавливает запрос до записи. Bitrix может вернуть отказ. Внешний вызов может завершиться ошибкой после успешной записи. Последний путь нельзя исправить строкой «повторить Update»: она не делает внешнюю операцию безопасной и скрывает, что поле уже изменилось.</p>\n<h2>Первый шов: функция возвращает только результат записи</h2>\n<p>Вынесенная функция получает нормализованные значения аргументами. Она не знает о форме, не печатает HTML и не вызывает интеграцию. Её контракт узкий: вернуть ID после успешного обновления или остановить путь исключением. Это не новая архитектура, а точка проверки входа, API и ошибки.</p>\n<pre><code>&lt;?php\nfunction saveProductName($elementId, $name)\n{\n if (!CModule::IncludeModule('iblock')) {\n throw new RuntimeException('Module iblock is unavailable');\n }\n\n $elementId = (int) $elementId;\n $name = trim((string) $name);\n if ($elementId &lt;= 0 || $name === '') {\n throw new InvalidArgumentException('ID and NAME are required');\n }\n\n $element = new CIBlockElement();\n if (!$element-&gt;Update($elementId, array('NAME' =&gt; $name))) {\n throw new RuntimeException($element-&gt;LAST_ERROR ?: 'Element update failed');\n }\n\n return $elementId;\n}\n\ntry {\n $savedId = saveProductName($_POST['ID'], $_POST['NAME']);\n $message = 'Карточка сохранена: ' . $savedId;\n} catch (InvalidArgumentException $error) {\n $message = $error-&gt;getMessage();\n} catch (RuntimeException $error) {\n $message = $error-&gt;getMessage();\n}</code></pre>\n<p>Фрагмент остаётся учебным. Он не заменяет правила доступа, CSRF-защиту, журналирование и обработку конкретной версии Bitrix. Перед переносом сохраните контракт формы и проверьте зарегистрированные обработчики. Если поля могут отсутствовать в запросе, добавьте явную проверку ключей до обращения к ним.</p>\n<p>После успешной записи следующий эффект вызывают явно. Если уведомление допустимо только для изменённого элемента, его место видно после <code>saveProductName</code>. Если уведомление упало, сообщение должно назвать именно эту проблему. Нельзя превращать её в «элемент не сохранён», если повторное чтение показывает обратное.</p>\n<h2>Обработчики и набор полей</h2>\n<p><code>OnBeforeIBlockElementUpdate</code> получает параметры до изменения и может их переопределить или отменить операцию. <code>OnAfterIBlockElementUpdate</code> вызывается после попытки обновления даже при неудаче; в его массиве доступны <code>RESULT</code> и, при ошибке, <code>RESULT_MESSAGE</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://dev.1c-bitrix.ru/api_help/iblock/classes/ciblockelement/update.php?print=Y\" target=\"_blank\" rel=\"noopener noreferrer\">Официальная документация 1С-Битрикс: CIBlockElement::Update</a></li><li><a href=\"https://dev.1c-bitrix.ru/api_help/iblock/events/onbeforeiblockelementupdate.php\" target=\"_blank\" rel=\"noopener noreferrer\">Официальная документация 1С-Битрикс: OnBeforeIBlockElementUpdate</a></li><li><a href=\"https://dev.1c-bitrix.ru/api_help/iblock/events/onafteriblockelementupdate.php\" target=\"_blank\" rel=\"noopener noreferrer\">Официальная документация 1С-Битрикс: OnAfterIBlockElementUpdate</a></li><li><a href=\"https://www.php.net/manual/en/class.runtimeexception.php\" target=\"_blank\" rel=\"noopener noreferrer\">PHP Manual: RuntimeException</a></li></ul>"
}