From 2359da44393999caad7677f42b49bba79dfc08a3 Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Fri, 4 Sep 2026 01:29:32 +0300 Subject: [PATCH] editorial: refine article 362 --- editorial/agent-rewrites/362.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editorial/agent-rewrites/362.json b/editorial/agent-rewrites/362.json index ddcdc91..403b86e 100644 --- a/editorial/agent-rewrites/362.json +++ b/editorial/agent-rewrites/362.json @@ -2,6 +2,6 @@ "index": 362, "slug": "bitrix-api-функция-для-генерации-кода-элемент", "title": "Bitrix API: как сгенерировать символьный код элемента и не сломать URL", - "excerpt": "CUtil::translit создаёт только кандидата для CODE. Разбираем нормализацию, проверку уникальности, обновление элемента и безопасное поведение при смене публичного адреса.", - "contentHtml": "

В админке создают два элемента с названием «Телефон Samsung», а оба получают один символьный код telefon-samsung. Один URL начинает открывать не ту карточку или выборка возвращает первый найденный элемент. При массовом импорте ошибка повторяется сотни раз. Цена ошибки — дубли адресов, неверные карточки в каталоге и ручное восстановление старых ссылок.

\n

Другой симптом появляется позже. Редактор меняет название товара, код пересчитывается автоматически, а внешняя ссылка на прежний адрес отвечает 404. Здесь проблема не в качестве транслитерации. Публичный адрес связали с изменяемым полем без правила миграции.

\n

Тезис простой: CUtil::translit решает преобразование строки. Функция не знает, какие коды заняты, в каком инфоблоке ищут элемент и должен ли старый URL продолжать работать. Надёжный генератор делает четыре шага: получает кандидат, нормализует его, проверяет уникальность в нужной области и только потом сохраняет код. Для опубликованного элемента смена кода становится отдельной операцией с перенаправлением или таблицей старых адресов.

\n

Что возвращает CUtil::translit

\n

Метод принимает исходную строку, язык и массив параметров. В учебном примере язык задан как ru, регистр меняется на нижний, пробелы и прочие неподходящие символы заменяются дефисом, а повторяющиеся замены удаляются. Параметр max_len ограничивает длину результата.

\n

Эти параметры создают читаемую строку, но не делают её идентификатором. Названия «Кофе Classic 250 г» и «Кофе Classic 250г» могут после преобразования оказаться одинаковыми. Строка из одних знаков пунктуации может превратиться в пустое значение. Одинаковые названия в разных разделах могут быть допустимыми для редактора, но не для URL, если маршрут не содержит раздел.

\n

Символьный код нужно рассматривать как данные элемента. Название может измениться из-за исправления опечатки, локализации или требований каталога. Если URL строится из CODE, смена поля меняет внешний адрес. Поэтому генерация при создании и пересчёт при каждом редактировании — разные сценарии.

\n
\"Поток
Транслитерация формирует кандидата. Решение о сохранении принимает код, который знает инфоблок, текущий ID и правило адреса.
\n

Кандидат, нормализация и уникальность

\n

Сначала задайте область уникальности. Если компонент ищет элемент по IBLOCK_ID и CODE, проверка должна использовать тот же инфоблок. Если код входит в глобальный адрес без раздела, безопаснее считать его уникальным для всей витрины. Если URL содержит SECTION_CODE, проект может разрешить одинаковый код в разных разделах, но это решение должно совпадать с фильтром компонента.

\n

При редактировании текущий элемент нельзя считать конфликтом с самим собой. Фильтр проверки исключает его ID. Иначе любое сохранение существующей записи будет добавлять новый суффикс, а код начнёт расти от item до item-2 и дальше.

\n

После транслитерации уберите повторные дефисы и дефисы по краям. Пустой результат замените на понятный резервный префикс. В этом примере используется item. Реальный префикс выбирайте по доменной модели, а не по случайному значению.

\n

Симптом → причина → проверка → действие

\n
СимптомПричинаПроверкаДействие
Две карточки имеют один CODEПроверили только транслитерациюПосчитать элементы по инфоблоку и кодуДобавлять суффикс до сохранения и исправить конфликтующие записи
При редактировании код меняется самГенератор запускается для любого сохраненияСравнить старый и новый код и режим операцииГенерировать автоматически только при создании или при пустом поле
Код пустой или начинается с дефисаНазвание не содержит подходящих символовПроверить результат после trim и регулярного выраженияПрименить явный резервный префикс и проверить его уникальность
Код свободен при проверке, но запись конфликтует позжеДве операции выполняются параллельноПовторить создание одновременно и проверить базуДобавить ограничение или транзакционную стратегию, затем обработать ошибку записи
Старый адрес отвечает 404Опубликованный CODE изменили без миграцииОткрыть старый URL и проверить правила маршрутаСохранить историю адресов и настроить редирект либо не менять код
\n

