8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"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><?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}</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->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}</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>"
|
||
}
|