diff --git a/editorial/agent-rewrites/350.json b/editorial/agent-rewrites/350.json index b192108..f635765 100644 --- a/editorial/agent-rewrites/350.json +++ b/editorial/agent-rewrites/350.json @@ -3,5 +3,5 @@ "slug": "editorial-2018-04-mechanism-bitrix-slugs", "title": "Bitrix ЧПУ: как путь становится ELEMENT_CODE и где ломается карточка", "excerpt": "Карточка отвечает 404, хотя CODE в инфоблоке выглядит правильно. Разбираем путь от запроса до фильтра элемента и проверяем каждый слой коротким PHP-примером.", - "contentHtml": "
Карточка товара отвечает 404, хотя в инфоблоке есть элемент с нужным CODE. Иногда страница открывается по /catalog/classic-250-g/, но не по /catalog/kofe/classic-250-g/. Иногда компонент выбирает не тот товар. Цена ошибки — потерянный переход, неверная карточка и правка данных, которые не были причиной.
Главный тезис: ЧПУ не превращает строку адреса в запрос к инфоблоку автоматически. Bitrix сначала сопоставляет путь с шаблоном комплексного компонента. Затем записывает значения в массив переменных. Только после этого компонент строит фильтр. Если смешать шаги, поиск причины превращается в перебор вариантов CODE.
CComponentEngine::ParseComponentPath получает папку ЧПУ, массив шаблонов и запрошенный путь. Если шаблон подходит, метод возвращает его код и заполняет переданный массив. Шаблон #SECTION_CODE#/#ELEMENT_CODE#/ для запроса /catalog/kofe/classic-250-g/ должен дать страницу detail, раздел kofe и код classic-250-g. Если шаблон не найден, метод возвращает пустую строку.
Шаблон задают относительно папки компонента. Для SEF_FOLDER=/catalog/ нужна запись #SECTION_CODE#/#ELEMENT_CODE#/. Начальный слеш в самом шаблоне меняет результат разбора. Полный URL и шаблон нельзя сравнивать как одинаковые строки.
После разбора компонент сам выбирает поля. Он может фильтровать по IBLOCK_ID, CODE, активности, разделу, правам и свойствам. Наличие ELEMENT_CODE ещё не доказывает, что пользователь увидит элемент. Оно доказывает только восстановление части пути.
| Участок | Пример | Что проверить |
|---|---|---|
| Папка компонента | /catalog/ | Совпадает ли с SEF_FOLDER |
| Шаблон детали | #SECTION_CODE#/#ELEMENT_CODE#/ | Маркеры и отсутствие начального слеша |
| Переменная | ELEMENT_CODE=classic-250-g | Заполнен ли массив после разбора |
| Фильтр | IBLOCK_ID + CODE + ACTIVE | Не исключает ли условие элемент |
| Обратная ссылка | /catalog/kofe/classic-250-g/ | Тот же набор маркеров |
Есть и отрицательный путь. Если веб-сервер не передал запрос в точку входа Bitrix или rewrite изменил URI, компонент не получит исходный адрес. Исправление шаблона инфоблока ничего не изменит. Сначала нужно увидеть фактический URI на входе приложения.
\nНиже учебный пример с условным IBLOCK_ID=12. Он показывает порядок диагностики, а не готовый фильтр любого каталога. В рабочем компоненте фильтр нужно сверить с его реальным кодом.
<?php\n\nCModule::IncludeModule(\"iblock\");\n\n$templates = array(\n \"detail\" => \"#SECTION_CODE#/#ELEMENT_CODE#/\",\n);\n$variables = array();\n$page = CComponentEngine::ParseComponentPath(\n \"/catalog/\", $templates, $variables,\n \"/catalog/kofe/classic-250-g/\"\n);\n\nif ($page === \"\" || empty($variables[\"ELEMENT_CODE\"])) {\n throw new RuntimeException(\"URL не разобран как детальная страница\");\n}\n\n$result = CIBlockElement::GetList(\n array(),\n array(\n \"IBLOCK_ID\" => 12,\n \"=CODE\" => $variables[\"ELEMENT_CODE\"],\n \"ACTIVE\" => \"Y\",\n ),\n false, array(\"nTopCount\" => 2),\n array(\"ID\", \"NAME\", \"CODE\")\n);\n\n$elements = array();\nwhile ($element = $result->Fetch()) {\n $elements[] = $element;\n}\nif (count($elements) === 0) {\n throw new RuntimeException(\"Элемент не найден\");\n}\nif (count($elements) > 1) {\n throw new RuntimeException(\"Фильтр вернул несколько элементов\");\n}\nВызов ограничен двумя строками, чтобы обнаружить неоднозначность. Ноль результатов указывает на IBLOCK_ID, код, активность или дополнительное условие. Два результата показывают слишком широкий фильтр. Нельзя брать первый элемент сортировкой и считать проблему решённой.
Карточка может ломаться при построении ссылки. Список собирает путь с одним именем переменной, а детальный компонент ждёт другое. Соберите учебный URL через CComponentEngine::MakePathFromTemplate, а затем разберите его тем же шаблоном.
<?php\n\n$url = CComponentEngine::MakePathFromTemplate(\n \"#SECTION_CODE#/#ELEMENT_CODE#/\",\n array(\n \"SECTION_CODE\" => \"kofe\",\n \"ELEMENT_CODE\" => \"classic-250-g\",\n )\n);\n\n// $url: kofe/classic-250-g/\nФрагмент проверяет только согласованность маркеров. Он не проверяет rewrite, права и фильтры. Учебное совпадение строки не является production-результатом.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Страница не определяется | Не совпали папка или шаблон | Посмотреть результат ParseComponentPath | Сверить SEF_FOLDER и шаблон |
detail найден, код пуст | Маркер отсутствует или ключ не читается | Вывести массив переменных | Исправить шаблон или обращение к ключу |
| Код есть, карточки нет | Неверный инфоблок, код или активность | Повторить GetList с базовыми фильтрами | Исправить фильтр или данные после проверки |
| Код есть, результатов два | Фильтр широк или нарушена уникальность | Запросить ID, IBLOCK_ID и CODE | Добавить доказанный контекст или устранить конфликт |
| Ссылка строится иначе | Генератор и разбор используют разные шаблоны | Собрать и разобрать путь обратно | Оставить один набор маркеров |
| PHP получает другой путь | Rewrite меняет запрос | Сравнить URI браузера и приложения | Исправить внешний слой, не меняя CODE |
ParseComponentPath.ELEMENT_CODE отдельным GetList с известным IBLOCK_ID.Методика применима к маршруту, который уже обрабатывает PHP-компонент Bitrix в режиме ЧПУ. Она не заменяет nginx, Apache, rewrite, права на инфоблок и проверку кеша. Она также не создаёт уникальность CODE. Если коды могут повторяться, фильтру нужен подтверждённый контекст: раздел или внешний идентификатор.
Не меняйте данные как первую реакцию на 404. Сначала разделите три результата: шаблон не распознан, переменная не заполнена, элемент исключён фильтром. После этого причина привязана к конкретному слою.
\nГотовность проверяется так: каждый контрольный URL даёт страницу detail, непустой ELEMENT_CODE, ровно один ожидаемый элемент в том же фильтре и обратную ссылку, которая снова проходит разбор. Для отрицательных случаев проверка показывает понятную причину: пустой шаблон, пустой код или ноль результатов.
CODE, ACTIVE; документация отмечает изменения метода в новых версиях модуля.Возьмём учебный стенд каталога Bitrix. Разработчик открывает /catalog/kofe/classic-250-g/ и получает 404, хотя в инфоблоке есть элемент с таким CODE. Сначала он чистит кеш и повторяет запрос, но результат не меняется. Цена такой ошибки — потерянный переход и правка данных, которые могли быть исправны.
Здесь нужно ответить на один вопрос: на каком шаге строка адреса перестаёт совпадать с тем, что ищет компонент? ЧПУ не превращает URL в запрос к инфоблоку автоматически. Комплексный компонент сначала сопоставляет путь с шаблоном, затем записывает значения в массив переменных и только после этого строит выборку. Разделим эти шаги — и вместо гадания по CODE получим проверяемую цепочку.
В учебном каталоге для детальной страницы задан шаблон #SECTION_CODE#/#ELEMENT_CODE#/, папка компонента — /catalog/. Ожидаемый адрес — /catalog/kofe/classic-250-g/. Первая гипотеза разработчика звучит правдоподобно: «символьный код не тот, надо переименовать элемент». Проверка должна начаться раньше — с фактического URI и результата разбора пути. Если компонент не распознал страницу, изменение поля элемента ничего не исправит.
CComponentEngine::ParseComponentPath получает папку режима ЧПУ, массив URL-шаблонов и запрошенный путь. При совпадении он возвращает код шаблона и записывает значения маркеров в переданный массив. В нашем примере ожидаются detail, SECTION_CODE=kofe и ELEMENT_CODE=classic-250-g. В официальной документации результатом отсутствия шаблона названа пустая строка; примечание той же страницы для лишнего слеша говорит о false. Для старого ядра безопаснее проверять falsey-результат, а не рассчитывать на одно конкретное представление.
Документация помечает сам метод как доступный с версии 5.1.7, а необязательный аргумент requestURL — с версии 5.1.8. Это исторический API комплексных компонентов. Названия параметров, шаблон и дополнительные фильтры нужно сверить с версией ядра и реальным кодом проекта; пример ниже не заменяет такой сверки.
Шаблон задают относительно папки компонента. Для SEF_FOLDER=/catalog/ записью будет #SECTION_CODE#/#ELEMENT_CODE#/. Полный адрес браузера и относительный шаблон — разные значения. Лишний начальный слеш внутри шаблона может сорвать сопоставление, поэтому его нельзя добавлять «для красоты».
| Слой | Пример | Что фиксируем |
|---|---|---|
| Папка ЧПУ | /catalog/ | Совпадает с SEF_FOLDER компонента |
| Шаблон детали | #SECTION_CODE#/#ELEMENT_CODE#/ | Маркеры и отсутствие начального слеша |
| Переменные | SECTION_CODE=kofe, ELEMENT_CODE=classic-250-g | Значения после ParseComponentPath |
| Выборка | IBLOCK_ID + CODE + ACTIVE | Тот же инфоблок и реальные условия детали |
Наличие ELEMENT_CODE доказывает только то, что компонент восстановил часть пути. Оно не доказывает существование активного элемента в нужном инфоблоке. В фильтре могут участвовать раздел, права, даты активности, свойства витрины и другие условия. Их нельзя выводить из одной строки URL.
Ниже короткий диагностический фрагмент для тестовой страницы. Число 12 — условный IBLOCK_ID, его нужно заменить на идентификатор конкретного проекта. Ограничение nTopCount=2 нужно не для выбора первого элемента, а чтобы отличить один результат от неоднозначного набора.
<?php\n\nif (!CModule::IncludeModule(\"iblock\")) {\n throw new RuntimeException(\"Модуль iblock не подключён\");\n}\n\n$templates = array(\n \"detail\" => \"#SECTION_CODE#/#ELEMENT_CODE#/\",\n);\n$variables = array();\n$requestPath = \"/catalog/kofe/classic-250-g/\";\n$page = CComponentEngine::ParseComponentPath(\n \"/catalog/\",\n $templates,\n $variables,\n $requestPath\n);\n\nif (!$page || empty($variables[\"ELEMENT_CODE\"])) {\n throw new RuntimeException(\"Путь не разобран или код не найден\");\n}\nif ($page !== \"detail\") {\n throw new RuntimeException(\"Распознан не detail: \" . $page);\n}\n\n$result = CIBlockElement::GetList(\n array(\"ID\" => \"ASC\"),\n array(\n \"IBLOCK_ID\" => 12,\n \"=CODE\" => $variables[\"ELEMENT_CODE\"],\n \"ACTIVE\" => \"Y\",\n ),\n false,\n array(\"nTopCount\" => 2),\n array(\"ID\", \"IBLOCK_ID\", \"NAME\", \"CODE\")\n);\n\n$elements = array();\nwhile ($element = $result->GetNext()) {\n $elements[] = $element;\n}\nif (count($elements) === 0) {\n throw new RuntimeException(\"Путь разобран, элемент не найден\");\n}\nif (count($elements) > 1) {\n throw new RuntimeException(\"Фильтр вернул два или более элемента\");\n}\n\n// Здесь один проверенный элемент: сравниваем его ID с ожидаемым.\nУспешный результат этого фрагмента — не «страница точно исправна», а один элемент в базовом фильтре после успешного разбора. Если получено ноль строк, проверяем IBLOCK_ID, значение CODE, активность и дополнительные условия компонента. Если получено две строки, фильтр недостаточно точен либо нарушена уникальность данных. Сортировка по ID делает вывод повторяемым, но не назначает меньший ID правильным товаром.
В нашем сценарии базовый фильтр нашёл ожидаемый элемент, но карточка всё ещё не открывается. Следующая проверка — обратное направление. CComponentEngine::MakePathFromTemplate подставляет значения массива в маркеры и возвращает строку. Он не проверяет rewrite, права и наличие элемента, поэтому совпадение строк — только проверка согласованности шаблонов.
<?php\n\n$url = CComponentEngine::MakePathFromTemplate(\n \"#SECTION_CODE#/#ELEMENT_CODE#/\",\n array(\n \"SECTION_CODE\" => \"kofe\",\n \"ELEMENT_CODE\" => \"classic-250-g\",\n )\n);\n\n// $url: kofe/classic-250-g/\n// Папку /catalog/ добавляет слой, который владеет SEF_FOLDER.\nЕсли генератор ссылки использует #ELEMENT_ID#, а детальный компонент ждёт #ELEMENT_CODE#, оба фрагмента могут быть корректными по отдельности, но связка сломается. Соберите путь тем же набором маркеров, передайте его в ParseComponentPath и сравните полученные переменные с исходными. Это возвращает расследование к наблюдаемому контракту.
| Симптом | Вероятный слой | Проверка | Действие |
|---|---|---|---|
| Страница не определяется | Папка, rewrite или шаблон | Сверить фактический URI и результат ParseComponentPath | Исправить внешний маршрут или шаблон |
detail найден, переменная пустая | Маркер или имя ключа | Вывести массив переменных | Согласовать шаблон и чтение ключа |
| Код есть, строк нет | Инфоблок, активность или фильтр | Повторить GetList с теми же базовыми условиями | Исправить доказанное условие, а не менять код вслепую |
| Строк больше одной | Широкий фильтр или конфликт данных | Оставить ограничение nTopCount=2 и сравнить ID | Добавить подтверждённый контекст или выбрать правило уникальности |
| Ссылка не проходит обратный разбор | Разные шаблоны генератора и детали | Собрать и снова разобрать путь | Оставить один набор маркеров |
SEF_FOLDER и шаблон детали конкретного комплексного компонента.ParseComponentPath.GetList с известным IBLOCK_ID, точным кодом и ограничением до двух строк.MakePathFromTemplate и повторить разбор с теми же значениями.Методика начинается после того, как запрос попал в PHP-компонент. Она не диагностирует сама по себе nginx, Apache, rewrite, кеш, права на инфоблок или логику родительского компонента. Если приложение получает другой URI, сначала исправляют внешний слой. Если путь распознан, но выборка пуста, проверяют данные и фильтр. Эти причины нельзя объединять под общим «сломался ЧПУ».
\nМетодика также не создаёт уникальность CODE. В некоторых каталогах одинаковый код допустим в разных разделах или витринах; тогда контекст должен быть частью адреса и фильтра. Не добавляйте SECTION_ID автоматически: сначала выясните, как конкретный компонент определяет идентичность элемента.
В учебном сценарии исправление готово, когда один контрольный URL даёт detail, непустые SECTION_CODE и ELEMENT_CODE, ровно один ожидаемый элемент в фильтре компонента и обратный путь с теми же значениями. Для отрицательных адресов тест должен показать, на каком слое остановилась проверка. Только после этого решают, нужна ли правка шаблона, маршрута, фильтра или данных.
requestURL и результат без найденного шаблона.IBLOCK_ID, ACTIVE и CODE.