Files
2026-09-04 00:11:39 +03:00

8 lines
18 KiB
JSON
Raw Permalink 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": 325,
"slug": "editorial-2018-12-field-legacy-refactoring",
"title": "Bitrix: как заменить генерацию CODE в legacy-коде и не потерять исходное значение",
"excerpt": "Точечная замена обработчика Bitrix начинается с одного элемента: фиксируем исходный CODE, проверяем инфоблок, записываем только нужное поле и читаем результат обратно. Отдельно разбираем отрицательный путь и осторожный откат.",
"contentHtml": "<p>После изменения формы карточка товара открывается по старому адресу, а новая запись получает другой URL. Иногда поле <code>CODE</code> остаётся пустым. Иногда его меняет обработчик, о котором забыли. Цена ошибки — 404 для опубликованной страницы, сломанная ссылка в выгрузке и риск затереть чужое изменение при поспешном откате.</p><p>Причина часто лежит в одном legacy-обработчике. Он читает запрос, строит символьный код, обновляет элемент инфоблока и показывает сообщение формы. Успешный ответ формы не доказывает, что изменился правильный элемент. Ответ <code>true</code> от API не доказывает, что после событий в базе лежит ожидаемая строка.</p><p><strong>Тезис:</strong> первую замену ограничивают одним вызовом и одним полем. До записи подтверждают ID и <code>IBLOCK_ID</code>. После записи снова читают элемент по тому же ID. Откат маршрута и восстановление данных считают разными операциями.</p><h2>Где возникает расхождение</h2><p>Форма передаёт ID и название. Старый обработчик вызывает <code>legacyUpdateCode()</code>. Функция транслитерирует название и передаёт результат в <code>CIBlockElement::Update</code>. Рядом могут работать импорт, cron-задача и обработчик события. Они используют похожий код, но принимают разные входы.</p><p>Если сразу заменить функцию во всех местах, после сбоя неясно, что сработало: неверный ID, другой инфоблок, правило транслитерации или обработчик Bitrix. Сначала нужен контролируемый шов. Он получает выбранный ID и название, знает ожидаемый инфоблок, меняет только <code>CODE</code> и возвращает результат вызывающему коду. HTML, редирект и отправка письма остаются снаружи.</p><figure><img src=\"/assets/editorial/2018/bitrix-legacy-replacement-rollback-2018.svg\" alt=\"Схема точечной замены генерации CODE в Bitrix с проверкой элемента, повторной выборкой и раздельным откатом маршрута и данных\" /><figcaption>Сначала проверяем один элемент и сохраняем его исходный CODE. При расхождении выключаем новый маршрут. Старое значение восстанавливаем только после проверки, что его не изменил другой процесс.</figcaption></figure><h2>Контракт операции</h2><p>До рефакторинга выпишите контракт старого вызова. Входом будут положительный ID, название для расчёта, ожидаемый ID инфоблока и разрешённый контур. Результатом — исходный код, рассчитанный код и фактический код после чтения. Отказ должен прекращать текущий путь, а не превращаться в сообщение об успехе.</p><p>В учебном примере числа <code>7</code> и <code>451</code> условны. Их нельзя переносить в рабочую систему. Они показывают, что проверка должна называть конкретный инфоблок и выбранный элемент, а не принимать эти значения из формы.</p><table><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td><code>CODE</code> пуст</td><td>Пустое название или пустой результат транслитерации</td><td>Проверить строку до <code>Update</code></td><td>Остановить запись</td></tr><tr><td>Изменился не тот элемент</td><td>ID пришёл из формы без проверки</td><td>Прочитать <code>ID</code> и <code>IBLOCK_ID</code> по фильтру</td><td>Не вызывать writer</td></tr><tr><td>Код после успеха другой</td><td>Обработчик события или параллельная операция изменила поле</td><td>Сделать повторную выборку</td><td>Выключить новый маршрут</td></tr><tr><td>Откат затирает новое значение</td><td>Исходный снимок устарел</td><td>Сравнить текущее поле с результатом опыта</td><td>Не восстанавливать автоматически</td></tr></tbody></table><p>Таблица задаёт порядок расследования. Нельзя начинать с восстановления старого значения, пока неизвестно, кто записал текущее. Откат маршрута влияет на следующие вызовы. Откат данных меняет сам элемент и требует проверки снимка.</p><h2>Выделяем writer с одной ответственностью</h2><p>Ниже ограниченный учебный фрагмент для legacy Bitrix API. Он не является кодом конкретного production-проекта и не утверждает, что параметры подходят вашему каталогу. Метод подключает модуль, выбирает элемент, проверяет инфоблок, записывает только <code>CODE</code> и читает элемент ещё раз.</p><pre><code>&lt;?php\nfinal class CheckedCodeWriter\n{\n private $iblockId;\n public function __construct($iblockId)\n {\n $this-&gt;iblockId = (int) $iblockId;\n if ($this-&gt;iblockId &lt;= 0) throw new InvalidArgumentException(&#039;iblock ID must be positive&#039;);\n }\n public function writeFromName($elementId, $name)\n {\n if ((int) $elementId &lt;= 0) throw new InvalidArgumentException(&#039;element ID must be positive&#039;);\n if (!CModule::IncludeModule(&#039;iblock&#039;)) throw new RuntimeException(&#039;module unavailable&#039;);\n $before = $this-&gt;find((int) $elementId);\n if (!$before || (int) $before[&#039;IBLOCK_ID&#039;] !== $this-&gt;iblockId) {\n throw new RuntimeException(&#039;unexpected element or iblock&#039;);\n }\n $code = CUtil::translit(trim((string) $name), &#039;ru&#039;, array(\n &#039;change_case&#039; =&gt; &#039;L&#039;, &#039;replace_space&#039; =&gt; &#039;-&#039;,\n &#039;replace_other&#039; =&gt; &#039;-&#039;, &#039;delete_repeat_replace&#039; =&gt; true, &#039;max_len&#039; =&gt; 100,\n ));\n if ($code === &#039;&#039;) throw new InvalidArgumentException(&#039;CODE is empty&#039;);\n $element = new CIBlockElement();\n if (!$element-&gt;Update((int) $elementId, array(&#039;CODE&#039; =&gt; $code))) {\n throw new RuntimeException($element-&gt;LAST_ERROR ?: &#039;Update failed&#039;);\n }\n $after = $this-&gt;find((int) $elementId);\n if (!$after || (string) $after[&#039;CODE&#039;] !== $code) {\n throw new RuntimeException(&#039;CODE differs after Update&#039;);\n }\n return array(&#039;before&#039; =&gt; $before[&#039;CODE&#039;], &#039;after&#039; =&gt; $after[&#039;CODE&#039;]);\n }\n private function find($elementId)\n {\n $result = CIBlockElement::GetList(array(), array(&#039;ID&#039; =&gt; $elementId, &#039;IBLOCK_ID&#039; =&gt; $this-&gt;iblockId), false, false,\n array(&#039;ID&#039;, &#039;IBLOCK_ID&#039;, &#039;CODE&#039;));\n return $result-&gt;Fetch();\n }\n}</code></pre><p>Фрагмент показывает границу, а не готовую библиотеку. Он не проверяет права, уникальность кода, SEO-правила, торговые предложения или кеш. Он также не делает атомарной запись в инфоблок и вызов внешней системы. Эти условия добавляют отдельными проверками.</p><h2>Почему нужно читать элемент после Update</h2><p>Проверка булевого ответа полезна, но недостаточна. До обновления могут выполняться обработчики. <code>OnBeforeIBlockElementUpdate</code> может изменить поля или отменить изменение. После записи могут сработать другие действия. Поэтому проверяем не только вызов, но и состояние выбранного элемента.</p><p>Повторная выборка не доказывает согласованность всего каталога. Она отвечает на узкий вопрос: у элемента с этим ID находится ожидаемый <code>CODE</code>. Если ответ отрицательный, новый путь не готов. Сначала выключаем его для следующих запросов, затем разбираем вход, события и конкурирующие записи.</p><h2>Подключаем новый путь без массовой замены</h2><p>Старый обработчик может остаться точкой входа. Переключатель имеет безопасное значение по умолчанию и ограничивает новый маршрут выбранным сценарием:</p><pre><code>&lt;?php\ndefined(&#039;USE_CHECKED_CODE_WRITER&#039;) || define(&#039;USE_CHECKED_CODE_WRITER&#039;, 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(&#039;example ID only&#039;);\n return (new CheckedCodeWriter(7))-&gt;writeFromName($elementId, $name);\n}</code></pre><p>В рабочем проекте ограничение задают конфигурацией и правилами доступа, а не параметром URL. Если тестового элемента нет, нельзя включать новый маршрут на рабочей карточке ради быстрой проверки.</p><h2>Порядок точечной замены</h2><ol><li>Найти известные вызовы старого обработчика: форму, импорт, cron и события. Не считать список полным без поиска по проекту.</li><li>Выбрать разрешённый тестовый элемент и записать его <code>ID</code>, <code>IBLOCK_ID</code> и исходный <code>CODE</code>.</li><li>Зафиксировать правила расчёта и проверить, что результат не пустой.</li><li>Вынести проверку элемента, запись одного поля и повторную выборку в writer.</li><li>Оставить старый маршрут включённым по умолчанию.</li><li>Выполнить операцию на тестовом контуре и сравнить фактический код с ожидаемым.</li><li>При расхождении выключить новый маршрут и проверить события до восстановления данных.</li><li>Расширять охват по одному вызову, сохраняя для каждого свои входы и критерии.</li></ol><h2>Отрицательный путь и откат</h2><p>Неверный ID, чужой инфоблок, пустое название, ошибка <code>Update</code> и несовпадение после чтения должны останавливать текущую операцию. Нельзя показывать сообщение «сохранено», если writer вернул исключение. Нельзя вызывать второй <code>Update</code> в обработчике ошибки без установленной причины.</p><p>Откат состоит из двух решений. Сначала вернуть флаг нового маршрута в безопасное состояние. Затем решить, нужно ли восстанавливать поле. Сравните текущее значение с тем, которое должен был поставить опыт. Если поле отличается, его мог изменить редактор или импорт. Автоматическая запись старого снимка тогда опасна.</p><h2>Ограничения метода</h2><p>Точечный writer не превращает старый модуль в новую архитектуру и не заменяет аудит всех источников записи. Один успешный элемент не проверяет дубликаты, разные языки, спецсимволы, права, индексацию и ссылки, построенные по старому коду.</p><p>Не передавайте в <code>Update</code> лишние поля ради полноты примера. Чем шире массив изменения, тем больше поверхность побочных эффектов. Если проект передаёт свойства элемента, отдельно изучите их контракт и обработчики.</p><p>Подход не решает транзакцию между Bitrix и внешним API. Ошибка внешней отправки не отменяет автоматически уже выполненную запись. Повтор и расхождение нужно описать отдельным процессом.</p><h2>Критерий готовности</h2><p>Замену можно считать готовой для выбранного сценария, если writer принимает проверяемый ID, подтверждает инфоблок и меняет только заявленное поле. Он не пропускает ошибку как успех, читает элемент после <code>Update</code>, получает ожидаемый <code>CODE</code> и отключается одной понятной настройкой. Для расхождения должен существовать запрет на слепой откат.</p><p>Это локальный критерий, а не доказательство готовности всей миграции. Для следующего вызова нужны новый снимок, новый ожидаемый результат и отдельная проверка. Если условия нельзя показать на одном разрешённом элементе, расширять замену нельзя.</p><h2>Проверяемые источники</h2><ul><li><a href=\"https://dev.1c-bitrix.ru/api_help/iblock/classes/ciblockelement/getlist.php\" target=\"_blank\" rel=\"noopener noreferrer\">1С-Битрикс: CIBlockElement::GetList</a></li><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/main/reference/cutil/translit.php\" target=\"_blank\" rel=\"noopener noreferrer\">1С-Битрикс: CUtil::translit</a></li></ul>"
}