{ "index": 32, "slug": "editorial-2027-02-mechanism-bitrix-lessons", "title": "Bitrix API и версия: имя метода не обещает одинаковый контракт", "excerpt": "Как проверить установленную поверхность Bitrix API, сопоставить поля legacy и D7 и остановить миграцию, если среда не подтверждает совместимость.", "contentHtml": "
Ошибка начинается без падения. В документации найден метод, класс подключён, вызов возвращает ID или объект. Но на другой установке модуль не загружен, поле называется иначе, пустая строка означает другое состояние, а обработчик события меняет результат. Симптом появляется позже: форма теряет значение, импорт создаёт дубль, редкая операция падает после обновления. Цена ошибки — не только исправление PHP. Команда получает повреждённые данные, повторную загрузку и миграцию, которую уже нельзя безопасно повторить.
\nТезис: совместимость Bitrix проверяют не по имени класса и не по номеру версии. Нужна граница из трёх фактов: модуль подключён, нужная поверхность API доступна, а вход и выход совпадают с контрактом проекта. Только после этого выбирают legacy-вызов, D7 или адаптер между ними.
\nУ старого и нового API может быть одна предметная область, но разные правила. Документация Bitrix указывает CUser и Bitrix\\Main\\UserTable как поверхности работы с пользователями. Это не утверждение, что вызовы взаимозаменяемы в конкретном проекте. У них могут различаться способ выборки, типы полей, ошибки, события и требования к версии ядра.
Сначала проверяют загрузчик. CModule::IncludeModule('iblock') или \\Bitrix\\Main\\Loader::includeModule('iblock') отвечает на вопрос «модуль установлен и подключён?». Ответ true ещё не подтверждает нужный метод и mapping полей. Ответ false закрывает путь к следующему слою: нельзя маскировать отсутствие модуля вызовом класса, который случайно доступен через другой bootstrap.
Затем проверяют поверхность. Нужны точные операции: найти запись, создать, обновить, получить идентификатор и разобрать ошибку. Проверка только существования класса слишком слаба. Она не отвечает, принимает ли метод нужные поля и сохранит ли различие между отсутствующим полем и явной очисткой.
\nПоследний слой — смысл результата. Успешный ID доказывает, что операция вернула идентификатор. Он не доказывает, что значение записалось в нужный формат, обработчики отработали ожидаемо, а публичная выборка увидит запись. Поэтому контракт нужно проверять через повторное чтение и отрицательные случаи.
\nНиже — учебная функция. Она не обращается к реальному серверу и не обещает поддержку перечисленных версий. Входной manifest нужно получить в конкретной среде безопасной диагностикой. Функция только разделяет отсутствие модуля, legacy-поверхность и D7-поверхность.
function chooseUserSurface(manifest) {\n if (!manifest.moduleLoaded) {\n return { kind: 'stop', reason: 'module-not-loaded' };\n }\n\n if (manifest.methods.includes('CUser::Update')) {\n return { kind: 'legacy', operation: 'update-user' };\n }\n\n if (manifest.methods.includes('Bitrix\\\\Main\\\\UserTable')) {\n return { kind: 'd7', operation: 'update-user' };\n }\n\n return { kind: 'stop', reason: 'operation-not-confirmed' };\n}\n\n// Учебные данные. Это не результат работы production-установки.\nconst surface = chooseUserSurface({\n moduleLoaded: true,\n methods: ['CUser::Update'],\n});\n\nconsole.log(surface);\n// { kind: 'legacy', operation: 'update-user' }\nВажен порядок условий. Сначала функция останавливается при отсутствии модуля. Затем она выбирает подтверждённую операцию, а не любой похожий класс. Если обе поверхности доступны, выбор должен задавать адаптер проекта: например, установленная версия, зафиксированный набор полей и проверенная матрица регрессии. Автоматически предпочитать D7 только потому, что он новее, нельзя.
\nАдаптер должен принимать канонический вход. Для пользователя это может быть объект с id, email, phone и явными состояниями missing и clear. Внутри адаптера поля переводятся в формат выбранного API. Так legacy-детали не расползаются по формам, импорту и обработчикам.
function toLegacyFields(user) {\n const fields = {};\n\n if (user.email.state === 'value') {\n fields.EMAIL = user.email.value;\n } else if (user.email.state === 'clear') {\n fields.EMAIL = '';\n }\n\n if (user.phone.state === 'value') {\n fields.PERSONAL_PHONE = user.phone.value.trim();\n }\n\n return fields;\n}\nЭтот код показывает только mapping. Он не вызывает CUser::Update, не проверяет права и не описывает локальные события. В рабочем проекте перед вызовом нужно зафиксировать, что означает пустое поле, кто владеет нормализацией, какие ошибки возвращает API и что должен увидеть read-back. Если D7-модель хранит поле в другом представлении, адаптер должен преобразовать его обратно в тот же канонический результат.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Класс найден, вызов падает на части серверов | Модуль не установлен или не подключён в этом bootstrap | Проверить IncludeModule в том же окружении и записать результат | Остановить операцию с диагностикой либо подключить модуль явно |
| Одинаковое имя поля даёт разный результат | Различаются тип, формат или пользовательское поле | Сравнить mapping, тип значения, missing и clear | Оставить преобразование в адаптере и добавить read-back |
| Update вернул успех, но данные не видны | Проверяется только код ответа; фильтр или событие меняет выборку | Прочитать запись по ID и выполнить контрольный запрос с условиями каталога | Разделить факт записи и публичную видимость |
| После обновления появился неизвестный метод | Документация описывает другую версию или другую поверхность | Сверить версию ядра, модуль и фактический manifest операций | Вернуть адаптер к подтверждённой операции или ограничить поддержку |
| Повторный импорт создаёт новые записи | В контракте нет стабильного ключа и идемпотентного поиска | Повторить тот же вход и сравнить внешний ключ и результат чтения | Найти существующую запись по согласованному ключу до создания |
| Миграция проходит на тесте, но меняет production-смысл | Тест проверяет ID, но не события, права и пустые состояния | Добавить characterization-тесты для успеха, ошибки, retry и очистки | Не заменять поверхность до закрытия отрицательного пути |
Документация Bitrix описывает публичную поверхность, но не знает локальные обработчики, права, переопределения в /local, структуру инфоблока и фактический bootstrap. Две установки с одним номером версии могут иметь разные модули и данные. Поэтому ссылка на страницу API не является доказательством совместимости проекта.
Manifest тоже не равен полному тесту. Он подтверждает доступность слоя, но не доказывает корректность SQL, событий и бизнес-правил. Не следует печатать в диагностике пользовательские данные. Достаточно версии, имени модуля и названий операций. Секреты и значения полей в такой вывод не входят.
\nИногда правильное решение — не мигрировать. Если legacy-вызов покрыт тестами, выполняет нужную операцию и новый API не даёт проверяемого выигрыша, адаптер может сохранить старую поверхность. Если новый метод доступен, но его mapping или события не доказаны, переход откладывают. Остановка с причиной безопаснее частичной миграции и ручного восстановления данных.
\nГраница готова, когда повторяемая проверка показывает: нужный модуль подключается; операция существует в каждой обязательной среде; поля имеют записанный mapping; значение, отсутствие и очистка дают ожидаемый read-back; ошибка и отказ прав не превращаются в успех; повторный запуск не создаёт дубль; а сборка и тесты проходят без ручного вмешательства. Для каждой версии нужен сохранённый результат проверки. Если нет хотя бы одного из этих доказательств, готов только план проверки, а не миграция.
\n