{ "index": 362, "slug": "bitrix-api-функция-для-генерации-кода-элемент", "title": "Bitrix API: как сгенерировать символьный код элемента и не сломать URL", "excerpt": "CUtil::translit создаёт только кандидата для CODE. Разбираем нормализацию, проверку уникальности, ограничение длины, обновление элемента и безопасное поведение при смене публичного адреса.", "contentHtml": "
В тестовом каталоге редактор импортирует два элемента с названием «Телефон Samsung». Оба получают кандидат telefon-samsung, а затем один и тот же код сохраняется в инфоблоке. Страница товара то открывает первую найденную карточку, то отдаёт 404 после смены правил маршрута. При массовом импорте такая ошибка превращается в дубли адресов и ручное восстановление ссылок.
Есть и второй симптом. Редактор исправляет опечатку в названии опубликованного товара, обработчик снова вызывает генератор, а старая ссылка перестаёт вести на эту запись. Первое предположение — проблема в транслитерации. Проверка показывает другую границу: CUtil::translit преобразует строку, но не знает занятые коды, область поиска и судьбу уже опубликованного URL.
Поэтому разделим две задачи. При создании элемента нужно получить нормализованный кандидат, проверить его в том же контексте, где работает каталог, и только потом сохранить. При редактировании уже опубликованной записи нужно отдельно решить, разрешена ли смена CODE. Это не косметика: код одновременно становится данными элемента и частью публичного адреса.
CUtil::translit — статический метод старого API Bitrix с сигнатурой «строка, язык, параметры». Официальная документация описывает его как транслитерацию строки, а не как генератор уникального идентификатора. В параметрах можно явно задать максимальную длину, регистр, замену пробелов и прочих символов, а также удаление повторяющихся замен.
Для русского названия в примере задаём язык ru, нижний регистр и дефис как замену. На входе «Телефон Samsung» получится читаемый кандидат. Но «Телефон Samsung» и «Телефон—Samsung» вполне могут привести к одному результату. Строка из знаков пунктуации может стать пустой, а ограничение max_len само по себе не добавит суффикс и не проверит его в базе.
Есть ещё одна деталь для исторического примера. Исходная статья написана для legacy-классов Bitrix. В документации CIBlockElement::GetList отдельно отмечены изменения ключей и возможностей начиная с версии 18.6.200 модуля «Информационные блоки». Перед переносом кода в текущий проект нужно сверить версию модуля и фактический слой доступа; совпадение имени метода ещё не доказывает совпадение контракта.
Область уникальности определяется не названием поля, а путём чтения. Если компонент ищет элемент по IBLOCK_ID и CODE, проверяйте именно эту пару. Если публичный маршрут добавляет раздел, сайт или язык, разберите фильтр компонента и решите, может ли один элемент принадлежать нескольким разделам. Одинаковые коды допустимы только тогда, когда полный маршрут действительно различает записи.
Для простого каталога безопасное правило выглядит так: код уникален внутри конкретного инфоблока. Это не универсальная настройка SEO, а выбранный контракт примера. Если проект строит глобальный URL без раздела, область может оказаться шире инфоблока. Если код нужен ещё для API или импорта, эти потребители тоже должны быть учтены до первой публикации.
\nПри редактировании фильтр должен исключать ID текущего элемента. Иначе запись с уже сохранённым кодом будет считаться конфликтом сама с собой, и каждое сохранение начнёт превращать item в item-2, затем в item-3. Для нового элемента ID равен нулю, поэтому исключение не скрывает существующие записи.
Нормализация должна завершаться до проверки: сжать повторные дефисы, удалить дефисы по краям и задать резервный префикс для пустого результата. Суффикс нужно учитывать в том же лимите. Если max_len равен 80, база длиной 80 символов плюс -2 уже не укладывается в ограничение. Сначала оставьте место под суффикс, затем проверяйте готовый кандидат.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
Две карточки имеют один CODE | Проверили только транслитерацию | Посчитать записи по той же области и коду | Добавлять суффикс до сохранения и исправить существующие конфликты |
| При редактировании код меняется сам | Генератор запускается для любого сохранения | Сравнить старый код, новый код и режим операции | Генерировать только при создании, пустом поле или явной команде |
| Код пустой или начинается с дефиса | После замены не осталось допустимых символов | Проверить результат после сжатия и обрезки | Выбрать резервный префикс и проверить его тем же запросом |
| Суффикс вышел за лимит длины | Лимит применили до добавления -2 | Проверить длину каждого готового кандидата | Оставлять место под суффикс до проверки уникальности |
| Проверка прошла, но запись конфликтует позже | Две операции выполняются параллельно | Повторить создание одновременно и разобрать ошибку записи | Сериализовать операцию или повторить выбор после отказа |
| Старый адрес отвечает 404 | Опубликованный CODE изменили без миграции | Открыть старый URL и проверить маршрут | Сохранить историю адресов, настроить редирект или запретить смену |
Ниже — учебный вариант для legacy API. Функция codeExists оставлена отдельным адаптером, чтобы было видно её контракт: она ищет точный код в заданном инфоблоке и не считает конфликтом редактируемый элемент. Запрос на проверку не заменяет обработку ошибки записи.
<?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 попыток — не гарантия свободного значения, а явный предел, после которого обработчик должен вернуть ошибку и привлечь оператора или отдельную стратегию импорта.
В фильтре указаны тот же IBLOCK_ID, точное сравнение =CODE и исключение !ID. Если проект использует другой слой доступа, переносите не синтаксис целиком, а эти три условия. Сначала проверьте, что компонент и генератор считают область одинаково; иначе локально корректная функция всё равно даст конфликт на публичном маршруте.
После генерации код передают в тот же массив полей, который используется для создания элемента. Метод CIBlockElement::Add возвращает ID при успехе и false при ошибке; текст причины доступен в LAST_ERROR. Поэтому результат нужно проверять, а не считать вызов успешным только потому, что запрос завершился без исключения.
<?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 и базы данных; предварительную проверку нельзя называть атомарной гарантией.
Для существующей опубликованной записи не пересчитывайте код при каждом изменении названия. Если поле уже заполнено, оставьте его, а новый код применяйте только при создании, пустом значении или явном согласованном действии. При намеренной смене сначала сохраните старый адрес и правило перехода, затем измените запись и проверьте оба URL. Операция Update также должна проверять возвращённый результат и LAST_ERROR.
Перечитать элемент по IBLOCK_ID и CODE полезно, но этого недостаточно. Каталог может дополнительно фильтровать по активности, сайту, разделу или правам. Финальная проверка должна повторить путь чтения: получить ожидаемый ID, собрать фактический адрес тем же шаблоном и открыть его на тестовых данных.
ЧПУ в Bitrix обрабатываются отдельными правилами сайта в urlrewrite.php. В режиме компонента с ЧПУ участвуют, среди прочего, SEF_MODE и SEF_FOLDER; порядок правил и их условие тоже влияют на результат. Поэтому смена функции транслитерации не исправит неправильную папку или более раннее совпадающее правило.
Для опубликованного адреса заранее выберите одно из трёх состояний: код стабилен и не меняется вместе с названием; история старых кодов ведёт на текущую запись; старый путь намеренно закрывается с согласованным ответом. Нельзя одновременно обещать стабильный URL и автоматически строить его из редактируемого имени.
\nCODE: компонент, API, импорт, sitemap и внешние ссылки.CUtil::translit с явными языком, регистром, заменами и длиной.CIBlockElement::Add или Update, проверить ID или LAST_ERROR и перечитать запись.Тест с одинаковыми названиями должен показать разные коды. Тест с изменением пробелов и знаков — одинакового кандидата до суффикса. Тест с пустым результатом — резервный item, прошедший ту же проверку. Тест с длинным именем и занятым первым кандидатом — готовый код не длиннее 80 символов. Повторное сохранение существующей записи должно сохранить её код.
Отдельно проверьте связь с разделами. Если элемент может быть привязан к нескольким разделам, правило «уникально внутри раздела» может быть недостаточным для URL. Сначала зафиксируйте полный маршрут, затем выбирайте область проверки. Нельзя выводить область уникальности из одного поля формы.
\nСуффикс подходит для технического разрешения совпадения, но не всегда подходит для бизнес-идентификатора. Для артикула, ключа поставщика или интеграции стабильнее использовать внешний идентификатор, а транслитерацию имени оставить запасным вариантом. Язык тоже задавайте явно: смена языка может создать другой код и новый публичный адрес.
\nРешение готово, когда два одинаковых названия в выбранной области получают разные значения, существующий элемент не конфликтует сам с собой, длина каждого готового кода укладывается в контракт, ошибка параллельной записи не скрывается, а URL открывает именно ожидаемый ID. Для уже опубликованной записи поведение старого адреса должно быть проверено отдельно.
\nIBLOCK_ID, CODE, ID и версияционные оговорки.CODE, возвращаемый ID и LAST_ERROR.urlrewrite.php, правила URL и параметры ЧПУ.