{ "index": 327, "slug": "editorial-2018-12-practice-legacy-refactoring", "title": "Bitrix: как безопасно заменить один CIBlockElement::Update", "excerpt": "Как выделить узкий шов вокруг CIBlockElement::Update, проверить вход, не задеть чужой инфоблок и подтвердить результат повторной выборкой.", "contentHtml": "

В старом Bitrix-обработчике карточка товара иногда получает пустой символьный код. Страница после сохранения отвечает успешно, но прежний URL перестаёт вести к элементу. Ошибка может проявиться позже: её поймает импорт, кеш или шаблон, который строит ссылку из поля CODE. Цена одной неверной правки — 404, потерянный переход из поиска и сложное расследование, потому что обработчик уже смешивает форму, запись и вывод сообщения.

\n

Проблему часто пытаются решить заменой всех вызовов на новый класс. Это увеличивает область риска. Надёжнее сначала выделить один переход: вход формы → выбранный элемент → изменение одного поля → повторное чтение. Такой шов не переписывает модуль. Он делает результат конкретного вызова наблюдаемым.

\n

Тезис: узкий шов должен доказывать одну запись

\n

Шов вокруг CIBlockElement::Update принимает ID элемента, ожидаемый ID инфоблока и новый CODE. До записи он проверяет вход и читает текущий элемент. После записи он снова читает тот же ID и сравнивает фактическое значение с ожидаемым. Если условие не выполнено, путь останавливается с ошибкой.

\n

Шов не отвечает за HTML, редирект, отправку писем и очистку кеша. Старый обработчик может оставить эти действия рядом. Но он не должен считать их доказательством успешной записи. Сообщение в браузере показывает результат HTTP-сценария, а повторная выборка показывает состояние элемента.

\n
\"Схема
Шов охватывает только переход к полю CODE. Форма и внешние действия остаются за его границей.
\n

Механизм: ограничить вход, поле и результат

\n

Сначала назовите контракт. ID должен быть положительным целым числом. Новый код после trim() не должен быть пустым. Выборка по ID должна вернуть элемент. Его IBLOCK_ID должен совпасть с ожидаемым. В вызов Update передаём только CODE, а не весь массив формы.

\n

Проверка инфоблока защищает от особенно неприятной ошибки: правильный ID может относиться к другой сущности. Передача всех полей также опасна. Пустое свойство из формы или устаревшее значение из массива может затереть данные, которых не было в задаче. Чем уже массив изменения, тем короче след операции.

\n
СимптомПричинаПроверкаДействие
URL стал пустым или изменился неожиданноВ Update попал пустой или чужой CODEСравнить вход, IBLOCK_ID и значение после записиОстановить путь и передавать только проверенный CODE
Метод вернул успех, но поле не совпалоОбработчик события изменил данные после вызоваСнова выбрать элемент по тому же IDРазобрать обработчики до расширения замены
Ошибка формы появилась после сохраненияПосле записи упало письмо, кеш или внешняя интеграцияРазделить результат записи и результат следующего действияНе повторять Update; показать отдельную ошибку
Проверка работает в одном месте и ломается в другомОдин legacy-вызов имеет несколько источников входаНайти все вызовы и записать их предусловияПереключать вызовы по одному
Откат затирает новое значениеСтарый снимок уже не соответствует текущему состояниюСравнить текущее поле со значением, записанным экспериментомНе восстанавливать поле автоматически при расхождении
\n

Учебный пример шва

\n

Ниже приведён учебный пример для старого Bitrix API и PHP 7.2. Он не утверждает, что такой код уже запускался в production. ID инфоблока, права, обработчики событий и правила уникальности нужно проверить в конкретном проекте.

\n
<?php\n\nfinal class ProductCodeWriter\n{\n    private $expectedIblockId;\n\n    public function __construct($expectedIblockId)\n    {\n        $this->expectedIblockId = (int) $expectedIblockId;\n    }\n\n    public function write($elementId, $code)\n    {\n        if (!CModule::IncludeModule('iblock')) {\n            throw new RuntimeException('Модуль iblock недоступен');\n        }\n\n        $elementId = (int) $elementId;\n        $code = trim((string) $code);\n        if ($elementId <= 0 || $code === '') {\n            throw new InvalidArgumentException('Нужны ID элемента и непустой CODE');\n        }\n\n        $before = $this->find($elementId);\n        if (!$before || (int) $before['IBLOCK_ID'] !== $this->expectedIblockId) {\n            throw new RuntimeException('Элемент не найден в ожидаемом инфоблоке');\n        }\n\n        if ((string) $before['CODE'] === $code) {\n            return array('changed' => false, 'code' => $code);\n        }\n\n        $element = new CIBlockElement();\n        if (!$element->Update($elementId, array('CODE' => $code))) {\n            throw new RuntimeException($element->LAST_ERROR ?: 'Update завершился ошибкой');\n        }\n\n        $after = $this->find($elementId);\n        if (!$after || (string) $after['CODE'] !== $code) {\n            throw new RuntimeException('CODE не подтвердился после Update');\n        }\n\n        return array('changed' => true, 'code' => $after['CODE']);\n    }\n\n    private function find($elementId)\n    {\n        $result = CIBlockElement::GetList(\n            array(),\n            array('ID' => (int) $elementId),\n            false,\n            false,\n            array('ID', 'IBLOCK_ID', 'CODE')\n        );\n\n        return $result->Fetch();\n    }\n}
\n

Важны четыре границы. Класс не читает глобальный $_POST. Он не печатает сообщение. Он не принимает решение за внешний сервис. Он не возвращает true только потому, что метод API не сообщил об ошибке. Повторная выборка делает условие готовности явным.

\n

Если код уже совпадает, метод возвращает changed: false. Это нормальный результат идемпотентного повторного вызова. Не нужно писать в базу второй раз ради сообщения «сохранено». Если Update вернул false, исключение содержит LAST_ERROR, когда Bitrix его заполнил. Продолжать к письму или редиректу после такого отказа нельзя.

\n

Подключение из старого обработчика

\n

Старый файл может сохранить свою проверку запроса и выбор шаблона. Меняется только прямой вызов API. Класс возвращает отчёт, а обработчик решает, как показать его пользователю.

\n
try {\n    $writer = new ProductCodeWriter(7);\n    $report = $writer->write($_POST['ID'], $_POST['CODE']);\n\n    $message = $report['changed']\n        ? 'Символьный код обновлён.'\n        : 'Символьный код уже совпадает.';\n} catch (InvalidArgumentException $error) {\n    $message = $error->getMessage();\n} catch (RuntimeException $error) {\n    $message = $error->getMessage();\n}
\n

Число 7 — пример, а не значение для копирования. В рабочем проекте его берут из конфигурации сценария и проверяют по данным элемента. Если обработчик после этой конструкции отправляет письмо, ошибка письма не означает, что CODE не записался. Логи и сообщение должны различать две операции.

\n

Порядок проверки

\n
  1. Найдите прямой вызов CIBlockElement::Update и запишите его источники входа: форма, импорт, cron или событие.
  2. Выберите один элемент на разрешённом тестовом контуре. Не используйте рабочую карточку только ради быстрого эксперимента.
  3. Сохраните ID элемента, ID инфоблока, старый CODE и ожидаемый новый код.
  4. Проверьте отрицательные входы: нулевой ID, пустой код, неизвестный элемент и элемент из другого инфоблока.
  5. Вызовите шов для одного элемента и отдельно зафиксируйте его отчёт.
  6. Снова выберите элемент через API и сравните фактический CODE с ожидаемым.
  7. Проверьте путь после записи отдельно: письмо, кеш, редирект или интеграция не должны маскировать результат шва.
  8. Если значение расходится, отключите новый маршрут для следующих вызовов и разберите обработчики событий. Не добавляйте второй Update «на всякий случай».
  9. Только после этого подключайте следующий источник входа и повторяйте проверку с его собственными условиями.
\n

Ограничения и отрицательный путь

\n

Шов не делает CODE уникальным. Если два элемента получают один код, правило нужно проверять отдельным запросом и учитывать политику проекта. Шов также не решает права доступа, SEO-правила, синхронизацию торговых предложений и очистку кеша. Эти обязанности нельзя приписывать одной функции записи.

\n

У Bitrix есть обработчики до и после изменения. Обработчик до записи может изменить поле или отменить операцию. Обработчик после записи может запустить другой эффект. Поэтому успешный возврат Update и подтверждённый CODE доказывают только состояние выбранного элемента в момент повторного чтения. Они не доказывают согласованность поиска и внешнего каталога.

\n

Откат маршрута и откат данных — разные действия. Возврат к старому классу защищает следующие вызовы, но не возвращает уже изменённое поле. Восстанавливать старый CODE можно только после проверки, что текущим значением остаётся результат этого эксперимента. Если его изменил редактор или импорт, остановитесь и согласуйте восстановление. Молчаливый откат может затереть более новое изменение.

\n

Не каждый вызов требует класса. Если функция уже получает явные аргументы, меняет одно поле и возвращает результат, дополнительная оболочка не даст пользы. Выделяйте шов там, где операция смешана с формой, повторяется или нуждается в отдельной проверке. Цель — не увеличить число файлов, а сделать границу проверяемой.

\n

Критерий готовности

\n

Локальная замена готова, если для выбранного вызова выполнены все условия: вход явно ограничен; проверен ожидаемый инфоблок; в Update передано только нужное поле; отрицательные пути останавливают сценарий; результат повторно прочитан по тому же ID; обработчики и действия после записи не выданы за доказательство успешного сохранения. После расхождения существует понятный путь отключения нового маршрута, а восстановление данных не выполняется поверх чужого изменения.

\n

Это не сертификат безопасности всего legacy-модуля. Это проверяемое утверждение об одной операции. Когда оно подтверждено, следующий участок можно разбирать отдельно.

\n

Проверяемые источники

" }