Files
progcode/editorial/agent-rewrites/327.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
18 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": 327,
"slug": "editorial-2018-12-practice-legacy-refactoring",
"title": "Bitrix: безопасная замена одного Update в legacy-коде",
"excerpt": "Как выделить узкий шов вокруг CIBlockElement::Update, проверить вход, не задеть чужой инфоблок и доказать результат повторной выборкой.",
"contentHtml": "<p>В старом Bitrix-обработчике карточка товара иногда получает пустой символьный код. Страница после сохранения отвечает успешно, но прежний URL перестаёт вести к элементу. Ошибка может проявиться позже: её поймает импорт, кеш или шаблон, который строит ссылку из поля <code>CODE</code>. Цена одной неверной правки — 404, потерянный переход из поиска и сложное расследование, потому что обработчик уже смешивает форму, запись и вывод сообщения.</p>\n<p>Проблему часто пытаются решить заменой всех вызовов на новый класс. Это увеличивает область риска. Надёжнее сначала выделить один переход: вход формы → выбранный элемент → изменение одного поля → повторное чтение. Такой шов не переписывает модуль. Он делает результат конкретного вызова наблюдаемым.</p>\n<h2>Тезис: узкий шов должен доказывать одну запись</h2>\n<p>Шов вокруг <code>CIBlockElement::Update</code> принимает ID элемента, ожидаемый ID инфоблока и новый <code>CODE</code>. До записи он проверяет вход и читает текущий элемент. После записи он снова читает тот же ID и сравнивает фактическое значение с ожидаемым. Если условие не выполнено, путь останавливается с ошибкой.</p>\n<p>Шов не отвечает за HTML, редирект, отправку писем и очистку кеша. Старый обработчик может оставить эти действия рядом. Но он не должен считать их доказательством успешной записи. Сообщение в браузере показывает результат HTTP-сценария, а повторная выборка показывает состояние элемента.</p>\n<figure><img src=\"/assets/editorial/2018/bitrix-legacy-safe-seam-2018.svg\" alt=\"Схема узкого шва Bitrix: форма передаёт ID и CODE в проверяющий writer, writer проверяет инфоблок, вызывает CIBlockElement Update и читает элемент обратно\" /><figcaption>Шов охватывает только переход к полю <code>CODE</code>. Форма и внешние действия остаются за его границей.</figcaption></figure>\n<h2>Механизм: ограничить вход, поле и результат</h2>\n<p>Сначала назовите контракт. ID должен быть положительным целым числом. Новый код после <code>trim()</code> не должен быть пустым. Выборка по ID должна вернуть элемент. Его <code>IBLOCK_ID</code> должен совпасть с ожидаемым. В вызов <code>Update</code> передаём только <code>CODE</code>, а не весь массив формы.</p>\n<p>Проверка инфоблока защищает от особенно неприятной ошибки: правильный ID может относиться к другой сущности. Передача всех полей также опасна. Пустое свойство из формы или устаревшее значение из массива может затереть данные, которых не было в задаче. Чем уже массив изменения, тем короче след операции.</p>\n<table><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>URL стал пустым или изменился неожиданно</td><td>В <code>Update</code> попал пустой или чужой <code>CODE</code></td><td>Сравнить вход, <code>IBLOCK_ID</code> и значение после записи</td><td>Остановить путь и передавать только проверенный <code>CODE</code></td></tr><tr><td>Метод вернул успех, но поле не совпало</td><td>Обработчик события изменил данные после вызова</td><td>Снова выбрать элемент по тому же ID</td><td>Разобрать обработчики до расширения замены</td></tr><tr><td>Ошибка формы появилась после сохранения</td><td>После записи упало письмо, кеш или внешняя интеграция</td><td>Разделить результат записи и результат следующего действия</td><td>Не повторять <code>Update</code>; показать отдельную ошибку</td></tr><tr><td>Проверка работает в одном месте и ломается в другом</td><td>Один legacy-вызов имеет несколько источников входа</td><td>Найти все вызовы и записать их предусловия</td><td>Переключать вызовы по одному</td></tr><tr><td>Откат затирает новое значение</td><td>Старый снимок уже не соответствует текущему состоянию</td><td>Сравнить текущее поле со значением, записанным экспериментом</td><td>Не восстанавливать поле автоматически при расхождении</td></tr></tbody></table>\n<h2>Учебный пример шва</h2>\n<p>Ниже приведён учебный пример для старого Bitrix API и PHP 7.2. Он не утверждает, что такой код уже запускался в production. ID инфоблока, права, обработчики событий и правила уникальности нужно проверить в конкретном проекте.</p>\n<pre><code>&lt;?php\n\nfinal class ProductCodeWriter\n{\n private $expectedIblockId;\n\n public function __construct($expectedIblockId)\n {\n $this-&gt;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 &lt;= 0 || $code === '') {\n throw new InvalidArgumentException('Нужны ID элемента и непустой CODE');\n }\n\n $before = $this-&gt;find($elementId);\n if (!$before || (int) $before['IBLOCK_ID'] !== $this-&gt;expectedIblockId) {\n throw new RuntimeException('Элемент не найден в ожидаемом инфоблоке');\n }\n\n if ((string) $before['CODE'] === $code) {\n return array('changed' =&gt; false, 'code' =&gt; $code);\n }\n\n $element = new CIBlockElement();\n if (!$element-&gt;Update($elementId, array('CODE' =&gt; $code))) {\n throw new RuntimeException($element-&gt;LAST_ERROR ?: 'Update завершился ошибкой');\n }\n\n $after = $this-&gt;find($elementId);\n if (!$after || (string) $after['CODE'] !== $code) {\n throw new RuntimeException('CODE не подтвердился после Update');\n }\n\n return array('changed' =&gt; true, 'code' =&gt; $after['CODE']);\n }\n\n private function find($elementId)\n {\n $result = CIBlockElement::GetList(\n array(),\n array('ID' =&gt; (int) $elementId),\n false,\n false,\n array('ID', 'IBLOCK_ID', 'CODE')\n );\n\n return $result-&gt;Fetch();\n }\n}</code></pre>\n<p>Важны четыре границы. Класс не читает глобальный <code>$_POST</code>. Он не печатает сообщение. Он не принимает решение за внешний сервис. Он не возвращает <code>true</code> только потому, что метод API не сообщил об ошибке. Повторная выборка делает условие готовности явным.</p>\n<p>Если код уже совпадает, метод возвращает <code>changed: false</code>. Это нормальный результат идемпотентного повторного вызова. Не нужно писать в базу второй раз ради сообщения «сохранено». Если <code>Update</code> вернул <code>false</code>, исключение содержит <code>LAST_ERROR</code>, когда Bitrix его заполнил. Продолжать к письму или редиректу после такого отказа нельзя.</p>\n<h2>Подключение из старого обработчика</h2>\n<p>Старый файл может сохранить свою проверку запроса и выбор шаблона. Меняется только прямой вызов API. Класс возвращает отчёт, а обработчик решает, как показать его пользователю.</p>\n<pre><code>try {\n $writer = new ProductCodeWriter(7);\n $report = $writer-&gt;write($_POST['ID'], $_POST['CODE']);\n\n $message = $report['changed']\n ? 'Символьный код обновлён.'\n : 'Символьный код уже совпадает.';\n} catch (InvalidArgumentException $error) {\n $message = $error-&gt;getMessage();\n} catch (RuntimeException $error) {\n $message = $error-&gt;getMessage();\n}</code></pre>\n<p>Число <code>7</code> — пример, а не значение для копирования. В рабочем проекте его берут из конфигурации сценария и проверяют по данным элемента. Если обработчик после этой конструкции отправляет письмо, ошибка письма не означает, что <code>CODE</code> не записался. Логи и сообщение должны различать две операции.</p>\n<h2>Порядок проверки</h2>\n<ol><li>Найдите прямой вызов <code>CIBlockElement::Update</code> и запишите его источники входа: форма, импорт, cron или событие.</li><li>Выберите один элемент на разрешённом тестовом контуре. Не используйте рабочую карточку только ради быстрого эксперимента.</li><li>Сохраните ID элемента, ID инфоблока, старый <code>CODE</code> и ожидаемый новый код.</li><li>Проверьте отрицательные входы: нулевой ID, пустой код, неизвестный элемент и элемент из другого инфоблока.</li><li>Вызовите шов для одного элемента и отдельно зафиксируйте его отчёт.</li><li>Снова выберите элемент через API и сравните фактический <code>CODE</code> с ожидаемым.</li><li>Проверьте путь после записи отдельно: письмо, кеш, редирект или интеграция не должны маскировать результат шва.</li><li>Если значение расходится, отключите новый маршрут для следующих вызовов и разберите обработчики событий. Не добавляйте второй <code>Update</code> «на всякий случай».</li><li>Только после этого подключайте следующий источник входа и повторяйте проверку с его собственными условиями.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Шов не делает <code>CODE</code> уникальным. Если два элемента получают один код, правило нужно проверять отдельным запросом и учитывать политику проекта. Шов также не решает права доступа, SEO-правила, синхронизацию торговых предложений и очистку кеша. Эти обязанности нельзя приписывать одной функции записи.</p>\n<p>У Bitrix есть обработчики до и после изменения. Обработчик до записи может изменить поле или отменить операцию. Обработчик после записи может запустить другой эффект. Поэтому успешный возврат <code>Update</code> и подтверждённый <code>CODE</code> доказывают только состояние выбранного элемента в момент повторного чтения. Они не доказывают согласованность поиска и внешнего каталога.</p>\n<p>Откат маршрута и откат данных — разные действия. Возврат к старому классу защищает следующие вызовы, но не возвращает уже изменённое поле. Восстанавливать старый <code>CODE</code> можно только после проверки, что текущим значением остаётся результат этого эксперимента. Если его изменил редактор или импорт, остановитесь и согласуйте восстановление. Молчаливый откат может затереть более новое изменение.</p>\n<p>Не каждый вызов требует класса. Если функция уже получает явные аргументы, меняет одно поле и возвращает результат, дополнительная оболочка не даст пользы. Выделяйте шов там, где операция смешана с формой, повторяется или нуждается в отдельной проверке. Цель — не увеличить число файлов, а сделать границу проверяемой.</p>\n<h2>Критерий готовности</h2>\n<p>Локальная замена готова, если для выбранного вызова выполнены все условия: вход явно ограничен; проверен ожидаемый инфоблок; в <code>Update</code> передано только нужное поле; отрицательные пути останавливают сценарий; результат повторно прочитан по тому же ID; обработчики и действия после записи не выданы за доказательство успешного сохранения. После расхождения существует понятный путь отключения нового маршрута, а восстановление данных не выполняется поверх чужого изменения.</p>\n<p>Это не сертификат безопасности всего legacy-модуля. Это проверяемое утверждение об одной операции. Когда оно подтверждено, следующий участок можно разбирать отдельно.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://dev.1c-bitrix.ru/api_help/main/reference/cmodule/includemodule.php\" target=\"_blank\" rel=\"noopener noreferrer\">1С-Битрикс: CModule::IncludeModule</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://www.php.net/exceptions\" target=\"_blank\" rel=\"noopener noreferrer\">PHP Manual: Exceptions</a></li></ul>"
}