{ "index": 351, "slug": "editorial-2018-04-practice-bitrix-slugs", "title": "Bitrix CODE без дублей: как проверить символьный код до публикации", "excerpt": "Транслитерация создаёт только кандидата. Разбираем, как проверить CODE в нужном инфоблоке, сохранить его вместе с элементом и найти ошибку, если URL открывает не ту карточку.", "contentHtml": "
Карточка товара открывается по адресу /catalog/kofe/classic-250-g/. Менеджер добавляет второй товар с тем же названием, но с другой расстановкой пробелов. После нормализации оба элемента получают одного кандидата CODE. Если детальный компонент ищет запись только по этому полю, запрос становится неоднозначным: результат зависит от его фильтра и сортировки. Цена ошибки — неверная карточка в заказе, сломанные ссылки из рекламы и ручная чистка дублей.
Проблема возникает не потому, что Bitrix плохо транслитерирует строку. CUtil::translit решает одну задачу: приводит текст к заданному виду. Уникальность решает другой код. Он должен проверить кандидата в конкретном информационном блоке (IBLOCK_ID), из которого компонент строит карточку, и только потом сохранить значение. Если эти операции смешать, красивый адрес создаёт ложное ощущение готовности.
Возьмём три названия: «Кофе Classic 250 г», «Кофе Classic-250 г» и «Кофе Classic 250 г». При одинаковых параметрах нормализации они могут дать одного кандидата: kofe-classic-250-g. Для транслитератора это правильный результат. Он не знает, что в инфоблоке уже есть элемент с таким кодом.
Свободный код — это результат последовательности, а не одной функции:
\nCODE;Последний шаг нужен обязательно. Уникальный CODE не спасает, если детальный компонент фильтрует по другому инфоблоку, требует активность элемента или читает значение из другой переменной URL.
Сначала задайте границы правила. В примере итоговый CODE не длиннее 90 символов и уникален внутри одного IBLOCK_ID. Базовый кандидат ограничен 86 символами: ещё четыре остаются для суффикса от -2 до -50. Дефис разделяет слова, регистр — нижний. Это не универсальные значения, а учебная конфигурация для последовательного импорта.
Для нормализации используем CUtil::translit. Метод принимает исходную строку, язык и параметры замены пробелов, прочих символов, регистра и длины. После вызова нужно убрать дефисы по краям и проверить пустой результат. Имя из одних знаков или символов, которые правило не сохраняет, не даёт пригодного кода.
Проверка занятости должна повторить область поиска каталога. Если компонент работает с инфоблоком 12, фильтр по всем инфоблокам даст лишние конфликты. Если компонент ищет только активные элементы, это тоже часть проектного правила. Важно заранее решить, может ли архивный элемент удерживать старый адрес. В большинстве каталогов да: иначе повторная публикация способна занять прежнюю ссылку.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Две карточки имеют один адрес | После транслитерации не проверили занятость | Сделать выборку по IBLOCK_ID и =CODE | Ввести детерминированный суффикс или остановить импорт |
| Код свободен, но страница даёт 404 | Маршрут и фильтр компонента используют разные правила | Сверить SEF_FOLDER, шаблон и фильтр элемента | Исправить маршрут или значение, не менять код вслепую |
| Проверка находит чужую запись | Поиск идёт не в том инфоблоке | Вывести фактический IBLOCK_ID страницы и импорта | Сузить фильтр до области ответственности каталога |
| Повторный импорт меняет URL | Суффикс зависит от случайного счётчика или порядка загрузки | Запустить импорт дважды на одинаковом входе | Связать правило с постоянным внешним идентификатором |
| Добавление завершилось без понятной ошибки | Не проверили результат Add | Проверить возвращаемый ID и LAST_ERROR | Не публиковать элемент до разбора ошибки |
Функция возвращает первый свободный кандидат. Она подходит для ручного ввода и последовательного учебного импорта. Число 50 ограничивает только этот пример. Оно не является лимитом Bitrix и не доказывает, что код можно безопасно создавать при параллельных запросах.
\n<?php\n\nif (!CModule::IncludeModule('iblock')) {\n throw new RuntimeException('Модуль iblock недоступен');\n}\n\nfunction getFreeElementCode($iblockId, $name)\n{\n $iblockId = (int) $iblockId;\n if ($iblockId <= 0) {\n throw new InvalidArgumentException('Некорректный IBLOCK_ID');\n }\n\n $base = CUtil::translit(trim($name), 'ru', array(\n 'max_len' => 86,\n 'change_case' => 'L',\n 'replace_space' => '-',\n 'replace_other' => '-',\n 'delete_repeat_replace' => true,\n ));\n\n $base = trim($base, '-');\n if ($base === '') {\n throw new InvalidArgumentException('NAME не дал пригодный CODE');\n }\n\n for ($number = 1; $number <= 50; $number++) {\n $candidate = $number === 1 ? $base : $base . '-' . $number;\n $result = CIBlockElement::GetList(\n array(),\n array('IBLOCK_ID' => (int) $iblockId, '=CODE' => $candidate),\n false,\n array('nTopCount' => 1),\n array('ID')\n );\n\n if (!$result->Fetch()) {\n return $candidate;\n }\n }\n\n throw new RuntimeException('Свободный CODE не найден');\n}\nФильтр содержит IBLOCK_ID и точное условие =CODE. В выборку попадает только ID, потому что для проверки занятости остальные поля не нужны. nTopCount ограничивает результат одной найденной записью. Так запрос отвечает на конкретный вопрос: занят ли кандидат?
Теперь выбранный код передаём в CIBlockElement::Add. Не создавайте элемент, а затем отдельным Update дописывайте код: между двумя операциями появляется окно, в котором запись уже видна без нужного адреса. В учебном варианте элемент создаём неактивным и сначала сверяем его поля. URL проверяем на тестовой витрине после активации по проектному процессу, затем переводим запись в рабочее состояние.
<?php\n\n$iblockId = 12;\n$element = new CIBlockElement();\n$code = getFreeElementCode($iblockId, $name);\n$id = $element->Add(array(\n 'IBLOCK_ID' => $iblockId,\n 'NAME' => $name,\n 'CODE' => $code,\n 'ACTIVE' => 'N',\n));\n\nif ($id === false) {\n throw new RuntimeException($element->LAST_ERROR);\n}\n\n// Учебный пример: публикация выполняется после отдельной проверки URL.\nВозвращённый ID подтверждает, что метод добавил запись. Он не подтверждает правильность маршрута. Откройте страницу с тем URL, который собирает реальный шаблон каталога, и убедитесь, что компонент вернул именно этот ID. Логировать следует идентификатор элемента, инфоблок, код и результат разбора URL. Не подменяйте эту проверку просмотром адресной строки.
\nПроверка «сначала GetList, потом Add» предварительная и не атомарная. Два воркера могут одновременно увидеть свободный kofe-classic-250-g, оба получить его и оба начать сохранение. Последовательный пример этого не обнаружит, потому что в нём нет конкурирующего состояния.
При параллельной синхронизации не маскируйте проблему случайным числом. Такой код может измениться при повторной загрузке и не поможет сопоставить товар с источником. Выберите механизм на уровне проекта: последовательную очередь, блокировку, уникальное ограничение базы, если оно поддерживается схемой, или код на основе стабильного артикула. Если предварительный поиск уже прошёл, но Add вернул false, сохраните LAST_ERROR и решите, можно ли безопасно повторить операцию с новым кандидатом. Перед выбором проверьте версию Bitrix, тип базы, существующие URL и допустимость их изменения.
Есть и другой отрицательный путь. Если имя меняется, а CODE строится заново при каждом обновлении, адрес товара меняется вместе с названием. Для публичного каталога это ломает внешние ссылки. Обычно код фиксируют при создании, а новое название хранят в NAME. Если адрес всё же нужно изменить, задайте явное правило перенаправления и проверьте старую ссылку отдельно.
CIBlockElement::GetList с тем же IBLOCK_ID, который использует каталог.CODE в CIBlockElement::Add. Обработайте false, LAST_ERROR и возвращённый ID.GetList; затем на тестовой витрине активируйте его по проектному процессу, откройте детальный URL и сверьте ID, инфоблок и код в диагностике.Код выше показывает выбор кандидата для нового элемента, но не реализует идемпотентность импорта. Для повторной загрузки сначала найдите элемент по стабильному внешнему идентификатору, а затем не пересчитывайте его CODE из NAME. Материал считается готовым к применению, если два похожих названия не получают один активный адрес, повторная загрузка с тем же внешним идентификатором сохраняет тот же код, а детальная страница возвращает ожидаемый ID. Для параллельного сценария нужен отдельный проверяемый результат: система либо предотвращает конфликт, либо отклоняет одну запись с диагностируемой ошибкой. Ответ «вроде открылось» критерием не является.
Пример использует старый процедурный API инфоблоков, потому что он показывает механизм исходной задачи. Имена методов, доступность параметров и поведение обработчиков нужно сверить с версией модуля в конкретном проекте: документация GetList отдельно перечисляет изменения, начиная с версии 18.6.200 модуля инфоблоков. Код из статьи — учебная основа, а не готовый импортёр.
Уникальность CODE внутри инфоблока не делает URL глобально уникальным. Конфликт может появиться с разделом, другим типом страницы, языковой витриной или ручным маршрутом. Поэтому финальная проверка должна идти через реальный URL-компонент, а не только через таблицу элементов.