DarkRiDDeR10 мин

Bitrix API. Символьный код: как не получить два одинаковых адреса

BitrixPHPПрактика

Добавляем товар в Bitrix и берём CODE из названия. На тесте всё выглядит хорошо. Потом менеджер заводит «Кофе Classic 250 г» второй раз — с запятой или лишним пробелом. После транслитерации получается тот же адрес, а ссылка из каталога ведёт к записи, которую никто не собирался открывать. Главный вопрос этой заметки простой: как получить читаемый код и не принять совпадение за успех? Цена ошибки — неверная карточка, потерянная ссылка и ручная чистка дублей.

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

Что даёт системный транслит

В Bitrix для этой задачи есть CUtil::translit. Метод принимает строку, язык и набор параметров. В нём можно задать регистр, замену пробелов и прочих символов, ограничение длины, а также удаление повторяющихся замен. Для адреса каталога мне удобнее дефис и нижний регистр: в результате не приходится отдельно объяснять, почему одни карточки имеют подчёркивание, а другие — дефис.

Но нормализация не делает два разных названия разными. «Кофе Classic 250 г», «Кофе Classic-250 г» и «Кофе Classic 250 г» вполне могут прийти к одному кандидату. Это не ошибка CUtil::translit. Функция честно выполнила свою работу: привела вход к одному виду. Сравнивать и разрешать конфликт должен вызывающий код.

Схема построения символьного кода: имя, транслитерация, проверка через GetList, суффикс или создание элемента
Транслит формирует кандидата. Решение о свободном коде появляется только после проверки в нужном инфоблоке.

Минимальный контракт

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

ШагЧто считаем результатомЧто проверяем
ИмяЕсть непустое названиеНе передаём в транслит пустую строку и не придумываем код из 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);
}

// Публикуем только после проверки обязательных данных и ссылки.

Последовательность проверки

  1. Взять два названия, которые различаются только знаками и пробелами, и получить для них кандидаты.
  2. Создать первый элемент на тестовом инфоблоке с исходным кандидатом.
  3. Запустить функцию для второго имени и убедиться, что она вернула суффикс, а не прежний код.
  4. Прочитать оба элемента через CIBlockElement::GetList с тем же IBLOCK_ID.
  5. Открыть детальные страницы и сверить 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 при ошибке