DarkRiDDeR14 мин

Bitrix. Замена старого обработчика: один элемент, проверка и откат

BitrixPHP

Нужно заменить старый участок, который формирует CODE, но его вызовы разбросаны между импортом и формой редактирования. Цена ошибки — не «некрасивый рефакторинг»: один новый код может сломать URL элемента, а поспешный откат поверх неизвестного значения способен затереть изменение другого человека.

Это учебный полевой разбор одного вопроса: как заменить один legacy-обработчик Bitrix на проверяемый путь и оставить возможность отката? Он не описывает реальный проект, запуск или результат релиза. Возьмём один элемент на разрешённом тестовом контуре, снимем его исходное значение, направим только этот вызов через новый код и после записи снова прочитаем элемент. Так можно увидеть расхождение до того, как расширять замену на импорт.

Выбираем один участок, а не «весь импорт»

Представим старую функцию legacyUpdateCode(). Она получает название, сама делает транслитерацию и сразу вызывает CIBlockElement::Update. Задача не в том, чтобы объявить её плохой. Она уже может обслуживать десятки строк импорта. В первом проходе меняем только один контролируемый вызов: выбранный ID, известный инфоблок и понятное ожидаемое значение. Остальные обращения продолжают пользоваться старой функцией, пока для них не записаны такие же условия.

Полезно заранее выписать карту вызова на бумаге или в задаче: форма редактирования, import-скрипт, cron, обработчик события. Мы не утверждаем, что нашли эти места в конкретном репозитории — их надо искать в своём проекте. Карта нужна, чтобы не перепутать локальный опыт с глобальной заменой. Если один вызов проходит новый путь, это не разрешение переключить остальные молча.

Схема безопасной замены legacy-участка: снимок ID, IBLOCK_ID и старого CODE; выбор старого или нового CodeWriter для одного вызова; повторная выборка; при расхождении возврат маршрута и сохранённое исходное значение для согласованного отката.
Откат на схеме сначала выключает новый маршрут. Восстановление поля выполняют только по сохранённому снимку и после проверки, что его не менял другой процесс.

Снимок до изменения — это материал для проверки, а не журнал на словах

До вызова сохраняем минимум: ID элемента, ID инфоблока, прежний CODE, новый расчётный код и время проверки. Эти значения можно положить в тестовый сценарий, временный защищённый журнал или запись задачи — способ зависит от правил проекта. Не стоит печатать в общий лог весь массив элемента: в нём могут оказаться поля, которые не нужны для данной операции. Нам достаточно того, что позволит сравнить один переход.

ШагЧто фиксируемЧто считаем успехомЧто делаем при расхождении
До переключенияID, IBLOCK_ID, старый CODEЭлемент существует и принадлежит ожидаемому инфоблокуНе запускать новый путь для этого ID
РасчётНазвание и параметры транслитерацииНовый код не пустой и понятен человекуОстановить сценарий, не писать заглушку
ЗаписьВыбранный writer и ответ UpdateAPI сообщил успех без скрытой повторной записиВернуть управление старому маршруту для следующих попыток
Повторная выборкаФактический CODE по тому же IDСовпадает с расчётным значениемСначала отключить новый маршрут, затем расследовать события и вход
Откат поляСохранённый старый код и текущий кодТекущий код всё ещё тот, который поставил опытНе перезаписывать элемент; согласовать восстановление вручную

Последняя строка важнее всего. Откат маршрута и откат данных — разные действия. Переключатель может вернуть последующие вызовы на старую функцию. Но если новый путь уже изменил поле, автоматическое восстановление безопасно только при проверке, что между снимком и откатом значение не менял импорт, редактор или обработчик. Если такой гарантии нет, честнее остановиться и сравнить состояние, чем вернуть «старый» код поверх чужой работы.

Новый writer отвечает только за расчёт и одно поле

В примере ниже старый и новый writers существуют рядом. Это не постоянная архитектура и не рекомендация держать две реализации вечно. Две функции нужны на время локальной проверки, чтобы маршрут можно было вернуть без массового удаления кода. Новый вариант перед записью проверяет инфоблок, формирует символьный код стандартной функцией Bitrix и передаёт в Update только поле CODE.

<?php
// CodeWriter.php — учебный пример для PHP 7.2 и legacy Bitrix API
final class CheckedCodeWriter
{
    private $iblockId;

    public function __construct($iblockId)
    {
        $this->iblockId = (int) $iblockId;
    }

    public function writeFromName($elementId, $name)
    {
        if (!CModule::IncludeModule('iblock')) {
            throw new RuntimeException('Module iblock is unavailable');
        }

        $before = $this->find($elementId);
        if (!$before || (int) $before['IBLOCK_ID'] !== $this->iblockId) {
            throw new RuntimeException('Unexpected element or iblock');
        }

        $code = CUtil::translit(trim((string) $name), 'ru', array(
            'change_case' => 'L',
            'replace_space' => '-',
            'replace_other' => '-',
            'delete_repeat_replace' => true,
            'max_len' => 100,
        ));

        if ($code === '') {
            throw new InvalidArgumentException('CODE is empty after transliteration');
        }

        $element = new CIBlockElement();
        if (!$element->Update((int) $elementId, array('CODE' => $code))) {
            throw new RuntimeException($element->LAST_ERROR ?: 'Element update failed');
        }

        return array('before' => $before['CODE'], 'after' => $code);
    }

