Добавляем товар в Bitrix и берём CODE из названия. На тесте всё выглядит хорошо. Потом менеджер заводит «Кофе Classic 250 г» второй раз — с запятой или лишним пробелом. После транслитерации получается тот же адрес, а ссылка из каталога ведёт к записи, которую никто не собирался открывать. Главный вопрос этой заметки простой: как получить читаемый код и не принять совпадение за успех? Цена ошибки — неверная карточка, потерянная ссылка и ручная чистка дублей.
Сначала важная оговорка. Транслитерация не выбирает свободный URL. Она преобразует строку по заданным правилам. Уникальность — уже правило конкретного инфоблока и конкретного способа создания элементов. Поэтому проверяем не «красиво ли выглядит код», а есть ли другой элемент с тем же значением там, где его будет искать каталог.
Что даёт системный транслит
В Bitrix для этой задачи есть CUtil::translit. Метод принимает строку, язык и набор параметров. В нём можно задать регистр, замену пробелов и прочих символов, ограничение длины, а также удаление повторяющихся замен. Для адреса каталога мне удобнее дефис и нижний регистр: в результате не приходится отдельно объяснять, почему одни карточки имеют подчёркивание, а другие — дефис.
Но нормализация не делает два разных названия разными. «Кофе Classic 250 г», «Кофе Classic-250 г» и «Кофе Classic 250 г» вполне могут прийти к одному кандидату. Это не ошибка CUtil::translit. Функция честно выполнила свою работу: привела вход к одному виду. Сравнивать и разрешать конфликт должен вызывающий код.
Минимальный контракт
Для одного каталога достаточно договориться о нескольких вещах до написания функции. Они не привязаны к шаблону страницы и не требуют большой переделки. Зато по ним сразу видно, почему повторный импорт изменил адрес или почему карточка попала не в тот раздел.
| Шаг | Что считаем результатом | Что проверяем |
|---|---|---|
| Имя | Есть непустое название | Не передаём в транслит пустую строку и не придумываем код из ID молча |
| Нормализация | Один предсказуемый кандидат | Регистр, дефис, длина и повторяющиеся разделители заданы явно |
| Поиск | Нет элемента с тем же CODE | Ищем внутри конкретного IBLOCK_ID, а не по всему сайту |
| Сохранение | Метод Add вернул ID | При ошибке сохраняем LAST_ERROR и исходное имя |
| Проверка ссылки | Каталог находит именно эту запись | Сверяем URL-шаблон и фильтр детального компонента |
Воспроизводимый пример
Ниже функция для последовательного добавления из админки или небольшого импорта. Число 50 здесь не ограничение Bitrix, а мой предел для понятной ошибки: если за пятьдесят попыток не найден свободный вариант, лучше остановиться и посмотреть на входные данные. В реальном проекте ID инфоблока и правило суффикса стоит вынести в конфигурацию.
<?php
function getFreeElementCode($iblockId, $name)
{
$base = CUtil::translit(trim($name), "ru", array(
"max_len" => 90,
"change_case" => "L",
"replace_space" => "-",
"replace_other" => "-",
"delete_repeat_replace" => true,
));
$base = trim($base, "-");
if ($base === "") {
throw new InvalidArgumentException("Не удалось получить CODE из NAME");
}
for ($number = 1; $number <= 50; $number++) {
$candidate = $number === 1 ? $base : $base . "-" . $number;
$result = CIBlockElement::GetList(
array(),
array("IBLOCK_ID" => (int)$iblockId, "=CODE" => $candidate),
false,
array("nTopCount" => 1),
array("ID")
);
if (!$result->Fetch()) {
return $candidate;
}
}
throw new RuntimeException("Не найден свободный CODE за 50 попыток");
}
Знак = в фильтре делает намерение явным: мы ищем конкретный код, а не похожую строку. В выборку достаточно взять ID; имя, картинка и свойства для решения о занятости не нужны. Это маленькая деталь, но она не даёт диагностическому запросу превращаться в выборку всего каталога.
Сохраняем код вместе с элементом
После проверки не нужно делать отдельный Update ради CODE. Документация CIBlockElement::Add допускает поле CODE в массиве полей. Добавляю его в тот же вызов и обязательно разбираю ошибку. Возвращённый ID доказывает запись, но ещё не доказывает, что путь компонента совпадает с проектным URL.
<?php
$element = new CIBlockElement();
$id = $element->Add(array(
"IBLOCK_ID" => 12,
"NAME" => $name,
"CODE" => getFreeElementCode(12, $name),
"ACTIVE" => "N",
));
if ($id === false) {
throw new RuntimeException($element->LAST_ERROR);
}
// Публикуем только после проверки обязательных данных и ссылки.
Последовательность проверки
- Взять два названия, которые различаются только знаками и пробелами, и получить для них кандидаты.
- Создать первый элемент на тестовом инфоблоке с исходным кандидатом.
- Запустить функцию для второго имени и убедиться, что она вернула суффикс, а не прежний код.
- Прочитать оба элемента через
CIBlockElement::GetListс тем жеIBLOCK_ID. - Открыть детальные страницы и сверить ID в шаблоне или временном логе. Так мы проверяем не только данные, но и используемый компонентом маршрут.
Граница этого решения
Проверка «сначала GetList, потом Add» не является атомарной. Два параллельных воркера могут одновременно увидеть свободный код и попытаться сохранить одинаковое значение. Для ручного ввода и последовательного импорта этого обычно достаточно. Для параллельной синхронизации нужен отдельный проектный механизм: очередь, блокировка или код, связанный со стабильным внешним идентификатором. Какой именно — зависит от версии Bitrix, базы и требований к существующим URL.
Не стоит лечить эту задачу случайным числом в каждом коде. Такой адрес перестаёт быть повторяемым при повторном импорте, а диагностика становится сложнее. Если данные поставщика имеют стабильный артикул, полезно заранее решить, будет ли он участвовать в CODE или останется отдельным свойством. Главное — зафиксировать правило до публикации первой тысячи карточек.
Итог
Символьный код начинается с CUtil::translit, но не заканчивается на нём. Сначала делаем читаемого кандидата, затем проверяем его в нужном инфоблоке, сохраняем результат вместе с элементом и отдельно открываем ссылку. Такой порядок не решает гонку параллельного импорта, зато честно показывает её границу и избавляет от тихих совпадений в обычной работе.
Проверяемые источники
- Bitrix: CUtil::translit — параметры нормализации строки: регистр, замена пробелов и повторяющихся разделителей
- Bitrix: CIBlockElement::GetList — выборка элементов по фильтрам IBLOCK_ID, CODE, ACTIVE и с заданным порядком
- Bitrix: CIBlockElement::Add — создание элемента, поле CODE, возвращаемый ID и LAST_ERROR при ошибке