Files

8 lines
20 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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>. Если детальный компонент ищет запись только по этому полю, запрос становится неоднозначным: результат зависит от его фильтра и сортировки. Цена ошибки — неверная карточка в заказе, сломанные ссылки из рекламы и ручная чистка дублей.</p>\n<p>Проблема возникает не потому, что Bitrix плохо транслитерирует строку. <code>CUtil::translit</code> решает одну задачу: приводит текст к заданному виду. Уникальность решает другой код. Он должен проверить кандидата в конкретном информационном блоке (<code>IBLOCK_ID</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>CODE</code> не длиннее 90 символов и уникален внутри одного <code>IBLOCK_ID</code>. Базовый кандидат ограничен 86 символами: ещё четыре остаются для суффикса от <code>-2</code> до <code>-50</code>. Дефис разделяет слова, регистр — нижний. Это не универсальные значения, а учебная конфигурация для последовательного импорта.</p>\n<p>Для нормализации используем <code>CUtil::translit</code>. Метод принимает исходную строку, язык и параметры замены пробелов, прочих символов, регистра и длины. После вызова нужно убрать дефисы по краям и проверить пустой результат. Имя из одних знаков или символов, которые правило не сохраняет, не даёт пригодного кода.</p>\n<p>Проверка занятости должна повторить область поиска каталога. Если компонент работает с инфоблоком 12, фильтр по всем инфоблокам даст лишние конфликты. Если компонент ищет только активные элементы, это тоже часть проектного правила. Важно заранее решить, может ли архивный элемент удерживать старый адрес. В большинстве каталогов да: иначе повторная публикация способна занять прежнюю ссылку.</p>\n<div class=\"table-scroll\"><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></div>\n<h2>Учебный пример для последовательного добавления</h2>\n<p>Функция возвращает первый свободный кандидат. Она подходит для ручного ввода и последовательного учебного импорта. Число 50 ограничивает только этот пример. Оно не является лимитом Bitrix и не доказывает, что код можно безопасно создавать при параллельных запросах.</p>\n<pre><code>&lt;?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 &lt;= 0) {\n throw new InvalidArgumentException('Некорректный IBLOCK_ID');\n }\n\n $base = CUtil::translit(trim($name), 'ru', array(\n 'max_len' =&gt; 86,\n 'change_case' =&gt; 'L',\n 'replace_space' =&gt; '-',\n 'replace_other' =&gt; '-',\n 'delete_repeat_replace' =&gt; true,\n ));\n\n $base = trim($base, '-');\n if ($base === '') {\n throw new InvalidArgumentException('NAME не дал пригодный CODE');\n }\n\n for ($number = 1; $number &lt;= 50; $number++) {\n $candidate = $number === 1 ? $base : $base . '-' . $number;\n $result = CIBlockElement::GetList(\n array(),\n array('IBLOCK_ID' =&gt; (int) $iblockId, '=CODE' =&gt; $candidate),\n false,\n array('nTopCount' =&gt; 1),\n array('ID')\n );\n\n if (!$result-&gt;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> дописывайте код: между двумя операциями появляется окно, в котором запись уже видна без нужного адреса. В учебном варианте элемент создаём неактивным и сначала сверяем его поля. URL проверяем на тестовой витрине после активации по проектному процессу, затем переводим запись в рабочее состояние.</p>\n<pre><code>&lt;?php\n\n$iblockId = 12;\n$element = new CIBlockElement();\n$code = getFreeElementCode($iblockId, $name);\n$id = $element-&gt;Add(array(\n 'IBLOCK_ID' =&gt; $iblockId,\n 'NAME' =&gt; $name,\n 'CODE' =&gt; $code,\n 'ACTIVE' =&gt; 'N',\n));\n\nif ($id === false) {\n throw new RuntimeException($element-&gt;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>При параллельной синхронизации не маскируйте проблему случайным числом. Такой код может измениться при повторной загрузке и не поможет сопоставить товар с источником. Выберите механизм на уровне проекта: последовательную очередь, блокировку, уникальное ограничение базы, если оно поддерживается схемой, или код на основе стабильного артикула. Если предварительный поиск уже прошёл, но <code>Add</code> вернул <code>false</code>, сохраните <code>LAST_ERROR</code> и решите, можно ли безопасно повторить операцию с новым кандидатом. Перед выбором проверьте версию 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>Создайте тестовый элемент неактивным и сверьте его поля через <code>GetList</code>; затем на тестовой витрине активируйте его по проектному процессу, откройте детальный URL и сверьте ID, инфоблок и код в диагностике.</li><li>Повторите тест для повторного импорта, изменённого имени и двух одновременных запросов. Для последнего сценария зафиксируйте, какой механизм синхронизации использует проект.</li></ol>\n<p>Код выше показывает выбор кандидата для нового элемента, но не реализует идемпотентность импорта. Для повторной загрузки сначала найдите элемент по стабильному внешнему идентификатору, а затем не пересчитывайте его <code>CODE</code> из <code>NAME</code>. Материал считается готовым к применению, если два похожих названия не получают один активный адрес, повторная загрузка с тем же внешним идентификатором сохраняет тот же код, а детальная страница возвращает ожидаемый ID. Для параллельного сценария нужен отдельный проверяемый результат: система либо предотвращает конфликт, либо отклоняет одну запись с диагностируемой ошибкой. Ответ «вроде открылось» критерием не является.</p>\n<h2>Ограничения</h2>\n<p>Пример использует старый процедурный API инфоблоков, потому что он показывает механизм исходной задачи. Имена методов, доступность параметров и поведение обработчиков нужно сверить с версией модуля в конкретном проекте: документация <code>GetList</code> отдельно перечисляет изменения, начиная с версии 18.6.200 модуля инфоблоков. Код из статьи — учебная основа, а не готовый импортёр.</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 noreferrer\">1С-Битрикс: CUtil::translit</a> — параметры транслитерации, регистр, длина и замены символов.</li><li><a href=\"https://dev.1c-bitrix.ru/api_help/iblock/classes/ciblockelement/getlist.php\" target=\"_blank\" rel=\"noopener noreferrer\">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 noreferrer\">1С-Битрикс: CIBlockElement::Add</a> — добавление элемента, обработчики и результат операции.</li></ul>"
}