8 lines
25 KiB
JSON
8 lines
25 KiB
JSON
{
|
||
"index": 33,
|
||
"slug": "editorial-2027-02-practice-bitrix-lessons",
|
||
"title": "Bitrix legacy без догадок: как сравнить CUser и D7 перед миграцией",
|
||
"excerpt": "Практический способ решить, что делать со старым вызовом Bitrix: зафиксировать наблюдаемый контракт, проверить callers и события, а затем выбрать сохранение, адаптер или замену.",
|
||
"contentHtml": "<p>После замены старого вызова Bitrix страница может продолжить отвечать 200, а новый пользователь всё равно не появится во внешней системе. В административной форме будет успех, в логах — общий отказ, а повторная попытка иногда создаст дубль. Такая поломка возникает не из-за возраста класса. Она появляется, когда команда сравнивает две строки кода и не сравнивает поведение вокруг них.</p>\n<p>Разберём один типовой сценарий: проект обновляет работу с пользователем и выбирает между <code>CUser</code> и D7 <code>Bitrix\\Main\\UserTable</code>. Безопасное решение опирается на наблюдаемый контракт. Нужно знать входные поля, владельца состояния, результат, ошибки, события, права и поведение при повторе. Пока хотя бы один из этих пунктов неизвестен, есть три честных действия: сохранить вызов, поставить адаптер или отложить замену до получения фактов.</p>\n<h2>Начните с симптома и границы операции</h2>\n<p>Сначала запишите не название класса, а то, что увидел пользователь или соседняя система. Например: форма вернула успешный ответ, запись в таблице пользователей изменилась, но обработчик не отправил внешний идентификатор. Другой вариант: обновление прошло, а поле телефона исчезло. Третий: второй запуск с теми же данными создал ещё одну запись.</p>\n<p>Для каждого симптома нужны пять наблюдений: точный вход, caller, результат основного вызова, изменённое состояние и побочный эффект. Caller — это конкретный код, который передаёт данные в операцию: контроллер, обработчик события, импорт или административная форма. Один и тот же метод могут вызывать несколько callers с разными правилами пустых значений и правами.</p>\n<p>Сформулируйте границу так: «получить email и телефон, изменить пользователя с ID 42, вернуть подтверждённый результат, запустить нужный обработчик». Если в этой фразе нет способа подтвердить результат, контракт неполон. Возвращённый ID или <code>true</code> ещё не доказывают, что дочерняя интеграция приняла данные.</p>\n<figure><img src='/assets/editorial/2027/bitrix-lessons-2027-keep-wrap-replace-tree.svg' alt='Дерево решения для Bitrix legacy: после проверки callers, полей и побочных эффектов выбирается сохранение, адаптер или замена API' loading='lazy' /><figcaption>Решение начинается с контракта. Если побочные эффекты не подтверждены, сначала фиксируют их, а не расширяют зону изменения.</figcaption></figure>\n<h2>Какие части контракта нельзя потерять</h2>\n<p>У старого вызова обычно несколько обязанностей, даже если они скрыты в одной функции. Он может нормализовать email, очищать поле пустой строкой, проверять права, вызывать обработчики и записывать внешний ID. Миграция, которая переносит только список полей, переносит не контракт, а его видимую часть.</p>\n<p><strong>Вход.</strong> Зафиксируйте отличие между отсутствующим ключом, <code>null</code>, пустой строкой и пробелами. Для проекта это могут быть четыре разных команды: не менять значение, очистить его, отклонить запрос или сохранить нормализованный текст.</p>\n<p><strong>Результат.</strong> У <code>CUser::Update</code> официальный контракт — логическое значение: при ошибке метод возвращает <code>false</code>, а текст находится в <code>LAST_ERROR</code>. Если проект считает успехом отправку данных в CRM, это уже второй результат, который надо проверять отдельно.</p>\n<p><strong>События.</strong> Обработчик может менять состояние после основной записи. В официальном описании <code>OnAfterUserUpdate</code> прямо сказано, что событие вызывается после попытки изменения свойств методом <code>CUser::Update</code>, а в параметрах доступны результат и сообщение ошибки. Поэтому при переходе на ORM нельзя автоматически считать, что локальный обработчик, его порядок и его данные останутся прежними. Их нужно найти и проверить в конкретной версии и конфигурации.</p>\n<p><strong>Права и версия.</strong> Доступность класса, модуля, поля и операции зависит от выполняемой установки. Документация описывает публичный API, но не знает пользовательские поля проекта, зарегистрированные обработчики и ограничения роли. Это не недостаток документации, а граница того, что можно утверждать по ссылке.</p>\n<h2>CUser и UserTable: одна область, разные модели</h2>\n<p>Bitrix называет <code>Bitrix\\Main\\UserTable</code> аналогом старого <code>CUser</code>, но слово «аналог» не означает замену один к одному. Страница D7 описывает <code>UserTable</code> как класс для работы с пользователями, наследующий <code>DataManager</code>. У <code>DataManager::update</code> другая форма контракта: статический вызов принимает первичный ключ и массив данных и возвращает объект результата.</p>\n<p>У старого и нового путей различаются как минимум поверхности ошибок и событий. Старый путь — объект <code>CUser</code>, булев результат и <code>LAST_ERROR</code>. ORM-путь — статический метод сущности и объект результата, который надо обработать по правилам D7. Если адаптер скрывает эту разницу, он обязан определить единый результат наружу: например, подтверждённое изменение пользователя или структурированную ошибку.</p>\n<p>Данные тоже нельзя переносить механически. Поле <code>PERSONAL_PHONE</code> может быть стандартным, а <code>UF_...</code> — пользовательским полем со своим типом и форматом. Массив для поля-списка, строка для текста и значение для файла требуют разных проверок. Сверьте карту полей в работающей установке и зафиксируйте преобразования рядом с контрактом адаптера.</p>\n<p>Ещё одна граница — операция, а не только запись. Если после изменения пользователя срабатывает синхронизация, уведомление или расчёт доступа, сравнивайте цепочку целиком. Новый ORM-вызов, который обновил строку, может быть технически успешным и функционально неполным.</p>\n<div class='table-scroll'><table><caption>Как связать наблюдение с решением</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>какой обработчик запускается и где формируется внешний ID</td><td>список обработчиков, лог и read-back</td><td>сохранить путь до фиксации цепочки или обернуть её</td></tr><tr><td>Пустой email или телефон меняет состояние</td><td>отличие отсутствующего ключа, null и пустой строки</td><td>набор входов и снимок полей до/после</td><td>описать mapping и явное правило очистки</td></tr><tr><td>Один путь возвращает false, другой — объект результата</td><td>как caller распознаёт ошибку и получает сообщение</td><td>контракт ответа и отрицательный тест</td><td>привести ошибки к одному интерфейсу адаптера</td></tr><tr><td>Класс или поле недоступны в окружении</td><td>версию ядра, модуль, карту полей и права роли</td><td>версия, проверка загрузки и результат чтения карты</td><td>остановить замену до устранения неизвестного</td></tr><tr><td>Повторный запуск создаёт дубль</td><td>идемпотентность и владелец внешнего идентификатора</td><td>два одинаковых запуска и сравнение ID</td><td>добавить ключ повторения или оставить старый путь</td></tr></tbody></table></div>\n<p>Таблица не является доказательством причины. Она задаёт минимальный набор наблюдений. Для строки «обработчик не дал запись» нужен реальный журнал или чтение внешней системы; для строки о пустом значении — входной набор и состояние до и после. Если команда может назвать только предположение, миграция ещё не готова.</p>\n<h2>Учебный адаптер для старого пути</h2>\n<p>Ниже — небольшой PHP-пример границы вокруг <code>CUser::Update</code>. Он намеренно принимает проектные имена <code>email</code> и <code>phone</code>, а наружу отдаёт ID только после успешного вызова. В этом варианте пустой телефон означает очистку, а пустой email отклоняется. Это решение примера, не универсальное правило Bitrix.</p>\n<pre><code><?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}</code></pre>\n<p>В примере есть три существенные границы. <code>array_key_exists</code> не даёт случайно превратить отсутствие поля в очистку. Нормализация email вынесена в одно место. Ошибка старого API переводится в исключение, понятное caller-у. При этом функция не обещает, что внешний обработчик завершился: после неё нужен read-back пользователя и отдельная проверка побочного результата.</p>\n<p>У кода есть намеренные ограничения. Он не проверяет права, не знает обязательность email в конкретной установке и не нормализует телефон по правилам бизнеса. Он также не гарантирует, что ID существует: официальное описание <code>CUser::Update</code> отмечает, что отсутствие пользователя с указанным ID само по себе не даёт ошибки. Если для проекта это важно, адаптер должен явно прочитать запись до обновления или проверить результат чтением после него.</p>\n<h2>Как сравнить старый и новый путь</h2>\n<p>Не начинайте с массовой замены. Сначала соберите characterization test — тест, который фиксирует фактическое поведение старого пути, даже если оно кажется неудобным. Для одинакового входа запишите поля до операции, ответ, ошибку, события, внешний ID и поля после операции. Такой тест не объявляет старое поведение правильным; он показывает, что именно нельзя потерять случайно.</p>\n<p>Затем подготовьте отдельный тест для D7. Официальный <code>DataManager::update</code> возвращает объект результата, поэтому проверьте не только отсутствие исключения, но и успешность результата, сообщение ошибки и фактическое состояние записи. Если объект результата обрабатывается иначе, чем <code>LAST_ERROR</code>, адаптер должен скрыть эту разницу, а не заставлять каждый caller знать обе модели.</p>\n<p>События проверяйте наблюдением, а не названием. Найдите регистрации <code>OnBeforeUserUpdate</code>, <code>OnAfterUserUpdate</code> и проектные обработчики, проверьте условия их выполнения и порядок записи. Для нового пути отдельно установите, какие ORM-события и локальные интеграции используются. Если официальная страница описывает только старое событие, это повод провести проверку, а не обещать совместимость.</p>\n<p>Минимальный набор сравнений выглядит так: корректный email и телефон; отсутствие email при изменении телефона; пустой телефон для очистки; невалидный email; несуществующий ID; недостаточные права; повторный запуск; отказ внешней системы после записи. Для каждого случая нужны ожидаемый результат, наблюдаемое состояние и решение о том, допустимо ли отличие.</p>\n<h2>Когда оставить, обернуть или заменить</h2>\n<p><strong>Оставить</strong> — разумно, если задача не требует нового API, callers немного, а побочные эффекты ещё не описаны. Это не отказ от улучшений. Команда фиксирует границу и избегает изменения нескольких неизвестных одновременно.</p>\n<p><strong>Обернуть</strong> — полезно, когда callers несколько или старый API проникает в разные слои. Адаптер принимает канонический вход проекта, применяет mapping, единообразно возвращает успех или ошибку и оставляет место для read-back. Его задача — уменьшить число мест, где живут знания о Bitrix, а не спрятать незавершённую миграцию.</p>\n<p><strong>Заменить</strong> — можно после сравнения поведения и осознанного решения об отличиях. Для каждого намеренного отличия нужна запись: что меняется, почему это безопасно, кто владеет обновлённым состоянием и как откатить релиз. Если команда не может воспроизвести старый результат, замену лучше отложить.</p>\n<h2>Порядок проверки</h2>\n<ol><li>Запишите симптом, конкретный вход, caller, версию Bitrix, роль и цену ошибки.</li><li>Найдите все callers и выпишите фактические поля, значения по умолчанию, обработчики и реакцию на отказ.</li><li>Снимите состояние пользователя и связанной системы до операции, включая внешний ID.</li><li>Проверьте доступность класса, модуля, поля и метода в выполняемой установке.</li><li>Зафиксируйте поведение старого пути на успешном, отрицательном, пустом и повторном входе.</li><li>Опишите канонический input/output адаптера: формат полей, очистку, ID, ошибку и владельца состояния.</li><li>Сравните старый и новый путь на одинаковых данных, включая read-back и побочные события.</li><li>Оставьте только тот шаг, для которого названы тест, наблюдаемый результат и способ отката.</li></ol>\n<h2>Ограничения применимости</h2>\n<p>Эта схема подходит для выбора границы вокруг legacy-вызова. Она не заменяет аудит безопасности, ревизию ролей и прав, миграцию схемы или проверку производительности. Если операция работает в импорте, транзакции или очереди, отдельно исследуйте повтор, частичный отказ и порядок подтверждения внешней системы.</p>\n<p>Учебный пример нельзя копировать без сверки версии, обязательных полей, формата пользовательских полей, прав и зарегистрированных обработчиков. Нормализация email и разрешение очистки телефона — проектные решения. Для файла, списка или множественного пользовательского поля понадобятся другие типы входа и отдельные тесты.</p>\n<p>Unit-тест на чистом объекте не доказывает поведение реального модуля и событий. Полная копия production для первого шага тоже не обязательна: достаточно ограничить тестовую среду, указать, что она не покрывает, и не выдавать её результат за доказательство совместимости. Критерий готовности простой: другой разработчик повторяет вход, видит тот же результат, понимает намеренные отличия и знает, как вернуть старый путь.</p>\n<h2>Проверяемые источники</h2><ul><li><a href='https://dev.1c-bitrix.ru/api_help/main/reference/cuser/index.php' target='_blank' rel='noopener noreferrer'>1С-Битрикс: CUser</a> — официальное описание класса, его полей и указанного аналога в D7. Локальные callers, обработчики и права проекта эта страница не описывает.</li><li><a href='https://dev.1c-bitrix.ru/api_help/main/reference/cuser/update.php' target='_blank' rel='noopener noreferrer'>1С-Битрикс: CUser::Update</a> — официальный контракт булева результата, <code>LAST_ERROR</code> и параметров обновления.</li><li><a href='https://dev.1c-bitrix.ru/api_d7/bitrix/main/usertable/index.php' target='_blank' rel='noopener noreferrer'>1С-Битрикс: UserTable</a> и <a href='https://dev.1c-bitrix.ru/api_d7/bitrix/main/entity/datamanager/update.php' target='_blank' rel='noopener noreferrer'>DataManager::update</a> — официальные сведения о D7-сущности и форме ORM-обновления с объектом результата.</li><li><a href='https://dev.1c-bitrix.ru/api_help/main/events/onafteruserupdate.php' target='_blank' rel='noopener noreferrer'>1С-Битрикс: OnAfterUserUpdate</a> — официальное описание события после попытки изменения через <code>CUser::Update</code>, его результата и сообщения ошибки.</li><li><a href='https://www.php.net/manual/en/function.filter-var.php' target='_blank' rel='noopener noreferrer'>PHP Manual: filter_var</a> — описание функции, использованной в учебной проверке email; оно не задаёт правила конкретного проекта.</li></ul>"
|
||
}
|