Учебная реализация

\n

Ниже приведён учебный пример для старого API Bitrix. Функция codeExists намеренно обозначает адаптер проекта: в нём нужно выполнить выборку через CIBlockElement::GetList или другой принятый в проекте слой доступа. Пример не утверждает, что конкретная схема фильтра подходит каждому инфоблоку.

\n
<?php\n\nfunction makeElementCode($name, $iblockId, $elementId = 0)\n{\n    $base = CUtil::translit((string)$name, 'ru', array(\n        'max_len' => 80,\n        'change_case' => 'L',\n        'replace_space' => '-',\n        'replace_other' => '-',\n        'delete_repeat_replace' => true,\n    ));\n\n    $base = trim(preg_replace('/-+/', '-', $base), '-');\n    $base = $base !== '' ? $base : 'item';\n    $code = $base;\n    $suffix = 1;\n\n    while (codeExists($iblockId, $code, $elementId)) {\n        $suffix++;\n        $code = $base . '-' . $suffix;\n    }\n\n    return $code;\n}\n\nfunction codeExists($iblockId, $code, $elementId = 0)\n{\n    $filter = array(\n        'IBLOCK_ID' => (int)$iblockId,\n        '=CODE' => $code,\n        '!ID' => (int)$elementId,\n    );\n\n    $result = CIBlockElement::GetList(\n        array(),\n        $filter,\n        false,\n        array('nTopCount' => 1),\n        array('ID')\n    );\n\n    return (bool)$result->Fetch();\n}
\n

Код использует одинаковый контекст для генерации и проверки: тот же инфоблок, тот же формат CODE и исключение текущего ID. Если первый кандидат занят, цикл создаёт telefon-samsung-2, затем telefon-samsung-3. Номер не доказывает, что товары связаны между собой. Он только различает значения.

\n

Учебный код не выполняет запись элемента. В рабочем обработчике сначала проверьте обязательные поля и права, затем передайте полученный код в массив полей для CIBlockElement::Add или Update. После вызова проверьте возвращённый ID и ошибку API. Сохранение без проверки результата превращает конфликт в тихую потерю данных.

\n

Для обновления существующего элемента передавайте его ID в makeElementCode. Если код уже установлен и название просто изменилось, лучше оставить старое значение. Автоматический пересчёт нужен только при создании, при пустом поле или по отдельной подтверждённой команде. Это защищает опубликованные адреса от случайного изменения.

\n

Как проверить элемент после записи

\n

Сохранённая строка ещё не доказывает, что каталог откроет нужную карточку. Компонент может фильтровать по активности, разделу, сайту или дополнительному свойству. Проверка должна повторить путь чтения: найти элемент в том же инфоблоке по тому же CODE, затем открыть фактический URL.

\n

Если маршрут использует ЧПУ, адрес проходит через правила обработки URL. CODE может стать одной из переменных шаблона, но сам метод транслитерации не настраивает urlrewrite.php. Поэтому изменение генератора не исправит неверный SEF_FOLDER, порядок правил или фильтр компонента.

\n

Порядок действий

\n
  1. Зафиксировать, где используется код: фильтр инфоблока, шаблон ЧПУ, API, импорт или внешние ссылки.
  2. Определить область уникальности: инфоблок, раздел, сайт или вся витрина.
  3. Получить кандидата через CUtil::translit с явными параметрами языка, длины, регистра и замен.
  4. Сжать повторные дефисы, убрать дефисы по краям и обработать пустой результат.
  5. Проверить кандидат в выбранной области и исключить ID редактируемого элемента.
  6. При конфликте добавлять суффикс и проверять каждую новую строку тем же запросом.
  7. Сохранить код через API Bitrix, проверить ID и сообщение об ошибке, затем перечитать элемент.
  8. Собрать URL тем же шаблоном, которым пользуется витрина, и открыть его на тестовых данных.
  9. Для уже опубликованного кода отдельно решить судьбу старого адреса: редирект, история ссылок или запрет изменения.
\n

Отрицательный путь и границы решения

\n

Проверка существования перед записью не устраняет гонку. Два параллельных запроса могут увидеть свободный telefon-samsung и одновременно попытаться его сохранить. Для импорта и массового создания нужна защита на уровне базы, API или очереди. Если текущая схема Bitrix не даёт подходящего ограничения, обработчик должен уметь повторить выбор кандидата после ошибки записи. Нельзя выдавать предварительную проверку за гарантию.

