Files
progcode/editorial/agent-rewrites/031.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

2 lines
18 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":31,"slug":"editorial-2027-02-field-bitrix-lessons","title":"Миграция пользовательского поля Bitrix: сохранить смысл, а не только значение","excerpt":"Как перенести поле пользователя из legacy API Bitrix в новый контракт без тихой потери данных: mapping, пустые значения, read-back, повторный запуск и критерий готовности.","contentHtml":"<p>Симптом появляется после успешного вызова Bitrix API. Пользователь получил новый ID, но телефон остался пустым. Email сохранился в другом регистре. Повторный запуск создал вторую связь с внешней системой. В логах есть только <code>true</code> от <code>CUser::Update</code>, поэтому команда не видит, на каком шаге исчезло значение.</p><p>Цена ошибки выше, чем одна неверная строка. По телефону не находится пользователь. Уведомление уходит на старый адрес. Импорт нельзя безопасно повторить. Если исходное значение уже перезаписано, восстановление зависит от резервной копии или ручного поиска.</p><p><strong>Тезис:</strong> миграция поля готова не тогда, когда API принял запрос. Она готова, когда команда может показать mapping, прочитать сохранённый смысл обратно и повторить тот же вход без дубля или неожиданной очистки.</p><h2>Механизм: поле проходит четыре границы</h2><p>Legacy-код обычно передаёт массив с именами Bitrix: <code>PERSONAL_PHONE</code>, <code>EMAIL</code>, <code>XML_ID</code>. Новый код хочет получить объект вроде <code>{ phone, email, externalId }</code>. Это не простая замена имён. Каждое поле имеет формат, правило пустого значения и обратное представление.</p><p>Первая граница — вход. Отсутствующий <code>PERSONAL_PHONE</code> может означать «не менять телефон», а пустая строка — «очистить телефон». Если привести оба состояния к <code>null</code>, адаптер потеряет команду пользователя.</p><p>Вторая граница — нормализация. Для телефона допустимы пробелы и разные формы записи, но правило должно быть конкретным. Для email можно привести регистр к нижнему, если это разрешает контракт приложения. Нельзя применять одну функцию ко всем значениям: <code>XML_ID</code>, комментарий и парольный хэш имеют разные правила.</p><p>Третья граница — запись. Официальный метод <code>CUser::Update</code> принимает ID и массив полей. Успех означает, что метод не сообщил об ошибке. Он не доказывает, что downstream-обработчик, индекс или внешний обмен увидели ожидаемый смысл.</p><p>Четвёртая граница — чтение. После записи нужно получить ту же запись способом, которым её читает приложение. Сравнивайте не только ID и флаг успеха. Сравнивайте поля, внешний идентификатор, состояние пустоты и, если это важно, время изменения.</p><figure><img src=\"/assets/editorial/2027/bitrix-lessons-2027-migration-evidence-loop.svg\" alt=\"Цикл миграции пользовательского поля Bitrix: mapping, нормализация, запись, чтение и повторный запуск\"/><figcaption>Успешная запись занимает середину цикла. Доказательство результата появляется после повторного чтения и проверки повторяемости.</figcaption></figure><h2>Mapping должен описывать смысл</h2><p>Начните с небольшой таблицы. В ней видны не только старое и новое имя, но также пустое состояние, источник и проверка результата.</p><table><thead><tr><th>Поле</th><th>Legacy-вход</th><th>Каноническое значение</th><th>Проверка</th></tr></thead><tbody><tr><td>Телефон</td><td><code>PERSONAL_PHONE</code>, строка с пробелами</td><td><code>phone</code>, нормализованная строка</td><td>read-back и формат</td></tr><tr><td>Email</td><td><code>EMAIL</code>, исходный регистр</td><td><code>email</code>, регистр по правилу контракта</td><td>валидность и точное чтение</td></tr><tr><td>Связь</td><td><code>XML_ID</code></td><td><code>externalId</code></td><td>одна запись при retry</td></tr><tr><td>Не передан</td><td>ключ отсутствует</td><td><code>unchanged</code></td><td>старое значение не меняется</td></tr><tr><td>Очищен</td><td>ключ есть, значение пустое</td><td><code>clear</code></td><td>два разных теста</td></tr></tbody></table><p>Если пришёл неизвестный ключ, не угадывайте его назначение по похожему имени. Остановите преобразование и сообщите, какое поле не входит в контракт. Если email не проходит правило приложения, не превращайте его в пустую строку. Верните ошибку до записи.</p><h2>Учебный пример: нормализация до вызова Bitrix</h2><p>Следующая функция — изолированный учебный пример. Она не подключается к Bitrix и не обещает результат реального окружения. Её задача — показать, где сохраняется различие между отсутствующим полем и явной очисткой.</p><pre><code>function toCanonical(input) {\n const result = {};\n\n if (Object.hasOwn(input, 'PERSONAL_PHONE')) {\n if (input.PERSONAL_PHONE === '') {\n result.phone = { action: 'clear' };\n } else {\n const phone = String(input.PERSONAL_PHONE).trim();\n if (!phone) throw new Error('phone-invalid');\n result.phone = { action: 'set', value: phone };\n }\n }\n\n if (Object.hasOwn(input, 'EMAIL')) {\n const email = String(input.EMAIL).trim().toLowerCase();\n if (!email.includes('@')) throw new Error('email-invalid');\n result.email = { action: 'set', value: email };\n }\n\n if (Object.hasOwn(input, 'XML_ID')) {\n result.externalId = { action: 'set', value: String(input.XML_ID) };\n }\n\n return result;\n}\n\nconst value = toCanonical({\n PERSONAL_PHONE: ' +7 900 000-00-00 ',\n EMAIL: 'User@Example.TEST',\n XML_ID: 'crm-17',\n});\n\n// phone: set '+7 900 000-00-00'\n// email: set 'user@example.test'\n// externalId: set 'crm-17'</code></pre><p>Для учебного входа функция удаляет внешние пробелы, приводит email к нижнему регистру и оставляет связь как строку. Это выбранные правила примера, а не универсальная политика Bitrix. В реальном проекте их нужно заменить правилами доменного контракта.</p><p>Адаптер записи строится после такой проверки. Для <code>unchanged</code> поле не добавляется. Для <code>clear</code> передаётся явное значение очистки, согласованное с API и проектом.</p><pre><code>$fields = [];\n\nif ($canonical['phone']['action'] === 'set') {\n $fields['PERSONAL_PHONE'] = $canonical['phone']['value'];\n}\n\nif ($canonical['phone']['action'] === 'clear') {\n $fields['PERSONAL_PHONE'] = '';\n}\n\nif ($canonical['email']['action'] === 'set') {\n $fields['EMAIL'] = $canonical['email']['value'];\n}\n\n$user = new CUser;\nif (!$user-&gt;Update($userId, $fields)) {\n throw new RuntimeException($user-&gt;LAST_ERROR);\n}</code></pre><p>Код показывает только форму вызова. Он не проверяет права, события, пользовательские поля и транзакцию. Перед использованием нужно подтвердить, что модуль и нужная поверхность API доступны в конкретной установке. Для D7 и legacy-пути нельзя считать классы взаимозаменяемыми без отдельного mapping.</p><h2>Симптомы и проверки</h2><table><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>API вернул успех, поле пустое</td><td>Неверное имя, формат или обработчик изменил значение</td><td>Сравнить mapping, raw-вход и read-back</td><td>Исправить границу и повторить на одной записи</td></tr><tr><td>Повторный запуск создаёт дубль</td><td>Нет стабильного внешнего ключа или retry неидемпотентен</td><td>Повторить вход по <code>XML_ID</code> и проверить количество записей</td><td>Закрепить ключ операции и запретить создание без него</td></tr><tr><td>Поле исчезает при частичном обновлении</td><td>Missing и clear сведены к одному значению</td><td>Проверить отсутствие ключа и пустую строку отдельно</td><td>Передавать очистку только явной командой</td></tr><tr><td>Один сервер принимает вызов, другой — нет</td><td>Модуль не подключён или поверхности API различаются</td><td>Проверить <code>IncludeModule</code>, класс и метод в целевой среде</td><td>Остановить миграцию и выбрать совместимый adapter</td></tr><tr><td>Email стал недействительным</td><td>Нормализация скрыла ошибку или правило шире контракта</td><td>Проверить валидатор до записи и значение после чтения</td><td>Вернуть ошибку, не записывать пустой заменитель</td></tr></tbody></table><h2>Порядок действий</h2><ol><li>Выберите одно поле и один стабильный идентификатор записи. Не начинайте с массового запуска.</li><li>Снимите фактический вход: ключи, типы, пустые значения, внешний ID и источник данных.</li><li>Составьте mapping table. Отдельно назовите состояния <code>missing</code>, <code>clear</code>, <code>invalid</code> и <code>unchanged</code>.</li><li>Проверьте доступность модуля и метода в целевой версии Bitrix. Если условие не выполнено, остановите изменение.</li><li>Прогоните нормализацию на обезличенных значениях. Проверьте пробелы, регистр, пустоту, неверный формат и неизвестный ключ.</li><li>Запишите одну запись через адаптер. Сохраните безопасный идентификатор операции, не помещая персональные данные в лог.</li><li>Сделайте read-back: сравните поля, пустые состояния, внешний ID и побочные признаки, важные для приложения.</li><li>Повторите тот же вход. Убедитесь, что запись не дублируется, значение не меняется без правила, а операция остаётся обратимой.</li><li>Только после этого расширяйте выборку. Любое расхождение сначала классифицируйте как mapping, формат, права, событие или версию API.</li></ol><h2>Ограничения</h2><p>Документация Bitrix описывает публичный API, но не знает локальные события, права, пользовательские поля, обработчики и SQL-ограничения проекта. Успешный вызов в одной установке не доказывает совместимость другой. Версия ядра помогает сузить поиск, но не заменяет проверку фактической поверхности.</p><p>Read-back может отличаться от входа по допустимому правилу сервера. Телефон может получить другой формат. Время изменения зависит от среды. Такие расхождения нужно разделить на ожидаемую нормализацию и потерю смысла. Нельзя объявлять их одинаковыми только потому, что совпал ID.</p><p>Учебный JavaScript-пример не является production-валидатором. Проверка <code>email.includes('@')</code> намеренно упрощена. PHP-функция <code>filter_var</code> может быть частью технической проверки email, но она не определяет бизнес-правила, разрешённые домены и требуемое поведение пустого поля.</p><p>Если read-back невозможен, миграция не получает доказательство сохранения. В этом случае не расширяйте объём. Сначала добавьте безопасный способ проверить запись или оставьте адаптер за границей массового запуска. Отрицательное решение лучше тихой потери данных.</p><h2>Проверяемый критерий готовности</h2><p>Одна миграция поля готова, если другой инженер может восстановить вход и получить тот же результат: mapping объясняет каждое переданное поле, missing и clear различаются, API-поверхность подтверждена, read-back совпадает по смысловым значениям, а повторный запуск не создаёт дубль.</p><p>Для отрицательных случаев есть отдельные доказательства: неизвестное поле останавливается, неверный email не превращается в пустую строку, отсутствие поля не очищает старое значение, недоступный модуль не приводит к частичному запуску. Только после этих проверок можно говорить о расширении на следующую выборку.</p><h2>Проверяемые источники</h2><ul><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>. Страница не описывает локальные события и mapping проекта.</li><li><a href=\"https://dev.1c-bitrix.ru/api_help/main/reference/cuser/index.php\" target=\"_blank\" rel=\"noopener noreferrer\">1С-Битрикс: класс CUser и поля</a> — назначение <code>ID</code>, <code>XML_ID</code> и <code>PERSONAL_PHONE</code>, а также связь с D7-представлением. Это описание API, не подтверждение состояния установки.</li><li><a href=\"https://dev.1c-bitrix.ru/api_help/main/reference/cmodule/includemodule.php\" target=\"_blank\" rel=\"noopener noreferrer\">1С-Битрикс: CModule::IncludeModule</a> — проверка установки и подключения модуля перед использованием его классов. Результат проверки нужно получить в целевой среде.</li></ul>"}