{ "index": 325, "slug": "editorial-2018-12-field-legacy-refactoring", "title": "Bitrix: как заменить генерацию CODE в legacy-коде и не потерять исходное значение", "excerpt": "Точечная замена обработчика Bitrix начинается с одного элемента: фиксируем исходный CODE, проверяем инфоблок, записываем только нужное поле и читаем результат обратно. Отдельно разбираем отрицательный путь и осторожный откат.", "contentHtml": "
После изменения формы карточка товара открывается по старому адресу, а новая запись получает другой URL. Иногда поле CODE остаётся пустым. Иногда его меняет обработчик, о котором забыли. Цена ошибки — 404 для опубликованной страницы, сломанная ссылка в выгрузке и риск затереть чужое изменение при поспешном откате.
Причина часто лежит в одном legacy-обработчике. Он читает запрос, строит символьный код, обновляет элемент инфоблока и показывает сообщение формы. Успешный ответ формы не доказывает, что изменился правильный элемент. Ответ true от API не доказывает, что после событий в базе лежит ожидаемая строка.
Тезис: первую замену ограничивают одним вызовом и одним полем. До записи подтверждают ID и IBLOCK_ID. После записи снова читают элемент по тому же ID. Откат маршрута и восстановление данных считают разными операциями.
Форма передаёт ID и название. Старый обработчик вызывает legacyUpdateCode(). Функция транслитерирует название и передаёт результат в CIBlockElement::Update. Рядом могут работать импорт, cron-задача и обработчик события. Они используют похожий код, но принимают разные входы.
Если сразу заменить функцию во всех местах, после сбоя неясно, что сработало: неверный ID, другой инфоблок, правило транслитерации или обработчик Bitrix. Сначала нужен контролируемый шов. Он получает выбранный ID и название, знает ожидаемый инфоблок, меняет только CODE и возвращает результат вызывающему коду. HTML, редирект и отправка письма остаются снаружи.
До рефакторинга выпишите контракт старого вызова. Входом будут положительный ID, название для расчёта, ожидаемый ID инфоблока и разрешённый контур. Результатом — исходный код, рассчитанный код и фактический код после чтения. Отказ должен прекращать текущий путь, а не превращаться в сообщение об успехе.
В учебном примере числа 7 и 451 условны. Их нельзя переносить в рабочую систему. Они показывают, что проверка должна называть конкретный инфоблок и выбранный элемент, а не принимать эти значения из формы.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
CODE пуст | Пустое название или пустой результат транслитерации | Проверить строку до Update | Остановить запись |
| Изменился не тот элемент | ID пришёл из формы без проверки | Прочитать ID и IBLOCK_ID по фильтру | Не вызывать writer |
| Код после успеха другой | Обработчик события или параллельная операция изменила поле | Сделать повторную выборку | Выключить новый маршрут |
| Откат затирает новое значение | Исходный снимок устарел | Сравнить текущее поле с результатом опыта | Не восстанавливать автоматически |
Таблица задаёт порядок расследования. Нельзя начинать с восстановления старого значения, пока неизвестно, кто записал текущее. Откат маршрута влияет на следующие вызовы. Откат данных меняет сам элемент и требует проверки снимка.
Ниже ограниченный учебный фрагмент для legacy Bitrix API. Он не является кодом конкретного production-проекта и не утверждает, что параметры подходят вашему каталогу. Метод подключает модуль, выбирает элемент, проверяет инфоблок, записывает только CODE и читает элемент ещё раз.
<?php\nfinal class CheckedCodeWriter\n{\n private $iblockId;\n public function __construct($iblockId)\n {\n $this->iblockId = (int) $iblockId;\n if ($this->iblockId <= 0) throw new InvalidArgumentException('iblock ID must be positive');\n }\n public function writeFromName($elementId, $name)\n {\n if ((int) $elementId <= 0) throw new InvalidArgumentException('element ID must be positive');\n if (!CModule::IncludeModule('iblock')) throw new RuntimeException('module unavailable');\n $before = $this->find((int) $elementId);\n if (!$before || (int) $before['IBLOCK_ID'] !== $this->iblockId) {\n throw new RuntimeException('unexpected element or iblock');\n }\n $code = CUtil::translit(trim((string) $name), 'ru', array(\n 'change_case' => 'L', 'replace_space' => '-',\n 'replace_other' => '-', 'delete_repeat_replace' => true, 'max_len' => 100,\n ));\n if ($code === '') throw new InvalidArgumentException('CODE is empty');\n $element = new CIBlockElement();\n if (!$element->Update((int) $elementId, array('CODE' => $code))) {\n throw new RuntimeException($element->LAST_ERROR ?: 'Update failed');\n }\n $after = $this->find((int) $elementId);\n if (!$after || (string) $after['CODE'] !== $code) {\n throw new RuntimeException('CODE differs after Update');\n }\n return array('before' => $before['CODE'], 'after' => $after['CODE']);\n }\n private function find($elementId)\n {\n $result = CIBlockElement::GetList(array(), array('ID' => $elementId, 'IBLOCK_ID' => $this->iblockId), false, false,\n array('ID', 'IBLOCK_ID', 'CODE'));\n return $result->Fetch();\n }\n}Фрагмент показывает границу, а не готовую библиотеку. Он не проверяет права, уникальность кода, SEO-правила, торговые предложения или кеш. Он также не делает атомарной запись в инфоблок и вызов внешней системы. Эти условия добавляют отдельными проверками.
Проверка булевого ответа полезна, но недостаточна. До обновления могут выполняться обработчики. OnBeforeIBlockElementUpdate может изменить поля или отменить изменение. После записи могут сработать другие действия. Поэтому проверяем не только вызов, но и состояние выбранного элемента.
Повторная выборка не доказывает согласованность всего каталога. Она отвечает на узкий вопрос: у элемента с этим ID находится ожидаемый CODE. Если ответ отрицательный, новый путь не готов. Сначала выключаем его для следующих запросов, затем разбираем вход, события и конкурирующие записи.
Старый обработчик может остаться точкой входа. Переключатель имеет безопасное значение по умолчанию и ограничивает новый маршрут выбранным сценарием:
<?php\ndefined('USE_CHECKED_CODE_WRITER') || define('USE_CHECKED_CODE_WRITER', false);\nfunction updateCodeForScenario($elementId, $name)\n{\n if (USE_CHECKED_CODE_WRITER !== true) return legacyUpdateCode($elementId, $name);\n if ((int) $elementId !== 451) throw new RuntimeException('example ID only');\n return (new CheckedCodeWriter(7))->writeFromName($elementId, $name);\n}В рабочем проекте ограничение задают конфигурацией и правилами доступа, а не параметром URL. Если тестового элемента нет, нельзя включать новый маршрут на рабочей карточке ради быстрой проверки.
ID, IBLOCK_ID и исходный CODE.Неверный ID, чужой инфоблок, пустое название, ошибка Update и несовпадение после чтения должны останавливать текущую операцию. Нельзя показывать сообщение «сохранено», если writer вернул исключение. Нельзя вызывать второй Update в обработчике ошибки без установленной причины.
Откат состоит из двух решений. Сначала вернуть флаг нового маршрута в безопасное состояние. Затем решить, нужно ли восстанавливать поле. Сравните текущее значение с тем, которое должен был поставить опыт. Если поле отличается, его мог изменить редактор или импорт. Автоматическая запись старого снимка тогда опасна.
Точечный writer не превращает старый модуль в новую архитектуру и не заменяет аудит всех источников записи. Один успешный элемент не проверяет дубликаты, разные языки, спецсимволы, права, индексацию и ссылки, построенные по старому коду.
Не передавайте в Update лишние поля ради полноты примера. Чем шире массив изменения, тем больше поверхность побочных эффектов. Если проект передаёт свойства элемента, отдельно изучите их контракт и обработчики.
Подход не решает транзакцию между Bitrix и внешним API. Ошибка внешней отправки не отменяет автоматически уже выполненную запись. Повтор и расхождение нужно описать отдельным процессом.
Замену можно считать готовой для выбранного сценария, если writer принимает проверяемый ID, подтверждает инфоблок и меняет только заявленное поле. Он не пропускает ошибку как успех, читает элемент после Update, получает ожидаемый CODE и отключается одной понятной настройкой. Для расхождения должен существовать запрет на слепой откат.
Это локальный критерий, а не доказательство готовности всей миграции. Для следующего вызова нужны новый снимок, новый ожидаемый результат и отдельная проверка. Если условия нельзя показать на одном разрешённом элементе, расширять замену нельзя.