8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"index": 351,
|
||
"slug": "editorial-2018-04-practice-bitrix-slugs",
|
||
"title": "Bitrix CODE без дублей: как проверить символьный код до публикации",
|
||
"excerpt": "Транслитерация создаёт только кандидата. Разбираем, как проверить CODE в нужном инфоблоке, сохранить его вместе с элементом и найти ошибку, если URL открывает не ту карточку.",
|
||
"contentHtml": "<p>Карточка товара открывается по адресу <code>/catalog/kofe/classic-250-g/</code>. Менеджер добавляет второй товар с тем же названием, но с другой расстановкой пробелов. Новый элемент получает тот же <code>CODE</code>. В каталоге одна ссылка начинает вести к первой записи, а вторую приходится искать по ID. Цена ошибки — неверная карточка в заказе, сломанные ссылки из рекламы и ручная чистка дублей.</p>\n<p>Проблема возникает не потому, что Bitrix плохо транслитерирует строку. <code>CUtil::translit</code> решает одну задачу: приводит текст к заданному виду. Уникальность решает другой код. Он должен проверить кандидата в том инфоблоке, из которого компонент строит карточку, и только потом сохранить значение. Если эти операции смешать, красивый адрес создаёт ложное ощущение готовности.</p>\n<h2>Тезис: транслит не гарантирует свободный адрес</h2>\n<p>Возьмём три названия: «Кофе Classic 250 г», «Кофе Classic-250 г» и «Кофе Classic 250 г». При одинаковых параметрах нормализации они могут дать одного кандидата: <code>kofe-classic-250-g</code>. Для транслитератора это правильный результат. Он не знает, что в инфоблоке уже есть элемент с таким кодом.</p>\n<p>Свободный код — это результат последовательности, а не одной функции:</p>\n<ol><li>из непустого имени получают базовый кандидат;</li><li>в конкретном инфоблоке ищут элемент с таким <code>CODE</code>;</li><li>при совпадении получают следующий кандидат по зафиксированному правилу;</li><li>сохраняют выбранный код в том же вызове, который создаёт элемент;</li><li>проверяют, что URL-компонент использует тот же инфоблок и тот же код.</li></ol>\n<p>Последний шаг нужен обязательно. Уникальный <code>CODE</code> не спасает, если детальный компонент фильтрует по другому инфоблоку, требует активность элемента или читает значение из другой переменной URL.</p>\n<figure><img src=\"/assets/editorial/2018/bitrix-slug-build-2018.svg\" alt=\"Построение символьного кода Bitrix: имя, транслитерация, проверка GetList, суффикс и сохранение\" /><figcaption>Транслитерация формирует кандидата. Свободным его делает только проверка в том инфоблоке, где каталог ищет элемент.</figcaption></figure>\n<h2>Механизм проверки</h2>\n<p>Сначала задайте границы правила. В примере ниже код должен быть уникальным внутри одного <code>IBLOCK_ID</code>. Дефис разделяет слова. Регистр — нижний. Длина ограничена 90 символами. При конфликте добавляется суффикс <code>-2</code>, затем <code>-3</code>. Это не универсальные значения, а учебная конфигурация для последовательного импорта.</p>\n<p>Для нормализации используем <code>CUtil::translit</code>. Метод принимает исходную строку, язык и параметры замены пробелов, прочих символов, регистра и длины. После вызова нужно убрать дефисы по краям и проверить пустой результат. Имя из одних знаков или символов, которые правило не сохраняет, не даёт пригодного кода.</p>\n<p>Проверка занятости должна повторить область поиска каталога. Если компонент работает с инфоблоком 12, фильтр по всем инфоблокам даст лишние конфликты. Если компонент ищет только активные элементы, это тоже часть проектного правила. Важно заранее решить, может ли архивный элемент удерживать старый адрес. В большинстве каталогов да: иначе повторная публикация способна занять прежнюю ссылку.</p>\n<table><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Две карточки имеют один адрес</td><td>После транслитерации не проверили занятость</td><td>Сделать выборку по <code>IBLOCK_ID</code> и <code>=CODE</code></td><td>Ввести детерминированный суффикс или остановить импорт</td></tr><tr><td>Код свободен, но страница даёт 404</td><td>Маршрут и фильтр компонента используют разные правила</td><td>Сверить <code>SEF_FOLDER</code>, шаблон и фильтр элемента</td><td>Исправить маршрут или значение, не менять код вслепую</td></tr><tr><td>Проверка находит чужую запись</td><td>Поиск идёт не в том инфоблоке</td><td>Вывести фактический <code>IBLOCK_ID</code> страницы и импорта</td><td>Сузить фильтр до области ответственности каталога</td></tr><tr><td>Повторный импорт меняет URL</td><td>Суффикс зависит от случайного счётчика или порядка загрузки</td><td>Запустить импорт дважды на одинаковом входе</td><td>Связать правило с постоянным внешним идентификатором</td></tr><tr><td>Добавление завершилось без понятной ошибки</td><td>Не проверили результат <code>Add</code></td><td>Проверить возвращаемый ID и <code>LAST_ERROR</code></td><td>Не публиковать элемент до разбора ошибки</td></tr></tbody></table>\n<h2>Учебный пример для последовательного добавления</h2>\n<p>Функция возвращает первый свободный кандидат. Она подходит для ручного ввода и последовательного учебного импорта. Число 50 ограничивает только этот пример. Оно не является лимитом Bitrix и не доказывает, что код можно безопасно создавать при параллельных запросах.</p>\n<pre><code><?php\n\nfunction getFreeElementCode($iblockId, $name)\n{\n $base = CUtil::translit(trim($name), 'ru', array(\n 'max_len' => 90,\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}</code></pre>\n<p>Фильтр содержит <code>IBLOCK_ID</code> и точное условие <code>=CODE</code>. В выборку попадает только <code>ID</code>, потому что для проверки занятости остальные поля не нужны. <code>nTopCount</code> ограничивает результат одной найденной записью. Так запрос отвечает на конкретный вопрос: занят ли кандидат?</p>\n<p>Теперь выбранный код передаём в <code>CIBlockElement::Add</code>. Не создавайте элемент, а затем отдельным <code>Update</code> дописывайте код. Между двумя операциями появляется окно, в котором запись уже видна без нужного адреса. В учебном варианте элемент создаём неактивным, проверяем ссылку и только затем переводим в рабочее состояние проектным способом.</p>\n<pre><code><?php\n\n$element = new CIBlockElement();\n$code = getFreeElementCode(12, $name);\n$id = $element->Add(array(\n 'IBLOCK_ID' => 12,\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.</code></pre>\n<p>Возвращённый ID подтверждает, что метод добавил запись. Он не подтверждает правильность маршрута. Откройте страницу с тем URL, который собирает реальный шаблон каталога, и убедитесь, что компонент вернул именно этот ID. Логировать следует идентификатор элемента, инфоблок, код и результат разбора URL. Не подменяйте эту проверку просмотром адресной строки.</p>\n<h2>Отрицательный путь: когда суффикс не решает задачу</h2>\n<p>Проверка «сначала <code>GetList</code>, потом <code>Add</code>» не атомарна. Два воркера могут одновременно увидеть свободный <code>kofe-classic-250-g</code>, оба получить его и оба начать сохранение. Последовательный пример этого не обнаружит, потому что в нём нет конкурирующего состояния.</p>\n<p>При параллельной синхронизации не маскируйте проблему случайным числом. Такой код может измениться при повторной загрузке и не поможет сопоставить товар с источником. Выберите механизм на уровне проекта: последовательную очередь, блокировку, уникальное ограничение базы, если оно поддерживается схемой, или код на основе стабильного артикула. Перед выбором проверьте версию Bitrix, тип базы, существующие URL и допустимость их изменения.</p>\n<p>Есть и другой отрицательный путь. Если имя меняется, а <code>CODE</code> строится заново при каждом обновлении, адрес товара меняется вместе с названием. Для публичного каталога это ломает внешние ссылки. Обычно код фиксируют при создании, а новое название хранят в <code>NAME</code>. Если адрес всё же нужно изменить, задайте явное правило перенаправления и проверьте старую ссылку отдельно.</p>\n<h2>Порядок внедрения и критерий готовности</h2>\n<ol><li>Запишите контракт: область уникальности, язык, регистр, разделитель, максимальную длину и поведение пустого имени.</li><li>Получите базовый код из трёх похожих названий. Убедитесь, что одинаковые нормализованные строки считаются конфликтом.</li><li>Проверьте существующие элементы через <code>CIBlockElement::GetList</code> с тем же <code>IBLOCK_ID</code>, который использует каталог.</li><li>Добавьте суффикс по фиксированному правилу и ограничьте число попыток. После исчерпания попыток остановите операцию с понятной ошибкой.</li><li>Передайте выбранный <code>CODE</code> в <code>CIBlockElement::Add</code>. Обработайте <code>false</code>, <code>LAST_ERROR</code> и возвращённый ID.</li><li>Создайте тестовый элемент неактивным, откройте его детальный URL и сверьте ID, инфоблок и код в диагностике.</li><li>Повторите тест для повторного импорта, изменённого имени и двух одновременных запросов. Для последнего сценария зафиксируйте, какой механизм синхронизации использует проект.</li></ol>\n<p>Материал считается готовым к применению, если два похожих названия не получают один активный адрес, повторная загрузка с тем же внешним идентификатором сохраняет тот же код, а детальная страница возвращает ожидаемый ID. Для параллельного сценария нужен отдельный проверяемый результат: система либо предотвращает конфликт, либо отклоняет одну запись с диагностируемой ошибкой. Ответ «вроде открылось» критерием не является.</p>\n<h2>Ограничения</h2>\n<p>Пример использует старый процедурный API инфоблоков, потому что он показывает механизм исходной задачи. Имена методов, доступность параметров и поведение обработчиков нужно сверить с версией модуля в конкретном проекте. Официальная документация также отмечает изменения в <code>GetList</code> для новых версий модуля. Код из статьи — учебная основа, а не готовый импортёр.</p>\n<p>Уникальность <code>CODE</code> внутри инфоблока не делает URL глобально уникальным. Конфликт может появиться с разделом, другим типом страницы, языковой витриной или ручным маршрутом. Поэтому финальная проверка должна идти через реальный URL-компонент, а не только через таблицу элементов.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://dev.1c-bitrix.ru/api_help/main/reference/cutil/translit.php\" target=\"_blank\" rel=\"noopener\">1С-Битрикс: CUtil::translit</a> — параметры транслитерации, регистр, длина и замены символов.</li><li><a href=\"https://dev.1c-bitrix.ru/api_help/iblock/classes/ciblockelement/getlist.php\" target=\"_blank\" rel=\"noopener\">1С-Битрикс: CIBlockElement::GetList</a> — сигнатура метода и фильтрация элементов инфоблока.</li><li><a href=\"https://dev.1c-bitrix.ru/api_help/iblock/classes/ciblockelement/add.php?print=Y\" target=\"_blank\" rel=\"noopener\">1С-Битрикс: CIBlockElement::Add</a> — добавление элемента, обработчики и результат операции.</li></ul>"
|
||
}
|