\n

Суффикс решает технический конфликт, но может быть плохим бизнес-правилом. Для артикулов, интеграционных ключей и SEO-адресов иногда нужен стабильный код из внешнего идентификатора. Тогда транслитерация названия остаётся только запасным способом для новых записей.

\n

Перевод на другой язык может создать другой код. Если один инфоблок связан с несколькими сайтами и языками, язык передавайте явно и заранее решите, общий ли код у витрин. Не смешивайте локальные коды в одном поле без правила обратной совместимости.

\n

Параметры и доступные методы зависят от версии Bitrix. На новых версиях в классе CIBlockElement есть методы работы с символьными кодами, но их применение зависит от настройки инфоблока. Перед заменой старого вызова сравните версию ядра, настройки поля и фактический компонент каталога.

\n

Проверяемый критерий готовности

\n

Решение готово, если два одинаковых названия в выбранной области получают разные коды, повторное сохранение существующего элемента сохраняет его код, пустое название получает проверенный резервный код, а URL открывает именно созданный ID. Отдельный тест должен показать отрицательный путь при конфликте параллельных операций. Для опубликованной записи старый URL либо продолжает вести на элемент, либо его поведение зафиксировано и проверено.

\n

Проверяемые источники

\n" + "excerpt": "CUtil::translit создаёт только кандидата для CODE. Разбираем нормализацию, проверку уникальности, ограничение длины, обновление элемента и безопасное поведение при смене публичного адреса.", + "contentHtml": "

В тестовом каталоге редактор импортирует два элемента с названием «Телефон Samsung». Оба получают кандидат telefon-samsung, а затем один и тот же код сохраняется в инфоблоке. Страница товара то открывает первую найденную карточку, то отдаёт 404 после смены правил маршрута. При массовом импорте такая ошибка превращается в дубли адресов и ручное восстановление ссылок.

\n

Есть и второй симптом. Редактор исправляет опечатку в названии опубликованного товара, обработчик снова вызывает генератор, а старая ссылка перестаёт вести на эту запись. Первое предположение — проблема в транслитерации. Проверка показывает другую границу: CUtil::translit преобразует строку, но не знает занятые коды, область поиска и судьбу уже опубликованного URL.

\n

Поэтому разделим две задачи. При создании элемента нужно получить нормализованный кандидат, проверить его в том же контексте, где работает каталог, и только потом сохранить. При редактировании уже опубликованной записи нужно отдельно решить, разрешена ли смена CODE. Это не косметика: код одновременно становится данными элемента и частью публичного адреса.

\n

Что делает CUtil::translit

\n

CUtil::translit — статический метод старого API Bitrix с сигнатурой «строка, язык, параметры». Официальная документация описывает его как транслитерацию строки, а не как генератор уникального идентификатора. В параметрах можно явно задать максимальную длину, регистр, замену пробелов и прочих символов, а также удаление повторяющихся замен.

\n

Для русского названия в примере задаём язык ru, нижний регистр и дефис как замену. На входе «Телефон Samsung» получится читаемый кандидат. Но «Телефон Samsung» и «Телефон—Samsung» вполне могут привести к одному результату. Строка из знаков пунктуации может стать пустой, а ограничение max_len само по себе не добавит суффикс и не проверит его в базе.

\n

Есть ещё одна деталь для исторического примера. Исходная статья написана для legacy-классов Bitrix. В документации CIBlockElement::GetList отдельно отмечены изменения ключей и возможностей начиная с версии 18.6.200 модуля «Информационные блоки». Перед переносом кода в текущий проект нужно сверить версию модуля и фактический слой доступа; совпадение имени метода ещё не доказывает совпадение контракта.

\n
\"Поток
Транслитерация даёт кандидата. Область уникальности, лимит длины и правило для старого URL задаёт приложение.
\n

Где проверять уникальность

\n

Область уникальности определяется не названием поля, а путём чтения. Если компонент ищет элемент по IBLOCK_ID и CODE, проверяйте именно эту пару. Если публичный маршрут добавляет раздел, сайт или язык, разберите фильтр компонента и решите, может ли один элемент принадлежать нескольким разделам. Одинаковые коды допустимы только тогда, когда полный маршрут действительно различает записи.

\n

Для простого каталога безопасное правило выглядит так: код уникален внутри конкретного инфоблока. Это не универсальная настройка SEO, а выбранный контракт примера. Если проект строит глобальный URL без раздела, область может оказаться шире инфоблока. Если код нужен ещё для API или импорта, эти потребители тоже должны быть учтены до первой публикации.

\n

