После точечной правки карточки товар иногда исчезает по привычному URL: поле CODE пришло пустым или ушло не в тот инфоблок. Цена ошибки — не только 404 для посетителя. Следующая выгрузка или шаблон может прочитать уже другой адрес, и разбирать придётся не одну строку в обработчике, а весь след изменения.
Разберём один вопрос: как вынести маленький защищённый шов вокруг CIBlockElement::Update, когда нужно менять только символьный код элемента? Это учебный пример для старого Bitrix и PHP 7.2. Он не предполагает, что у нас есть доступ к вашему модулю, тестовой базе или журналу изменений. Сначала ограничиваем вход, затем меняем одно поле и читаем его обратно.
Симптом показывает место, а не весь объём переделки
В legacy-файле изменение часто прячется между разбором $_POST, подключением шаблона, проверкой прав и отправкой письма. В таком месте легко решить, что нужен новый модуль каталога. Пока доказан только другой факт: один переход вход формы → CODE элемента нельзя безопасно увидеть и повторить. Значит, первым делом отделяем этот переход, а не переносим все соседние строки.
Шов — это небольшая функция или класс, у которого видны вход, ожидаемое изменение и ошибка. Он не обязан знать, как отрисовывается форма и кому потом отправляется уведомление. Для примера шов получает ID элемента, новый код и ожидаемый ID инфоблока. На выходе он возвращает короткий отчёт: поле уже имело нужное значение либо было обновлено. Остальные части старого файла пока остаются на месте.
До кода записываем, что вправе изменить
Узкий контракт полезнее списка пожеланий. Входом считаем положительный ID, непустой символьный код и заранее известный инфоблок. Изменением считаем только поле CODE. До вызова API проверяем, что элемент существует и относится к этому инфоблоку. После вызова читаем тот же элемент и сравниваем фактическое значение. Если код уже совпал, писать в БД второй раз не нужно: это отдельный наблюдаемый результат, а не ошибка.
| Часть шва | Проверка до записи | Действие | Признак готовности |
|---|---|---|---|
| Модуль | CModule::IncludeModule("iblock") вернул true | Разрешить работу с API инфоблоков | Нет скрытого подключения класса из другого файла |
| Элемент | Выборка по ID вернула строку | Сравнить IBLOCK_ID и текущий CODE | Не меняем чужой инфоблок и не создаём элемент по ошибке |
| Вход | ID положительный, код после trim() не пустой | Передать ровно одно поле в Update | Форма не превращает пустое значение в случайное обновление |
| Запись | Update() вернул true | Прочитать элемент той же выборкой | Возвращённый CODE совпадает с ожидаемым |
| Отказ | API вернул false или проверка не прошла | Остановить текущий путь с понятным сообщением | Нет продолжения к шаблону как после успешной записи |
Такой контракт не гарантирует уникальность кода на любом сайте. В одном проекте код формирует компонент, в другом — импорт, в третьем на него влияют обработчики события. Задача этого шага скромнее: не дать конкретному вызову Update изменить неизвестный элемент или умолчать о неуспехе. Правило уникальности, транслитерация и маршруты каталога добавляются отдельными проверками, если они действительно принадлежат вашему случаю.
Выделяем CodeWriter, а не новый «слой приложения»
Ниже обычный PHP-класс. Он использует старый API Bitrix потому, что именно он уже находится в рассматриваемом коде. Перед работой он подключает модуль iblock, читает элемент через CIBlockElement::GetList и передаёт в Update только CODE. В примере нет автозагрузчика, контейнера зависимостей или обещания миграции на другой API: эти вещи не нужны, чтобы сделать один вызов понятнее.
<?php
// local/php_interface/lib/ProductCodeWriter.php — PHP 7.2
final class ProductCodeWriter
{
private $expectedIblockId;
public function __construct($expectedIblockId)
{
$this->expectedIblockId = (int) $expectedIblockId;
}
public function write($elementId, $code)
{
if (!CModule::IncludeModule('iblock')) {
throw new RuntimeException('The iblock module is not available');
}
$elementId = (int) $elementId;
$code = trim((string) $code);
if ($elementId <= 0 || $code === '') {
throw new InvalidArgumentException('Element ID and CODE are required');
}
$before = $this->find($elementId);
if (!$before || (int) $before['IBLOCK_ID'] !== $this->expectedIblockId) {
throw new RuntimeException('Unexpected element or iblock');
}
if ((string) $before['CODE'] === $code) {
return array('changed' => false, 'code' => $code);
}
$element = new CIBlockElement();
if (!$element->Update($elementId, array('CODE' => $code))) {
throw new RuntimeException($element->LAST_ERROR ?: 'Bitrix Update failed');
}
$after = $this->find($elementId);
if (!$after || (string) $after['CODE'] !== $code) {
throw new RuntimeException('CODE was not read back after Update');
}
return array('changed' => true, '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();
}
}
Код не заменяет права доступа и не делает код уникальным сам по себе. Он также не оборачивает обработчики Bitrix в транзакцию: документация CIBlockElement::Update указывает, что до и после записи работают события. Поэтому ответ true нужен, но сам по себе не равен доказательству, что шаблон, индекс или внешняя система уже увидели нужный адрес. Здесь мы доказываем только то, что выбрали правильный элемент и прочитали обратно его CODE.
Подключаем шов из старого обработчика одной строкой
Старый файл может по-прежнему собирать $_POST, проверять сессию и решать, какой шаблон показать. Замена касается только места, где раньше напрямую вызывался CIBlockElement::Update. Обработчик получает отчёт и сам выбирает, как показать ошибку. Это важно: класс не должен делать echo, редирект или запись в глобальный $APPLICATION, иначе ответственность снова смешается.
<?php
// fragment from a legacy save handler
try {
$writer = new ProductCodeWriter(7); // 7 — ID инфоблока этого сценария
$report = $writer->write($_POST['ID'], $_POST['CODE']);
$message = $report['changed']
? 'Символьный код обновлён.'
: 'Символьный код уже совпадает.';
} catch (InvalidArgumentException $error) {
$message = $error->getMessage();
} catch (RuntimeException $error) {
// В конкретном проекте здесь выбирают локальный журнал и вывод формы.
$message = $error->getMessage();
}
В таком виде изменение можно снять отдельно в истории: одна правка добавляет ProductCodeWriter и переключает один вызов. Не стоит в том же коммите переименовывать шаблоны, менять поля инфоблока и переписывать импорт. Если поведение расходится, разница будет лежать либо на входе, либо в шве, либо после него. Чем меньше одновременно изменённых мест, тем легче вернуть старую строку без отката соседней работы.
Проверяем один след, затем расширяем покрытие
- Найти прямой вызов
CIBlockElement::Updateи записать его входы: ID, инфоблок, поле и источник кода. - Выбрать изолированный элемент на разрешённом тестовом контуре; не брать рабочую карточку ради быстрой проверки.
- Сохранить исходный
CODEи ожидаемый новый код рядом с проверкой, не в комментарии памяти. - Вызвать шов только для этого элемента и проверить отчёт
changed. - Снова выбрать элемент API-запросом и сравнить фактический
CODEс ожидаемым. - Если появилось расхождение, вернуть вызов на прежний путь или восстановить сохранённое значение по процедуре проекта; не добавлять второй
Update«на всякий случай». - Лишь после понятного результата подключать следующий вызов и отдельно разбирать его предусловия.
Границы защищённого шва
Этот приём не устраняет все риски старого Bitrix-проекта. Он не говорит, какой CODE нужен для SEO, как синхронизировать его с торговыми предложениями и можно ли менять его у опубликованного элемента. Если на инфоблоке есть обработчик OnBeforeIBlockElementUpdate, он вправе изменить поля или отменить обновление. Значит, перед включением на конкретном сайте нужно посмотреть зарегистрированные обработчики и проверить их на разрешённом сценарии.
Также не следует превращать каждую строку PHP в класс. Если в файле один вызов и он уже имеет ясный вход, достаточно функции с тем же контрактом. Шов оправдан, когда повторяемая операция сейчас смешана с формой или когда нужно зафиксировать её проверку. Цель не в количестве файлов. Цель — увидеть, что именно меняется, и получить точку, куда можно поставить следующий локальный тест.
Итог: маленькая замена оставляет понятный след
Для первого рефакторинга достаточно вынести один вызов Update, ограничить инфоблок и поле, а затем прочитать результат обратно. Это не переписывает модуль и не обещает, что все связи каталога стали безопасными. Зато следующий разработчик получает конкретный маршрут: вход, проверка, запись, повторная выборка и ясная точка отказа. Когда этот маршрут устойчив, рядом можно вынести следующий — но только после отдельной проверки.