    private function find($elementId)
    {
        $result = CIBlockElement::GetList(
            array(),
            array('ID' => (int) $elementId),
            false,
            false,
            array('ID', 'IBLOCK_ID', 'CODE')
        );

        return $result->Fetch();
    }
}

Параметры CUtil::translit здесь выбраны для примера: нижний регистр, дефисы вместо пробелов и ограничение длины. Они не доказывают, что именно такой код нужен вашему каталогу. Например, правила SEO могут требовать другой язык, сохранение старых URL или дополнительную проверку уникальности. До подключения нового writer эти условия следует назвать отдельно. Нельзя делать вывод о совпадении поведения только по тому, что обе функции вернули непустую строку.

Маршрут выбираем явно и на короткое время

Для контролируемого опыта достаточно простого переключателя в локальной конфигурации. Он не должен быть скрыт в шаблоне или зависеть от случайного параметра URL. В примере константу задаёт окружение, которое уже контролирует проект. По умолчанию остаётся старый путь. Новый маршрут включают только для заранее выбранной проверки, а не для всех вызовов импорта.

<?php
// config.php: по умолчанию старый маршрут
defined('USE_CHECKED_CODE_WRITER') || define('USE_CHECKED_CODE_WRITER', false);

function updateCodeForOneScenario($elementId, $name)
{
    if (USE_CHECKED_CODE_WRITER !== true) {
        // Существующая функция остаётся точкой возврата.
        return legacyUpdateCode($elementId, $name);
    }

    $writer = new CheckedCodeWriter(7);
    return $writer->writeFromName($elementId, $name);
}

// Перед опытом сравниваем ID с заранее выбранным значением.
if ((int) $elementId !== 451) {
    throw new RuntimeException('The checked route is not enabled for this element');
}

Числа 7 и 451 в примере не являются настройкой для копирования. Они показывают, что контур должен назвать свой инфоблок и тестовый элемент явно. В рабочем коде значения берут из согласованной конфигурации, а не из формы. Если такого контура нет, не следует подменять его production-карточкой. Сначала подготовьте разрешённый элемент и способ увидеть его до и после вызова.

Проверяем новую ветку и готовим откат до запуска

В Bitrix Update вызывает события, поэтому сравнение не заканчивается на его булевом результате. После вызова снова выбираем элемент и проверяем CODE. Если новое значение не совпало с расчётным, первым действием будет выключить новый маршрут для следующих запросов. Затем смотрим вход, обработчики и фактическое значение. Откат поля не следует запускать автоматически из catch: в нём недостаточно информации о чужих изменениях.

  1. Составить карту старых вызовов и выбрать один разрешённый сценарий, не заявляя, что карта уже полна.
  2. Снять перед опытом ID, инфоблок и прежний CODE; отдельно записать ожидаемую строку после транслитерации.
  3. Оставить переключатель нового writer выключенным по умолчанию и подготовить понятный способ вернуть его в false.
  4. Включить новый путь только для выбранного ID на тестовом контуре.
  5. После вызова прочитать элемент заново через API и сравнить фактическое поле с ожидаемым.
  6. При расхождении сразу отключить новый маршрут. Восстанавливать старый CODE можно только после проверки, что текущая строка принадлежит этому опыту.
  7. Сохранить итог проверки рядом с задачей и лишь затем решать, нужен ли второй сценарий или доработка правил транслитерации.

Чего не доказывает один удачный элемент

Один элемент не проверяет все алфавиты, дубликаты, права редакторов, торговые предложения, SEO-шаблоны и работу импорта по расписанию. Он также не показывает, что в проекте нет обработчика, меняющего CODE после нашего вызова. Это не недостаток маленького опыта, если он честно ограничен. Для следующего сценария понадобятся новые входы, ожидаемый результат и такой же снимок до изменения.

Не нужно изображать такую замену как «бесшовную миграцию». Две реализации на короткое время добавляют стоимость: их нужно держать рядом, понимать разницу и потом удалить старый путь отдельной задачей. Но эта стоимость видна. В отличие от массовой подмены, она оставляет точку возврата и позволяет остановиться после первого расхождения, а не искать причину среди сотен уже изменённых карточек.

Итог: откат начинается до записи

Локальная замена становится безопаснее, когда до вызова известны исходное значение, выбранный маршрут и критерий сравнения после записи. Сначала выключаем новый путь для следующих запросов, затем решаем вопрос данных по сохранённому снимку. Такой порядок не делает legacy-код современным сам по себе. Он даёт следующей правке то, чего обычно не хватает в старом обработчике: один контролируемый элемент, наблюдаемый результат и честный путь назад.

Проверяемые источники