При редактировании фильтр должен исключать ID текущего элемента. Иначе запись с уже сохранённым кодом будет считаться конфликтом сама с собой, и каждое сохранение начнёт превращать item в item-2, затем в item-3. Для нового элемента ID равен нулю, поэтому исключение не скрывает существующие записи.

\n

Нормализация должна завершаться до проверки: сжать повторные дефисы, удалить дефисы по краям и задать резервный префикс для пустого результата. Суффикс нужно учитывать в том же лимите. Если max_len равен 80, база длиной 80 символов плюс -2 уже не укладывается в ограничение. Сначала оставьте место под суффикс, затем проверяйте готовый кандидат.

\n

Симптом → причина → проверка → действие

\n
СимптомПричинаПроверкаДействие
Две карточки имеют один CODEПроверили только транслитерациюПосчитать записи по той же области и кодуДобавлять суффикс до сохранения и исправить существующие конфликты
При редактировании код меняется самГенератор запускается для любого сохраненияСравнить старый код, новый код и режим операцииГенерировать только при создании, пустом поле или явной команде
Код пустой или начинается с дефисаПосле замены не осталось допустимых символовПроверить результат после сжатия и обрезкиВыбрать резервный префикс и проверить его тем же запросом
Суффикс вышел за лимит длиныЛимит применили до добавления -2Проверить длину каждого готового кандидатаОставлять место под суффикс до проверки уникальности
Проверка прошла, но запись конфликтует позжеДве операции выполняются параллельноПовторить создание одновременно и разобрать ошибку записиСериализовать операцию или повторить выбор после отказа
Старый адрес отвечает 404Опубликованный CODE изменили без миграцииОткрыть старый URL и проверить маршрутСохранить историю адресов, настроить редирект или запретить смену
\n

Учебная реализация

\n

Ниже — учебный вариант для legacy API. Функция codeExists оставлена отдельным адаптером, чтобы было видно её контракт: она ищет точный код в заданном инфоблоке и не считает конфликтом редактируемый элемент. Запрос на проверку не заменяет обработку ошибки записи.

\n
<?php\n\nfunction makeElementCode($name, $iblockId, $elementId = 0)\n{\n    $maxLength = 80;\n    $base = CUtil::translit((string)$name, 'ru', array(\n        'max_len' => $maxLength,\n        'change_case' => 'L',\n        'replace_space' => '-',\n        'replace_other' => '-',\n        'delete_repeat_replace' => true,\n    ));\n\n    $base = trim(preg_replace('/-+/', '-', $base), '-');\n    $base = $base !== '' ? $base : 'item';\n    $base = trim(substr($base, 0, $maxLength), '-');\n\n    for ($suffix = 1; $suffix <= 1000; $suffix++) {\n        $suffixText = $suffix === 1 ? '' : '-' . $suffix;\n        $stemLength = $maxLength - strlen($suffixText);\n        $stem = rtrim(substr($base, 0, $stemLength), '-');\n        $candidate = $stem . $suffixText;\n\n        if (!codeExists($iblockId, $candidate, $elementId)) {\n            return $candidate;\n        }\n    }\n\n    throw new RuntimeException('Не найден свободный CODE за 1000 попыток');\n}\n\nfunction codeExists($iblockId, $code, $elementId = 0)\n{\n    $filter = array(\n        'IBLOCK_ID' => (int)$iblockId,\n        '=CODE' => $code,\n        '!ID' => (int)$elementId,\n    );\n\n    $result = CIBlockElement::GetList(\n        array(),\n        $filter,\n        false,\n        array('nTopCount' => 1),\n        array('ID')\n    );\n\n    return (bool)$result->Fetch();\n}
\n

Вызов с $suffix = 1 сначала проверяет исходный кандидат. При занятом telefon-samsung проверяются telefon-samsung-2 и следующие значения. Перед каждой проверкой код уже укладывается в 80 символов. Ограничение в 1000 попыток — не гарантия свободного значения, а явный предел, после которого обработчик должен вернуть ошибку и привлечь оператора или отдельную стратегию импорта.

\n

В фильтре указаны тот же IBLOCK_ID, точное сравнение =CODE и исключение !ID. Если проект использует другой слой доступа, переносите не синтаксис целиком, а эти три условия. Сначала проверьте, что компонент и генератор считают область одинаково; иначе локально корректная функция всё равно даст конфликт на публичном маршруте.

\n

Сохраняем и обновляем элемент

\n

После генерации код передают в тот же массив полей, который используется для создания элемента. Метод CIBlockElement::Add возвращает ID при успехе и false при ошибке; текст причины доступен в LAST_ERROR. Поэтому результат нужно проверять, а не считать вызов успешным только потому, что запрос завершился без исключения.

