8 lines
24 KiB
JSON
8 lines
24 KiB
JSON
{
|
||
"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><?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}</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>"
|
||
}
|