{ "index": 326, "slug": "editorial-2018-12-mechanism-legacy-refactoring", "title": "Bitrix: как безопасно вынести запись из смешанного save.php", "excerpt": "Старый обработчик формы может изменить инфоблок, отправить уведомление и показать HTML в одном проходе. Разбираем, как отделить запись элемента, увидеть отрицательный путь и проверить локальный рефакторинг без ложного сообщения об успехе.", "contentHtml": "
Форма сообщает «ошибка», но название товара уже изменилось. Пользователь нажимает «Сохранить» ещё раз и повторно запускает письмо или внешний вызов. Цена ошибки — дублирование побочного эффекта, потеря причины и спор о том, какое состояние считать правильным.
\nТак происходит, когда старый save.php читает $_POST, вызывает CIBlockElement::Update, отправляет уведомление и выводит HTML в одной функции. Первый рефакторинг такого файла должен разделить не все слои системы, а один наблюдаемый переход. Сначала отделяем запись элемента от формы и последующих действий. Затем проверяем фактическое значение повторным чтением.
Один HTTP-запрос может оставить несколько следов. Входные поля приходят из формы. Инфоблок хранит изменённое поле. Почтовый или сетевой вызов оставляет след во внешней системе. Браузер получает только последний ответ обработчика. Если поздний вызов падает, этот ответ не рассказывает, что произошло с инфоблоком несколькими строками выше.
\nДлина файла здесь ничего не решает. Небольшая функция тоже опасна, если она сама читает глобальный массив, меняет данные и решает, какой HTML вывести. Для диагностики разделите три вопроса: вход допустим, запись состоялась, следующий побочный эффект выполнен. Один текст «сохранено» не может честно отвечать на все три.
\nCIBlockElement::Update возвращает логический результат и принимает массив полей. До записи Bitrix вызывает обработчики, которые могут изменить параметры или отменить операцию. После попытки обновления работают обработчики другого этапа. Поэтому результат метода и текст, который позже печатает форма, нельзя считать одним событием.
Если Update вернул ошибку, обработчик может показать её и остановиться. Если он вернул успех, это подтверждает результат вызова метода, но не успешную отправку письма и не согласованность внешнего каталога. Если после успеха возникло исключение, поле уже могло измениться. Повтор всей формы в таком состоянии опаснее, чем повтор чтения.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Браузер показал ошибку, но поле изменилось | Сбой произошёл после Update | Снова прочитать элемент по ID и сравнить поле | Не повторять POST; разобрать поздний вызов |
| Пустой ID выглядит как ошибка Bitrix | Глобальный вход привели к целому числу поздно | Проверить ID и обязательные поля до записи | Вернуть ошибку валидации без записи |
| Значение отличается от расчёта | Обработчик изменил вход или другой процесс записал поле | Проверить обработчики и повторную выборку | Остановить расширение шва |
| Повтор формы отправил два уведомления | Внешний эффект не имеет отдельного статуса | Развести результат записи и уведомления | Определить политику повтора |
| Затронуты лишние свойства | В Update передали широкий массив | Сравнить ключи с контрактом | Передавать только нужное поле |
Следующий фрагмент — учебный пример, ограниченный объяснением механизма. Он не взят из конкретного production-проекта и не доказывает наличие функции sendPartnerNotice в вашем сайте. В нём намеренно оставлены типичные границы старого файла.
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$elementId = (int) $_POST['ID'];
$name = trim((string) $_POST['NAME']);
if ($elementId <= 0 || $name === '') {
echo 'Заполните ID и название.';
return;
}
CModule::IncludeModule('iblock');
$element = new CIBlockElement();
if (!$element->Update($elementId, array('NAME' => $name))) {
echo $element->LAST_ERROR;
return;
}
sendPartnerNotice($elementId, $name);
echo 'Сохранено';
}\nУ примера три отрицательных пути. Валидация останавливает запрос до записи. Bitrix может вернуть отказ. Внешний вызов может завершиться ошибкой после успешной записи. Последний путь нельзя исправить строкой «повторить Update»: она не делает внешнюю операцию безопасной и скрывает, что поле уже изменилось.
\nВынесенная функция получает нормализованные значения аргументами. Она не знает о форме, не печатает HTML и не вызывает интеграцию. Её контракт узкий: вернуть ID после успешного обновления или остановить путь исключением. Это не новая архитектура. Это точка проверки входа, API и ошибки.
\n<?php
function saveProductName($elementId, $name)
{
if (!CModule::IncludeModule('iblock')) {
throw new RuntimeException('Module iblock is unavailable');
}
$elementId = (int) $elementId;
$name = trim((string) $name);
if ($elementId <= 0 || $name === '') {
throw new InvalidArgumentException('ID and NAME are required');
}
$element = new CIBlockElement();
if (!$element->Update($elementId, array('NAME' => $name))) {
throw new RuntimeException($element->LAST_ERROR ?: 'Element update failed');
}
return $elementId;
}
try {
$savedId = saveProductName($_POST['ID'], $_POST['NAME']);
$message = 'Карточка сохранена: ' . $savedId;
} catch (InvalidArgumentException $error) {
$message = $error->getMessage();
} catch (RuntimeException $error) {
$message = $error->getMessage();
}\nФрагмент остаётся учебным. Он не заменяет правила доступа, CSRF-защиту, журналирование и обработку конкретной версии Bitrix. Перед переносом сохраните контракт формы и проверьте зарегистрированные обработчики.
\nПосле успешной записи следующий эффект вызывают явно. Если уведомление допустимо только для изменённого элемента, его место видно после saveProductName. Если уведомление упало, сообщение должно назвать именно эту проблему. Нельзя превращать её в «элемент не сохранён», если повторное чтение показывает обратное.
OnBeforeIBlockElementUpdate получает параметры до изменения и может их переопределить или отменить операцию. Обработчик после попытки обновления может сработать и при неудаче, поэтому в нём нужно смотреть результат, а не только факт вызова. Эти события объясняют расхождение между переданным массивом и состоянием элемента.
Передавайте в Update только поля текущей задачи. Широкий массив повышает цену ошибки: обработчик видит лишнее, а свойства могут получить нежелательное значение. Узкий массив не гарантирует атомарность, но сокращает поверхность изменения. Если старый код обновляет имя, не добавляйте туда свойства и разделы «для полноты».
Проверяйте не только ответ метода. После разрешённого тестового вызова снова выберите тот же элемент по ID и сравните фактическое поле с ожидаемым. Это не производственный результат и не доказательство безопасности всех вызовов. Это локальная проверка одного входа, инфоблока и поля.
\n$_POST, echo и сторонние вызовы.Нужно различать четыре состояния: вход отклонён, запись отклонена, запись подтверждена, поздний эффект завершился отдельно. Первые два не должны менять элемент. Третье подтверждается повторным чтением. Четвёртое сообщает о частичном результате и не заставляет пользователя повторять весь запрос.
\nОткат маршрута и откат данных — разные действия. Вернуть старую функцию для следующих запросов можно быстро. Восстановить старое поле безопасно только после проверки, что его не изменил другой процесс. Сохранённое до теста значение не даёт права перезаписать карточку поверх работы редактора или импорта.
\nЭтот шов не создаёт транзакцию между инфоблоком и внешним API. Он не делает повтор сетевого запроса идемпотентным. Он не устраняет гонки, кеш, права доступа и все обработчики проекта. Если операция требует согласованного изменения нескольких систем, нужен отдельный контракт: ключ операции, допустимый повтор, состояние частичного выполнения и владелец восстановления.
\nНе нужно выносить каждую строку PHP в класс. Если один вызов уже имеет ясный вход и не скрывает побочные эффекты, функции достаточно. Шов оправдан там, где смешение мешает ответить на вопрос о состоянии данных.
\nЛокальный рефакторинг готов, когда для одного выбранного элемента выполнены все условия: вход проходит отдельную проверку; функция меняет только заявленное поле; результат Update обработан явно; обработчики просмотрены; фактическое значение подтверждено повторной выборкой; поздний эффект имеет отдельный результат; отрицательный путь не запускает повторную запись; при расхождении есть действие остановки или возврата.
Если хотя бы один пункт не проверен, готов только шов к следующему исследованию. Это лучше зелёного сообщения, которое скрывает уже изменённые данные.
\n