\n
<?php\n\n$element = new CIBlockElement();\n$fields = array(\n    'IBLOCK_ID' => 12,\n    'NAME' => $name,\n    'CODE' => makeElementCode($name, 12),\n    'ACTIVE' => 'N',\n);\n\n$id = $element->Add($fields);\nif ($id === false) {\n    throw new RuntimeException($element->LAST_ERROR);\n}
\n

Этот фрагмент не решает гонку. Два параллельных процесса могут оба увидеть свободный код между GetList и Add. Для последовательного ручного ввода этого наблюдения достаточно, но массовый импорт должен иметь дополнительный контракт: блокировку или очередь, транзакционную стратегию, доступное проекту ограничение либо повторный выбор кандидата после ошибки записи. Какой вариант допустим, зависит от версии Bitrix и базы данных; предварительную проверку нельзя называть атомарной гарантией.

\n

Для существующей опубликованной записи не пересчитывайте код при каждом изменении названия. Если поле уже заполнено, оставьте его, а новый код применяйте только при создании, пустом значении или явном согласованном действии. При намеренной смене сначала сохраните старый адрес и правило перехода, затем измените запись и проверьте оба URL. Операция Update также должна проверять возвращённый результат и LAST_ERROR.

\n

Проверяем URL после записи

\n

Перечитать элемент по IBLOCK_ID и CODE полезно, но этого недостаточно. Каталог может дополнительно фильтровать по активности, сайту, разделу или правам. Финальная проверка должна повторить путь чтения: получить ожидаемый ID, собрать фактический адрес тем же шаблоном и открыть его на тестовых данных.

\n

ЧПУ в Bitrix обрабатываются отдельными правилами сайта в urlrewrite.php. В режиме компонента с ЧПУ участвуют, среди прочего, SEF_MODE и SEF_FOLDER; порядок правил и их условие тоже влияют на результат. Поэтому смена функции транслитерации не исправит неправильную папку или более раннее совпадающее правило.

\n

Для опубликованного адреса заранее выберите одно из трёх состояний: код стабилен и не меняется вместе с названием; история старых кодов ведёт на текущую запись; старый путь намеренно закрывается с согласованным ответом. Нельзя одновременно обещать стабильный URL и автоматически строить его из редактируемого имени.

\n

Порядок проверки

\n
  1. Зафиксировать всех потребителей CODE: компонент, API, импорт, sitemap и внешние ссылки.
  2. Определить область уникальности и проверить, совпадает ли она с фильтром публичного чтения.
  3. Получить кандидата через CUtil::translit с явными языком, регистром, заменами и длиной.
  4. Сжать повторные дефисы, убрать крайние дефисы и обработать пустой результат.
  5. Оставить место под суффикс, проверить готовый кандидат и исключить ID текущего элемента.
  6. При конфликте перебирать значения до явного лимита и не скрывать исчерпание попыток.
  7. Передать код в CIBlockElement::Add или Update, проверить ID или LAST_ERROR и перечитать запись.
  8. Открыть URL на тестовом элементе и отдельно проверить старый адрес опубликованной записи.
  9. Для параллельного импорта воспроизвести гонку и проверить согласованный путь повторной записи.
\n

Отрицательные случаи и границы

\n

Тест с одинаковыми названиями должен показать разные коды. Тест с изменением пробелов и знаков — одинакового кандидата до суффикса. Тест с пустым результатом — резервный item, прошедший ту же проверку. Тест с длинным именем и занятым первым кандидатом — готовый код не длиннее 80 символов. Повторное сохранение существующей записи должно сохранить её код.

\n

Отдельно проверьте связь с разделами. Если элемент может быть привязан к нескольким разделам, правило «уникально внутри раздела» может быть недостаточным для URL. Сначала зафиксируйте полный маршрут, затем выбирайте область проверки. Нельзя выводить область уникальности из одного поля формы.

\n

Суффикс подходит для технического разрешения совпадения, но не всегда подходит для бизнес-идентификатора. Для артикула, ключа поставщика или интеграции стабильнее использовать внешний идентификатор, а транслитерацию имени оставить запасным вариантом. Язык тоже задавайте явно: смена языка может создать другой код и новый публичный адрес.

\n

Решение готово, когда два одинаковых названия в выбранной области получают разные значения, существующий элемент не конфликтует сам с собой, длина каждого готового кода укладывается в контракт, ошибка параллельной записи не скрывается, а URL открывает именно ожидаемый ID. Для уже опубликованной записи поведение старого адреса должно быть проверено отдельно.

\n

Проверяемые источники

\n" }