{ "index": 33, "slug": "editorial-2027-02-practice-bitrix-lessons", "title": "Bitrix legacy без догадок: сохранить вызов, поставить адаптер или заменить API", "excerpt": "Как принять решение по старому Bitrix API: отделить наблюдаемое поведение от имени класса, проверить границу и не начинать миграцию без контракта.", "contentHtml": "
После замены старого вызова Bitrix страница продолжает открываться, но новый пользователь не создаётся. В логах остаётся общий отказ. Административная форма показывает успех, хотя обработчик, который отправляет данные во внешнюю систему, не сработал. Цена ошибки — не один сломанный метод. Команде приходится восстанавливать порядок событий, формат полей и правила, которые раньше были спрятаны в legacy-коде.
\nВозраст класса не доказывает его опасность. Имя нового API не доказывает совместимость. Безопасное решение начинается с наблюдаемого контракта: какие входы принимает код, какое состояние меняет, что возвращает, какие события запускает и как сообщает об отказе. Пока контракт не проверен, есть три действия: сохранить вызов, обернуть его адаптером или заменить после сравнения поведения.
\nТипичный симптом выглядит безобидно: после обновления вызова исчезает запись, меняется формат телефона или внешний обработчик получает пустой идентификатор. Система может не упасть. Она продолжит работать с неполным состоянием. Поэтому проверять нужно не только отсутствие исключения. Нужны чтение результата, события, права, повторный запуск и реакция на частичный отказ.
\nРиск выше, если одна функция выполняет несколько операций. Она может привести email к нижнему регистру, создать пользователя, вызвать обработчик и вернуть ID. Замена класса меняет сразу четыре соглашения. Сравнение строк вызова этого не показывает.
\nВ Bitrix старый CUser и D7-класс Bitrix\\\\Main\\\\UserTable относятся к одной предметной области, но это не делает их взаимозаменяемыми в проекте. Нужно проверить mapping полей, способ ошибки, порядок событий и доступность модуля в конкретной установке. Документация даёт публичную поверхность API. Она не знает локальные обработчики, пользовательские поля и скрытые callers.
Сохранить — оставить текущий вызов и ограничить изменение вокруг него. Это подходит, когда callers мало, побочные эффекты не описаны, а задача не требует нового контракта. Сохранение не убирает технический долг. Оно не даёт неизвестному поведению разойтись дальше и оставляет обратимый шаг.
\nОбернуть — поставить одну границу между приложением и Bitrix API. Адаптер принимает поля проекта, проверяет обязательные значения, вызывает legacy-код и переводит ошибку в согласованный результат. Внешний код перестаёт зависеть от PERSONAL_PHONE, подключения модуля и объекта, в котором Bitrix хранит последнюю ошибку.
Заменить — перейти на новый контракт, а не только поменять имя класса. Сначала описывают mapping полей, порядок операций, успешный результат, ошибку, события и требования к версии. Затем запускают старый и новый путь на одинаковых входах. Если различие намеренное, его фиксируют как изменение поведения. Если случайное, замену откладывают.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Вызов вернул ID, но внешняя система не получила запись | событие зависит от старого порядка | журнал событий и повторное чтение записи | сохранить путь или обернуть его |
| Обновление прошло, поле стало пустым | пустое значение трактуется как очистка | сравнить отсутствие поля, null и пустую строку | задать mapping и правило пустого значения |
| Класс не найден | модуль не подключён или API недоступно в версии | проверить IncludeModule и поверхность методов | остановить замену и уточнить контракт |
| Повторный запуск создал дубль | операция не различает обработанный объект | запустить один вход дважды и сравнить ID | добавить ключ идемпотентности |
| Новый вызов работает для одного caller | caller-ы передают разные форматы | найти все места вызова и фактические входы | поставить адаптер с единым входом |
Таблица задаёт порядок вопросов, но не доказывает причину сама. Для каждой строки нужен артефакт: лог, diff полей, результат повторного запуска, список callers или проверка подключения модуля. Если артефакта нет, решение остаётся гипотезой.
\nНиже приведён учебный PHP-фрагмент. Он показывает форму узкой границы и не утверждает, что конкретный проект должен обновлять пользователя именно так. Перед применением нужно сверить версию Bitrix, права, обработчики и правила хранения контактов.
\n<?php\nfunction updateUserContact(int $userId, array $input): int\n{\n if ($userId < 1) {\n throw new InvalidArgumentException('userId must be positive');\n }\n\n $fields = [];\n if (array_key_exists('email', $input)) {\n $email = trim((string) $input['email']);\n if ($email === '' || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {\n throw new InvalidArgumentException('email is invalid');\n }\n $fields['EMAIL'] = strtolower($email);\n }\n\n if (array_key_exists('phone', $input)) {\n $fields['PERSONAL_PHONE'] = trim((string) $input['phone']);\n }\n if ($fields === []) {\n throw new InvalidArgumentException('no fields to update');\n }\n\n $user = new CUser();\n if ($user->Update($userId, $fields) === false) {\n throw new RuntimeException($user->LAST_ERROR);\n }\n return $userId;\n}\nАдаптер делает три вещи. Он отличает отсутствие поля от переданного значения. Он приводит email к одному формату. Он переводит отказ Bitrix в исключение, которое может обработать caller. Он не объявляет успехом сам факт вызова метода. После обновления нужен read-back: получить пользователя, сравнить поля и проверить побочный обработчик.
\nОтрицательный путь важнее короткого примера. Если проект разрешает очистить email пустой строкой, проверка выше неверна. Если обработчик ожидает исходный регистр, lowercase меняет контракт. Если старый метод обновляет связанные поля, узкий wrapper скрывает обязательную операцию. В этих случаях пример нельзя переносить целиком. Нужно изменить mapping, расширить контракт или оставить legacy-вызов.
\nНачните с callers. Поиск по имени метода недостаточен: вызов может находиться в сервисе, обработчике события, шаблоне или административной форме. Для каждого caller запишите входные поля, права, ожидаемый результат и реакцию на ошибку. Если один caller передаёт пустую строку, а другой не передаёт поле, это разные операции.
\nЗатем зафиксируйте владельца состояния. Кто создаёт запись? Кто меняет её после события? Кто формирует внешний ID? Какой код считается успехом? Где находится последняя ошибка? Ответы должны подтверждаться кодом, логом или чтением данных. Фраза «Bitrix сам вызывает обработчик» не является проверкой: обработчик зависит от модуля, версии, прав и условий события.
\nТретья граница — версия. Для старого API проверьте доступность модуля и метода в выполняемой среде. Для нового API проверьте namespace, mapping полей, типы значений и поведение исключений. Вызов CModule::IncludeModule должен быть частью проверки границы, а не строкой, которую добавляют после сбоя.
<?php\nif (!CModule::IncludeModule('main')) {\n throw new RuntimeException('Bitrix main module is unavailable');\n}\n\n$user = new CUser();\n$ok = $user->Update($userId, $fields);\nif (!$ok) {\n throw new RuntimeException($user->LAST_ERROR);\n}\nЭтот второй фрагмент тоже учебный. Он проверяет подключение модуля, но не проверяет права, события, mapping или корректность данных. В конкретном проекте идентификатор модуля и способ вызова нужно сверить с установленной версией.
\nПереход на D7 или другой новый слой не переносит автоматически пользовательские поля, события, права и внешние идентификаторы. Даже правильный mapping опасен, если операция выполняется внутри транзакции или участвует в импорте. Нужно понять поведение при повторе и частичном отказе.
\nUnit-тест на чистом объекте не проверяет модуль, реальную версию, события и права. Но полная копия production-среды не обязательна для первого шага. Достаточно сузить границу, зафиксировать факты и назвать, чего учебная проверка не покрывает. Не следует объявлять замену готовой только потому, что новый метод вернул ожидаемый ID.
\nИногда правильный результат — не менять код. Если нет воспроизводимого входа, список callers неполный, а ошибка зависит от неизвестного обработчика, сохранение вызова снижает риск. Сначала получают недостающий контракт. Затем возвращаются к адаптеру или замене.
\nИзменение готово, когда другой разработчик может повторить проверку по записи входа и получить тот же результат. В записи есть callers, версия и подключение модуля, mapping полей, события, успешный и отрицательный путь, read-back и способ отката. Для замены старый и новый путь дают одинаковый результат либо команда явно согласовала изменение. Если пункт неизвестен, готова не миграция, а следующая проверка.
\n