{ "index": 32, "slug": "editorial-2027-02-mechanism-bitrix-lessons", "title": "Bitrix API и версия: имя метода не обещает одинаковый контракт", "excerpt": "Как проверить установленную поверхность Bitrix API, сопоставить поля legacy и D7 и остановить миграцию, если среда не подтверждает совместимость.", "contentHtml": "

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

\n

Главный вывод: совместимость нельзя вывести из имени класса или номера версии. Её нужно доказать для конкретной операции: модуль подключён в нужном bootstrap, метод действительно доступен, поля имеют согласованный mapping, а результат выдерживает повторное чтение и отрицательные случаи. Если хотя бы один слой неизвестен, миграция ещё не готова.

\n

Имя метода — только первый слой

\n

Bitrix документирует два поколения поверхности для одной предметной области. Класс CUser относится к старому ядру, а Bitrix\\Main\\UserTable — к D7 и ORM. В документации прямо указано, что UserTable является аналогом CUser. Это полезная подсказка для поиска, но не обещание побитной совместимости: разные методы принимают разные аргументы, возвращают разные типы результата и по-разному сообщают об ошибках.

\n

Начинать нужно с модуля. Для пользовательской области это обычно модуль main, а не iblock. Старый вызов CModule::IncludeModule('main') проверяет, установлен ли модуль, и подключает его файл include.php. D7-вариант \\Bitrix\\Main\\Loader::includeModule('main') подключает модуль по имени и возвращает true или false; в документации для метода также перечислено исключение LoaderException. Ни один из этих ответов не доказывает, что нужная операция и её поля совпадают с ожиданиями приложения.

\n

Следующий слой — поверхность операции. Наличие класса доказывает только возможность разрешить имя. Для миграции обновления пользователя нужно отдельно подтвердить CUser::Update или выбранный D7-вызов, а затем зафиксировать набор полей, права и наблюдаемый результат. Проверка через class_exists без проверки операции создаёт ложное чувство совместимости.

\n
Четыре границы совместимости Bitrix API: модуль main, операция обновления, mapping полей и результат чтения
Контракт проходит четыре проверки: подключён ли модуль, доступна ли операция, одинаково ли трактуются поля и подтверждается ли результат повторным чтением.
\n

Что именно различается между CUser и D7

\n

Сравнивать нужно не названия классов, а один сценарий от входа до чтения. Например, пусть импорт обновляет email и телефон пользователя по стабильному локальному ID. В legacy-поверхности поля называются EMAIL и PERSONAL_PHONE. Документация CUser описывает их как строковые поля. Но проект может добавлять пользовательские поля, нормализовать телефон, ограничивать смену email или подключать обработчики события. Эти правила находятся за пределами общей сигнатуры.

\n
Что доказать перед заменой поверхности
СлойCUserD7 UserTableДоказательство в проекте
ПодключениеCModule::IncludeModule('main')Loader::includeModule('main')Успешный результат в том же bootstrap, где выполняется операция
ОперацияCUser::Update($id, $fields)ORM-метод выбранной моделиТочный вызов, аргументы, тип результата и обработка ошибки
ПоляEMAIL, PERSONAL_PHONE, UF_*Поля из карты сущности и их типыТаблица соответствий для value, missing и clear
Ошибкаfalse и LAST_ERRORРезультат ORM и исключенияТест отказа прав, неверного поля и недоступной записи
РезультатБулево подтверждение операцииРезультат ORM-операцииПовторное чтение и контроль публичной выборки
\n

У этой таблицы есть важная оговорка: последний столбец не заполняется документацией автоматически. Его заполняет команда на своей установке. В частности, официальная страница CUser::Update сообщает, что метод возвращает true при успехе и false при ошибке, а текст ошибки находится в LAST_ERROR. Та же страница отдельно говорит: если пользователя с указанным ID нет, ошибки не возникает. Значит, одного булева результата недостаточно — отсутствие записи нужно проверять до или после изменения.

\n

Воспроизводимая диагностика поверхности

\n

Диагностика должна быть безопасной: она выводит имена модулей и операций, но не email, телефоны, токены и значения пользовательских полей. Запускайте её в том же окружении и через тот же bootstrap, который использует рабочий код. Иначе результат описывает диагностический скрипт, а не реальный путь запроса.

\n
<?php\nuse Bitrix\\Main\\Loader;\n\nfunction inspectUserSurface(): array\n{\n    $report = [\n        'module' => 'main',\n        'moduleLoaded' => false,\n        'legacyUpdate' => false,\n        'd7UserTable' => false,\n    ];\n\n    try {\n        $report['moduleLoaded'] = Loader::includeModule('main');\n    } catch (\\Throwable $exception) {\n        return $report + ['reason' => 'module-load-exception'];\n    }\n\n    if (!$report['moduleLoaded']) {\n        return $report + ['reason' => 'module-not-loaded'];\n    }\n\n    $report['legacyUpdate'] = class_exists('CUser')\n        && method_exists('CUser', 'Update');\n    $report['d7UserTable'] = class_exists('\\Bitrix\\Main\\UserTable')\n        && method_exists('\\Bitrix\\Main\\UserTable', 'getMap');\n\n    return $report;\n}
\n

Этот фрагмент отвечает только на вопрос о доступности слоёв. Он не выбирает D7 автоматически и не пишет данные. Если moduleLoaded равен false, возвращается причина остановки. Если модуль загружен, но обе поверхности имеют значение false, нужно проверять bootstrap и версию ядра, а не подменять имя класса.

\n

Версия тоже входит в отчёт, но не заменяет поведенческую проверку. В официальной документации CUser и UserTable есть собственные границы версий: CUser описан с версии 3.0.6, UserTable наследует DataManager, а для старых версий модуля Main документация указывает другой класс-родитель. Эти сведения помогают понять, какой код вообще может встретиться в установке. Они не отвечают, как локальные обработчики и пользовательские поля поведут себя в конкретном проекте.

\n

Канонический вход и read-back

\n

Адаптер должен принимать один формат данных независимо от выбранной поверхности. У каждого поля полезно различать три состояния: missing — поле не участвует в обновлении, value — записывается новое значение, clear — значение очищается явно. Если передавать пустую строку вместо отдельного состояния, код теряет намерение вызывающей стороны и начинает зависеть от поведения конкретного API.

\n
function toLegacyFields(array $user): array\n{\n    $fields = [];\n\n    if ($user['email']['state'] === 'value') {\n        $fields['EMAIL'] = trim($user['email']['value']);\n    } elseif ($user['email']['state'] === 'clear') {\n        $fields['EMAIL'] = '';\n    }\n\n    if ($user['phone']['state'] === 'value') {\n        $fields['PERSONAL_PHONE'] = trim($user['phone']['value']);\n    } elseif ($user['phone']['state'] === 'clear') {\n        $fields['PERSONAL_PHONE'] = '';\n    }\n\n    return $fields;\n}
\n

Преобразование ещё не является обновлением. Перед записью адаптер проверяет, что вход содержит допустимый идентификатор, а после записи перечитывает запись по тому же ключу. Для CUser это может быть GetByID или контролируемый запрос, для D7 — выбранная ORM-операция чтения. Сравнивать нужно канонический результат: нормализованный телефон, фактический email, отсутствие поля и состояние, которое видит дальнейшая бизнес-логика.

\n

Успех записи и видимость — разные утверждения. Обработчик может изменить поле, индекс или статус; кеш может показать старое значение; фильтр каталога может исключить запись. Поэтому characterization-тест должен проверять как минимум исходное чтение, запись, повторное чтение и запрос потребителя. Если запись должна быть идемпотентной, второй запуск с тем же внешним ключом обязан обновить найденную сущность, а не создать новую.

\n

Симптом → причина → проверка → действие

\n
Диагностика несовпадения контрактов Bitrix
СимптомВероятная причинаПроверкаДействие
Класс найден, а вызов падает на части серверовmain не установлен или не подключён в реальном bootstrapЗаписать результат Loader::includeModule('main') без пользовательских данныхОстановить операцию с причиной либо исправить подключение
Метод существует, но поле отклоняетсяТип, имя или форма пользовательского поля различаютсяСверить карту полей, входной тип и состояние clearОставить преобразование в адаптере и добавить отрицательный тест
Update вернул успех, но пользователя нет в чтенииID не существует, чтение использует другой фильтр или сработал обработчикПроверить существование ID, перечитать запись и выполнить запрос потребителяРазделить отсутствие записи, факт изменения и публичную видимость
После перехода значения стали другимиLegacy и ORM по-разному нормализуют дату, телефон или пользовательское полеПрогнать одинаковый набор входов и сравнить канонический read-backЗафиксировать mapping либо оставить прежнюю поверхность
Повторный импорт создал дубльПеред созданием не используется стабильный внешний ключПовторить вход и сравнить количество записей и ключиСначала искать сущность, затем обновлять или создавать
На тесте всё работает, в рабочей среде — нетРазличаются права, обработчики, модули, данные или bootstrapСравнить обезличенный manifest и прогнать smoke-набор в целевой средеОграничить поддержку средами с подтверждённым контрактом
\n

Порядок безопасной миграции

\n
  1. Назвать одну предметную операцию и её инварианты: что можно изменить, что должно остаться прежним и какой результат увидит потребитель.
  2. Зафиксировать окружение: версию ядра, идентификатор модуля, bootstrap, права технического пользователя и включённые локальные обработчики.
  3. Проверить подключение main в реальном пути выполнения. Отдельно сохранить результат и исключение, не записывая значения полей.
  4. Подтвердить точную операцию: класс или таблицу, метод, аргументы, результат, исключения и способ получения текста ошибки.
  5. Составить mapping полей. Для каждого поля описать тип, нормализацию, missing, value, clear и внешний ключ.
  6. Запустить одинаковый набор на legacy и D7 в тестовой копии данных. Сравнить не только код ответа, но и read-back, события, права и публичную выборку.
  7. Спрятать вызов за адаптером с каноническим входом и классифицированными причинами остановки. Не смешивать выбор поверхности с бизнес-логикой формы или импорта.
  8. Проверить повторный запуск, несуществующий ID, неверное поле, отказ прав, исключение загрузчика и частичный результат.
  9. Включать новую поверхность поэтапно, с журналом обезличенных исходов и возможностью вернуть старый адаптер. При несовпадении контракта остановить переход, а не продолжать на догадке.
\n

Ограничения применимости

\n

Официальная документация описывает публичный API, но не локальную конфигурацию. Она не знает обработчики в /local, права, состав пользовательских полей, кеши, настройки сайтов и фактический bootstrap. Даже одинаковая версия ядра не гарантирует одинаковый набор модулей и данных.

\n

Проверка доступности метода не является тестом миграции. method_exists не выявляет семантику события, права записи, ограничения базы и работу фильтра потребителя. Manifest нужен для ранней остановки и сравнения сред, а не как единственное доказательство.

\n

Нельзя переносить mapping из примера в проект без проверки. Имена EMAIL и PERSONAL_PHONE относятся к стандартной модели пользователя, но у проекта могут быть собственные UF_*-поля, другой источник истины или обязательная нормализация. Секреты и персональные значения в диагностический вывод не входят.

\n

Иногда безопасный результат — не мигрировать. Если legacy-вызов покрыт тестами, а новый API не даёт измеримого выигрыша, адаптер может сохранить старую поверхность. Если модуль, операция или mapping не подтверждены, остановка с понятной причиной дешевле частичной записи и ручного восстановления.

\n

Критерий готовности

\n

Переход можно считать готовым только после повторяемого набора доказательств: модуль подключается в каждой обязательной среде; точная операция доступна; поля имеют записанный mapping; value, missing и clear дают ожидаемый read-back; ошибка, исключение и отказ прав не превращаются в успех; несуществующий ID обрабатывается явно; повторный запуск не создаёт дубль; запрос потребителя видит правильное состояние. Если не закрыт хотя бы один пункт, готова проверка неизвестности, но не замена API.

\n

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

" }