Files
progcode/editorial/agent-rewrites/033.json
T

8 lines
25 KiB
JSON
Raw 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": 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>&lt;?php\nfunction updateUserContact(int $userId, array $input): int\n{\n if ($userId &lt; 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-&gt;Update($userId, $fields) === false) {\n throw new RuntimeException($user-&gt;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>"
}