Files

8 lines
24 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 32,
"slug": "editorial-2027-02-mechanism-bitrix-lessons",
"title": "Bitrix API и версия: имя метода не обещает одинаковый контракт",
"excerpt": "Как проверить установленную поверхность Bitrix API, сопоставить поля legacy и D7 и остановить миграцию, если среда не подтверждает совместимость.",
"contentHtml": "<p>Ошибка при миграции Bitrix часто появляется не в строке вызова. Документация нашла класс, автозагрузка сработала, операция вернула успешный результат — а на другой установке модуль не подключён, пользовательское поле имеет другой тип или обработчик изменяет данные после записи. Через несколько часов форма теряет значение, импорт создаёт дубль, а откат уже требует ручного восстановления.</p>\n<p><strong>Главный вывод:</strong> совместимость нельзя вывести из имени класса или номера версии. Её нужно доказать для конкретной операции: модуль подключён в нужном bootstrap, метод действительно доступен, поля имеют согласованный mapping, а результат выдерживает повторное чтение и отрицательные случаи. Если хотя бы один слой неизвестен, миграция ещё не готова.</p>\n<h2>Имя метода — только первый слой</h2>\n<p>Bitrix документирует два поколения поверхности для одной предметной области. Класс <code>CUser</code> относится к старому ядру, а <code>Bitrix\\Main\\UserTable</code> — к D7 и ORM. В документации прямо указано, что UserTable является аналогом CUser. Это полезная подсказка для поиска, но не обещание побитной совместимости: разные методы принимают разные аргументы, возвращают разные типы результата и по-разному сообщают об ошибках.</p>\n<p>Начинать нужно с модуля. Для пользовательской области это обычно модуль <code>main</code>, а не <code>iblock</code>. Старый вызов <code>CModule::IncludeModule('main')</code> проверяет, установлен ли модуль, и подключает его файл <code>include.php</code>. D7-вариант <code>\\Bitrix\\Main\\Loader::includeModule('main')</code> подключает модуль по имени и возвращает <code>true</code> или <code>false</code>; в документации для метода также перечислено исключение <code>LoaderException</code>. Ни один из этих ответов не доказывает, что нужная операция и её поля совпадают с ожиданиями приложения.</p>\n<p>Следующий слой — поверхность операции. Наличие класса доказывает только возможность разрешить имя. Для миграции обновления пользователя нужно отдельно подтвердить <code>CUser::Update</code> или выбранный D7-вызов, а затем зафиксировать набор полей, права и наблюдаемый результат. Проверка через <code>class_exists</code> без проверки операции создаёт ложное чувство совместимости.</p>\n<figure><img src='/assets/editorial/2027/bitrix-lessons-2027-version-boundary-matrix.svg' alt='Четыре границы совместимости Bitrix API: модуль main, операция обновления, mapping полей и результат чтения' loading='lazy' /><figcaption>Контракт проходит четыре проверки: подключён ли модуль, доступна ли операция, одинаково ли трактуются поля и подтверждается ли результат повторным чтением.</figcaption></figure>\n<h2>Что именно различается между CUser и D7</h2>\n<p>Сравнивать нужно не названия классов, а один сценарий от входа до чтения. Например, пусть импорт обновляет email и телефон пользователя по стабильному локальному ID. В legacy-поверхности поля называются <code>EMAIL</code> и <code>PERSONAL_PHONE</code>. Документация CUser описывает их как строковые поля. Но проект может добавлять пользовательские поля, нормализовать телефон, ограничивать смену email или подключать обработчики события. Эти правила находятся за пределами общей сигнатуры.</p>\n<div class='table-scroll'><table><caption>Что доказать перед заменой поверхности</caption><thead><tr><th scope='col'>Слой</th><th scope='col'>CUser</th><th scope='col'>D7 UserTable</th><th scope='col'>Доказательство в проекте</th></tr></thead><tbody><tr><td>Подключение</td><td><code>CModule::IncludeModule('main')</code></td><td><code>Loader::includeModule('main')</code></td><td>Успешный результат в том же bootstrap, где выполняется операция</td></tr><tr><td>Операция</td><td><code>CUser::Update($id, $fields)</code></td><td>ORM-метод выбранной модели</td><td>Точный вызов, аргументы, тип результата и обработка ошибки</td></tr><tr><td>Поля</td><td><code>EMAIL</code>, <code>PERSONAL_PHONE</code>, <code>UF_*</code></td><td>Поля из карты сущности и их типы</td><td>Таблица соответствий для value, missing и clear</td></tr><tr><td>Ошибка</td><td><code>false</code> и <code>LAST_ERROR</code></td><td>Результат ORM и исключения</td><td>Тест отказа прав, неверного поля и недоступной записи</td></tr><tr><td>Результат</td><td>Булево подтверждение операции</td><td>Результат ORM-операции</td><td>Повторное чтение и контроль публичной выборки</td></tr></tbody></table></div>\n<p>У этой таблицы есть важная оговорка: последний столбец не заполняется документацией автоматически. Его заполняет команда на своей установке. В частности, официальная страница <code>CUser::Update</code> сообщает, что метод возвращает <code>true</code> при успехе и <code>false</code> при ошибке, а текст ошибки находится в <code>LAST_ERROR</code>. Та же страница отдельно говорит: если пользователя с указанным ID нет, ошибки не возникает. Значит, одного булева результата недостаточно — отсутствие записи нужно проверять до или после изменения.</p>\n<h2>Воспроизводимая диагностика поверхности</h2>\n<p>Диагностика должна быть безопасной: она выводит имена модулей и операций, но не email, телефоны, токены и значения пользовательских полей. Запускайте её в том же окружении и через тот же bootstrap, который использует рабочий код. Иначе результат описывает диагностический скрипт, а не реальный путь запроса.</p>\n<pre><code>&lt;?php\nuse Bitrix\\Main\\Loader;\n\nfunction inspectUserSurface(): array\n{\n $report = [\n 'module' =&gt; 'main',\n 'moduleLoaded' =&gt; false,\n 'legacyUpdate' =&gt; false,\n 'd7UserTable' =&gt; false,\n ];\n\n try {\n $report['moduleLoaded'] = Loader::includeModule('main');\n } catch (\\Throwable $exception) {\n return $report + ['reason' =&gt; 'module-load-exception'];\n }\n\n if (!$report['moduleLoaded']) {\n return $report + ['reason' =&gt; 'module-not-loaded'];\n }\n\n $report['legacyUpdate'] = class_exists('CUser')\n &amp;&amp; method_exists('CUser', 'Update');\n $report['d7UserTable'] = class_exists('\\Bitrix\\Main\\UserTable')\n &amp;&amp; method_exists('\\Bitrix\\Main\\UserTable', 'getMap');\n\n return $report;\n}</code></pre>\n<p>Этот фрагмент отвечает только на вопрос о доступности слоёв. Он не выбирает D7 автоматически и не пишет данные. Если <code>moduleLoaded</code> равен <code>false</code>, возвращается причина остановки. Если модуль загружен, но обе поверхности имеют значение <code>false</code>, нужно проверять bootstrap и версию ядра, а не подменять имя класса.</p>\n<p>Версия тоже входит в отчёт, но не заменяет поведенческую проверку. В официальной документации CUser и UserTable есть собственные границы версий: CUser описан с версии 3.0.6, UserTable наследует DataManager, а для старых версий модуля Main документация указывает другой класс-родитель. Эти сведения помогают понять, какой код вообще может встретиться в установке. Они не отвечают, как локальные обработчики и пользовательские поля поведут себя в конкретном проекте.</p>\n<h2>Канонический вход и read-back</h2>\n<p>Адаптер должен принимать один формат данных независимо от выбранной поверхности. У каждого поля полезно различать три состояния: <code>missing</code> — поле не участвует в обновлении, <code>value</code> — записывается новое значение, <code>clear</code> — значение очищается явно. Если передавать пустую строку вместо отдельного состояния, код теряет намерение вызывающей стороны и начинает зависеть от поведения конкретного API.</p>\n<pre><code>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}</code></pre>\n<p>Преобразование ещё не является обновлением. Перед записью адаптер проверяет, что вход содержит допустимый идентификатор, а после записи перечитывает запись по тому же ключу. Для CUser это может быть <code>GetByID</code> или контролируемый запрос, для D7 — выбранная ORM-операция чтения. Сравнивать нужно канонический результат: нормализованный телефон, фактический email, отсутствие поля и состояние, которое видит дальнейшая бизнес-логика.</p>\n<p>Успех записи и видимость — разные утверждения. Обработчик может изменить поле, индекс или статус; кеш может показать старое значение; фильтр каталога может исключить запись. Поэтому characterization-тест должен проверять как минимум исходное чтение, запись, повторное чтение и запрос потребителя. Если запись должна быть идемпотентной, второй запуск с тем же внешним ключом обязан обновить найденную сущность, а не создать новую.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class='table-scroll'><table><caption>Диагностика несовпадения контрактов Bitrix</caption><thead><tr><th scope='col'>Симптом</th><th scope='col'>Вероятная причина</th><th scope='col'>Проверка</th><th scope='col'>Действие</th></tr></thead><tbody><tr><td>Класс найден, а вызов падает на части серверов</td><td><code>main</code> не установлен или не подключён в реальном bootstrap</td><td>Записать результат <code>Loader::includeModule('main')</code> без пользовательских данных</td><td>Остановить операцию с причиной либо исправить подключение</td></tr><tr><td>Метод существует, но поле отклоняется</td><td>Тип, имя или форма пользовательского поля различаются</td><td>Сверить карту полей, входной тип и состояние <code>clear</code></td><td>Оставить преобразование в адаптере и добавить отрицательный тест</td></tr><tr><td>Update вернул успех, но пользователя нет в чтении</td><td>ID не существует, чтение использует другой фильтр или сработал обработчик</td><td>Проверить существование ID, перечитать запись и выполнить запрос потребителя</td><td>Разделить отсутствие записи, факт изменения и публичную видимость</td></tr><tr><td>После перехода значения стали другими</td><td>Legacy и ORM по-разному нормализуют дату, телефон или пользовательское поле</td><td>Прогнать одинаковый набор входов и сравнить канонический read-back</td><td>Зафиксировать mapping либо оставить прежнюю поверхность</td></tr><tr><td>Повторный импорт создал дубль</td><td>Перед созданием не используется стабильный внешний ключ</td><td>Повторить вход и сравнить количество записей и ключи</td><td>Сначала искать сущность, затем обновлять или создавать</td></tr><tr><td>На тесте всё работает, в рабочей среде — нет</td><td>Различаются права, обработчики, модули, данные или bootstrap</td><td>Сравнить обезличенный manifest и прогнать smoke-набор в целевой среде</td><td>Ограничить поддержку средами с подтверждённым контрактом</td></tr></tbody></table></div>\n<h2>Порядок безопасной миграции</h2>\n<ol><li>Назвать одну предметную операцию и её инварианты: что можно изменить, что должно остаться прежним и какой результат увидит потребитель.</li><li>Зафиксировать окружение: версию ядра, идентификатор модуля, bootstrap, права технического пользователя и включённые локальные обработчики.</li><li>Проверить подключение <code>main</code> в реальном пути выполнения. Отдельно сохранить результат и исключение, не записывая значения полей.</li><li>Подтвердить точную операцию: класс или таблицу, метод, аргументы, результат, исключения и способ получения текста ошибки.</li><li>Составить mapping полей. Для каждого поля описать тип, нормализацию, <code>missing</code>, <code>value</code>, <code>clear</code> и внешний ключ.</li><li>Запустить одинаковый набор на legacy и D7 в тестовой копии данных. Сравнить не только код ответа, но и read-back, события, права и публичную выборку.</li><li>Спрятать вызов за адаптером с каноническим входом и классифицированными причинами остановки. Не смешивать выбор поверхности с бизнес-логикой формы или импорта.</li><li>Проверить повторный запуск, несуществующий ID, неверное поле, отказ прав, исключение загрузчика и частичный результат.</li><li>Включать новую поверхность поэтапно, с журналом обезличенных исходов и возможностью вернуть старый адаптер. При несовпадении контракта остановить переход, а не продолжать на догадке.</li></ol>\n<h2>Ограничения применимости</h2>\n<p>Официальная документация описывает публичный API, но не локальную конфигурацию. Она не знает обработчики в <code>/local</code>, права, состав пользовательских полей, кеши, настройки сайтов и фактический bootstrap. Даже одинаковая версия ядра не гарантирует одинаковый набор модулей и данных.</p>\n<p>Проверка доступности метода не является тестом миграции. <code>method_exists</code> не выявляет семантику события, права записи, ограничения базы и работу фильтра потребителя. Manifest нужен для ранней остановки и сравнения сред, а не как единственное доказательство.</p>\n<p>Нельзя переносить mapping из примера в проект без проверки. Имена <code>EMAIL</code> и <code>PERSONAL_PHONE</code> относятся к стандартной модели пользователя, но у проекта могут быть собственные <code>UF_*</code>-поля, другой источник истины или обязательная нормализация. Секреты и персональные значения в диагностический вывод не входят.</p>\n<p>Иногда безопасный результат — не мигрировать. Если legacy-вызов покрыт тестами, а новый API не даёт измеримого выигрыша, адаптер может сохранить старую поверхность. Если модуль, операция или mapping не подтверждены, остановка с понятной причиной дешевле частичной записи и ручного восстановления.</p>\n<h2>Критерий готовности</h2>\n<p>Переход можно считать готовым только после повторяемого набора доказательств: модуль подключается в каждой обязательной среде; точная операция доступна; поля имеют записанный mapping; value, missing и clear дают ожидаемый read-back; ошибка, исключение и отказ прав не превращаются в успех; несуществующий ID обрабатывается явно; повторный запуск не создаёт дубль; запрос потребителя видит правильное состояние. Если не закрыт хотя бы один пункт, готова проверка неизвестности, но не замена API.</p>\n<h2>Проверяемые источники</h2><ul><li><a href='https://dev.1c-bitrix.ru/api_help/main/reference/cmodule/includemodule.php' target='_blank' rel='noopener noreferrer'>Документация Bitrix: CModule::IncludeModule</a> — описывает проверку установки, подключение файла модуля и булев результат.</li><li><a href='https://dev.1c-bitrix.ru/api_d7/bitrix/main/loader/includemodule.php' target='_blank' rel='noopener noreferrer'>Документация Bitrix D7: Loader::includeModule</a> — описывает подключение модуля по имени, результат <code>true/false</code> и исключение LoaderException.</li><li><a href='https://dev.1c-bitrix.ru/api_help/main/reference/cuser/index.php' target='_blank' rel='noopener noreferrer'>Документация Bitrix: CUser</a> — описывает legacy-класс, поля пользователя и аналог UserTable в D7.</li><li><a href='https://dev.1c-bitrix.ru/api_help/main/reference/cuser/update.php' target='_blank' rel='noopener noreferrer'>Документация Bitrix: CUser::Update</a> — фиксирует результат <code>true/false</code>, <code>LAST_ERROR</code> и поведение при несуществующем ID.</li><li><a href='https://dev.1c-bitrix.ru/api_d7/bitrix/main/usertable/index.php?print=Y' target='_blank' rel='noopener noreferrer'>Документация Bitrix D7: UserTable</a> — описывает D7-класс, его наследование и карту ORM-полей.</li></ul>"
}