{ "index": 327, "slug": "editorial-2018-12-practice-legacy-refactoring", "title": "Bitrix: как безопасно заменить один CIBlockElement::Update", "excerpt": "Как выделить узкий шов вокруг CIBlockElement::Update, проверить вход, не задеть чужой инфоблок и подтвердить результат повторной выборкой.", "contentHtml": "
В старом Bitrix-обработчике карточка товара иногда получает пустой символьный код. Страница после сохранения отвечает успешно, но прежний URL перестаёт вести к элементу. Ошибка может проявиться позже: её поймает импорт, кеш или шаблон, который строит ссылку из поля CODE. Цена одной неверной правки — 404, потерянный переход из поиска и сложное расследование, потому что обработчик уже смешивает форму, запись и вывод сообщения.
Проблему часто пытаются решить заменой всех вызовов на новый класс. Это увеличивает область риска. Надёжнее сначала выделить один переход: вход формы → выбранный элемент → изменение одного поля → повторное чтение. Такой шов не переписывает модуль. Он делает результат конкретного вызова наблюдаемым.
\nШов вокруг CIBlockElement::Update принимает ID элемента, ожидаемый ID инфоблока и новый CODE. До записи он проверяет вход и читает текущий элемент. После записи он снова читает тот же ID и сравнивает фактическое значение с ожидаемым. Если условие не выполнено, путь останавливается с ошибкой.
Шов не отвечает за HTML, редирект, отправку писем и очистку кеша. Старый обработчик может оставить эти действия рядом. Но он не должен считать их доказательством успешной записи. Сообщение в браузере показывает результат HTTP-сценария, а повторная выборка показывает состояние элемента.
\nCODE. Форма и внешние действия остаются за его границей.Сначала назовите контракт. ID должен быть положительным целым числом. Новый код после trim() не должен быть пустым. Выборка по ID должна вернуть элемент. Его IBLOCK_ID должен совпасть с ожидаемым. В вызов Update передаём только CODE, а не весь массив формы.
Проверка инфоблока защищает от особенно неприятной ошибки: правильный ID может относиться к другой сущности. Передача всех полей также опасна. Пустое свойство из формы или устаревшее значение из массива может затереть данные, которых не было в задаче. Чем уже массив изменения, тем короче след операции.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| URL стал пустым или изменился неожиданно | В Update попал пустой или чужой CODE | Сравнить вход, IBLOCK_ID и значение после записи | Остановить путь и передавать только проверенный CODE |
| Метод вернул успех, но поле не совпало | Обработчик события изменил данные после вызова | Снова выбрать элемент по тому же ID | Разобрать обработчики до расширения замены |
| Ошибка формы появилась после сохранения | После записи упало письмо, кеш или внешняя интеграция | Разделить результат записи и результат следующего действия | Не повторять Update; показать отдельную ошибку |
| Проверка работает в одном месте и ломается в другом | Один legacy-вызов имеет несколько источников входа | Найти все вызовы и записать их предусловия | Переключать вызовы по одному |
| Откат затирает новое значение | Старый снимок уже не соответствует текущему состоянию | Сравнить текущее поле со значением, записанным экспериментом | Не восстанавливать поле автоматически при расхождении |
Ниже приведён учебный пример для старого 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 не сообщил об ошибке. Повторная выборка делает условие готовности явным.
Если код уже совпадает, метод возвращает changed: false. Это нормальный результат идемпотентного повторного вызова. Не нужно писать в базу второй раз ради сообщения «сохранено». Если Update вернул false, исключение содержит LAST_ERROR, когда Bitrix его заполнил. Продолжать к письму или редиректу после такого отказа нельзя.
Старый файл может сохранить свою проверку запроса и выбор шаблона. Меняется только прямой вызов API. Класс возвращает отчёт, а обработчик решает, как показать его пользователю.
\ntry {\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 не записался. Логи и сообщение должны различать две операции.
CIBlockElement::Update и запишите его источники входа: форма, импорт, cron или событие.CODE и ожидаемый новый код.CODE с ожидаемым.Update «на всякий случай».Шов не делает CODE уникальным. Если два элемента получают один код, правило нужно проверять отдельным запросом и учитывать политику проекта. Шов также не решает права доступа, SEO-правила, синхронизацию торговых предложений и очистку кеша. Эти обязанности нельзя приписывать одной функции записи.
У Bitrix есть обработчики до и после изменения. Обработчик до записи может изменить поле или отменить операцию. Обработчик после записи может запустить другой эффект. Поэтому успешный возврат Update и подтверждённый CODE доказывают только состояние выбранного элемента в момент повторного чтения. Они не доказывают согласованность поиска и внешнего каталога.
Откат маршрута и откат данных — разные действия. Возврат к старому классу защищает следующие вызовы, но не возвращает уже изменённое поле. Восстанавливать старый CODE можно только после проверки, что текущим значением остаётся результат этого эксперимента. Если его изменил редактор или импорт, остановитесь и согласуйте восстановление. Молчаливый откат может затереть более новое изменение.
Не каждый вызов требует класса. Если функция уже получает явные аргументы, меняет одно поле и возвращает результат, дополнительная оболочка не даст пользы. Выделяйте шов там, где операция смешана с формой, повторяется или нуждается в отдельной проверке. Цель — не увеличить число файлов, а сделать границу проверяемой.
\nЛокальная замена готова, если для выбранного вызова выполнены все условия: вход явно ограничен; проверен ожидаемый инфоблок; в Update передано только нужное поле; отрицательные пути останавливают сценарий; результат повторно прочитан по тому же ID; обработчики и действия после записи не выданы за доказательство успешного сохранения. После расхождения существует понятный путь отключения нового маршрута, а восстановление данных не выполняется поверх чужого изменения.
Это не сертификат безопасности всего legacy-модуля. Это проверяемое утверждение об одной операции. Когда оно подтверждено, следующий участок можно разбирать отдельно.
\n