Files
progcode/editorial/agent-rewrites/350.json
T

8 lines
17 KiB
JSON
Raw 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": 350,
"slug": "editorial-2018-04-mechanism-bitrix-slugs",
"title": "Bitrix ЧПУ: как путь становится ELEMENT_CODE и где ломается карточка",
"excerpt": "Карточка отвечает 404, хотя CODE в инфоблоке выглядит правильно. Разбираем путь от запроса до фильтра элемента и проверяем каждый слой коротким PHP-примером.",
"contentHtml": "<p>Возьмём учебный стенд каталога Bitrix. Разработчик открывает <code>/catalog/kofe/classic-250-g/</code> и получает 404, хотя в инфоблоке есть элемент с таким <code>CODE</code>. Сначала он чистит кеш и повторяет запрос, но результат не меняется. Цена такой ошибки — потерянный переход и правка данных, которые могли быть исправны.</p>\n<p>Здесь нужно ответить на один вопрос: на каком шаге строка адреса перестаёт совпадать с тем, что ищет компонент? ЧПУ не превращает URL в запрос к инфоблоку автоматически. Комплексный компонент сначала сопоставляет путь с шаблоном, затем записывает значения в массив переменных и только после этого строит выборку. Разделим эти шаги — и вместо гадания по <code>CODE</code> получим проверяемую цепочку.</p>\n<h2>Сцена: 404 после правки CODE</h2>\n<p>В учебном каталоге для детальной страницы задан шаблон <code>#SECTION_CODE#/#ELEMENT_CODE#/</code>, папка компонента — <code>/catalog/</code>. Ожидаемый адрес — <code>/catalog/kofe/classic-250-g/</code>. Первая гипотеза разработчика звучит правдоподобно: «символьный код не тот, надо переименовать элемент». Проверка должна начаться раньше — с фактического URI и результата разбора пути. Если компонент не распознал страницу, изменение поля элемента ничего не исправит.</p>\n<h2>Что именно делает движок ЧПУ</h2>\n<p><code>CComponentEngine::ParseComponentPath</code> получает папку режима ЧПУ, массив URL-шаблонов и запрошенный путь. При совпадении он возвращает код шаблона и записывает значения маркеров в переданный массив. В нашем примере ожидаются <code>detail</code>, <code>SECTION_CODE=kofe</code> и <code>ELEMENT_CODE=classic-250-g</code>. В официальной документации результатом отсутствия шаблона названа пустая строка; примечание той же страницы для лишнего слеша говорит о <code>false</code>. Для старого ядра безопаснее проверять falsey-результат, а не рассчитывать на одно конкретное представление.</p>\n<p>Документация помечает сам метод как доступный с версии 5.1.7, а необязательный аргумент <code>requestURL</code> — с версии 5.1.8. Это исторический API комплексных компонентов. Названия параметров, шаблон и дополнительные фильтры нужно сверить с версией ядра и реальным кодом проекта; пример ниже не заменяет такой сверки.</p>\n<p>Шаблон задают относительно папки компонента. Для <code>SEF_FOLDER=/catalog/</code> записью будет <code>#SECTION_CODE#/#ELEMENT_CODE#/</code>. Полный адрес браузера и относительный шаблон — разные значения. Лишний начальный слеш внутри шаблона может сорвать сопоставление, поэтому его нельзя добавлять «для красоты».</p>\n<figure><img src=\"/assets/editorial/2018/bitrix-slug-route-2018.svg\" alt=\"Запрос каталога проходит через SEF-шаблон, даёт SECTION_CODE и ELEMENT_CODE, затем попадает в фильтр инфоблока\" /><figcaption>Разбор пути и выборка элемента — два шага. Между ними находится массив переменных, а правила фильтра принадлежат конкретному компоненту.</figcaption></figure>\n<h2>Четыре значения, которые нельзя смешивать</h2>\n<div class=\"table-scroll\"><table><thead><tr><th scope=\"col\">Слой</th><th scope=\"col\">Пример</th><th scope=\"col\">Что фиксируем</th></tr></thead><tbody><tr><td>Папка ЧПУ</td><td><code>/catalog/</code></td><td>Совпадает с <code>SEF_FOLDER</code> компонента</td></tr><tr><td>Шаблон детали</td><td><code>#SECTION_CODE#/#ELEMENT_CODE#/</code></td><td>Маркеры и отсутствие начального слеша</td></tr><tr><td>Переменные</td><td><code>SECTION_CODE=kofe</code>, <code>ELEMENT_CODE=classic-250-g</code></td><td>Значения после <code>ParseComponentPath</code></td></tr><tr><td>Выборка</td><td><code>IBLOCK_ID + CODE + ACTIVE</code></td><td>Тот же инфоблок и реальные условия детали</td></tr></tbody></table></div>\n<p>Наличие <code>ELEMENT_CODE</code> доказывает только то, что компонент восстановил часть пути. Оно не доказывает существование активного элемента в нужном инфоблоке. В фильтре могут участвовать раздел, права, даты активности, свойства витрины и другие условия. Их нельзя выводить из одной строки URL.</p>\n<h2>Минимальная воспроизводимая проверка</h2>\n<p>Ниже короткий диагностический фрагмент для тестовой страницы. Число <code>12</code> — условный <code>IBLOCK_ID</code>, его нужно заменить на идентификатор конкретного проекта. Ограничение <code>nTopCount=2</code> нужно не для выбора первого элемента, а чтобы отличить один результат от неоднозначного набора.</p>\n<pre><code>&lt;?php\n\nif (!CModule::IncludeModule(\"iblock\")) {\n throw new RuntimeException(\"Модуль iblock не подключён\");\n}\n\n$templates = array(\n \"detail\" =&gt; \"#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\" =&gt; \"ASC\"),\n array(\n \"IBLOCK_ID\" =&gt; 12,\n \"=CODE\" =&gt; $variables[\"ELEMENT_CODE\"],\n \"ACTIVE\" =&gt; \"Y\",\n ),\n false,\n array(\"nTopCount\" =&gt; 2),\n array(\"ID\", \"IBLOCK_ID\", \"NAME\", \"CODE\")\n);\n\n$elements = array();\nwhile ($element = $result-&gt;GetNext()) {\n $elements[] = $element;\n}\nif (count($elements) === 0) {\n throw new RuntimeException(\"Путь разобран, элемент не найден\");\n}\nif (count($elements) &gt; 1) {\n throw new RuntimeException(\"Фильтр вернул два или более элемента\");\n}\n\n// Здесь один проверенный элемент: сравниваем его ID с ожидаемым.</code></pre>\n<p>Успешный результат этого фрагмента — не «страница точно исправна», а один элемент в базовом фильтре после успешного разбора. Если получено ноль строк, проверяем <code>IBLOCK_ID</code>, значение <code>CODE</code>, активность и дополнительные условия компонента. Если получено две строки, фильтр недостаточно точен либо нарушена уникальность данных. Сортировка по <code>ID</code> делает вывод повторяемым, но не назначает меньший ID правильным товаром.</p>\n<h2>Поворот расследования: ссылка и разбор используют одну форму</h2>\n<p>В нашем сценарии базовый фильтр нашёл ожидаемый элемент, но карточка всё ещё не открывается. Следующая проверка — обратное направление. <code>CComponentEngine::MakePathFromTemplate</code> подставляет значения массива в маркеры и возвращает строку. Он не проверяет rewrite, права и наличие элемента, поэтому совпадение строк — только проверка согласованности шаблонов.</p>\n<pre><code>&lt;?php\n\n$url = CComponentEngine::MakePathFromTemplate(\n \"#SECTION_CODE#/#ELEMENT_CODE#/\",\n array(\n \"SECTION_CODE\" =&gt; \"kofe\",\n \"ELEMENT_CODE\" =&gt; \"classic-250-g\",\n )\n);\n\n// $url: kofe/classic-250-g/\n// Папку /catalog/ добавляет слой, который владеет SEF_FOLDER.</code></pre>\n<p>Если генератор ссылки использует <code>#ELEMENT_ID#</code>, а детальный компонент ждёт <code>#ELEMENT_CODE#</code>, оба фрагмента могут быть корректными по отдельности, но связка сломается. Соберите путь тем же набором маркеров, передайте его в <code>ParseComponentPath</code> и сравните полученные переменные с исходными. Это возвращает расследование к наблюдаемому контракту.</p>\n<h2>Симптом → причина → проверка → действие</h2>\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>Папка, rewrite или шаблон</td><td>Сверить фактический URI и результат <code>ParseComponentPath</code></td><td>Исправить внешний маршрут или шаблон</td></tr><tr><td><code>detail</code> найден, переменная пустая</td><td>Маркер или имя ключа</td><td>Вывести массив переменных</td><td>Согласовать шаблон и чтение ключа</td></tr><tr><td>Код есть, строк нет</td><td>Инфоблок, активность или фильтр</td><td>Повторить <code>GetList</code> с теми же базовыми условиями</td><td>Исправить доказанное условие, а не менять код вслепую</td></tr><tr><td>Строк больше одной</td><td>Широкий фильтр или конфликт данных</td><td>Оставить ограничение <code>nTopCount=2</code> и сравнить ID</td><td>Добавить подтверждённый контекст или выбрать правило уникальности</td></tr><tr><td>Ссылка не проходит обратный разбор</td><td>Разные шаблоны генератора и детали</td><td>Собрать и снова разобрать путь</td><td>Оставить один набор маркеров</td></tr></tbody></table></div>\n<h2>Порядок проверки на стенде</h2>\n<ol><li>Сохранить точный адрес из браузера вместе с завершающим слешем и query-параметрами, если они есть.</li><li>Проверить в access-логе или отладочном выводе, какой URI действительно дошёл до PHP-приложения.</li><li>Сверить <code>SEF_FOLDER</code> и шаблон детали конкретного комплексного компонента.</li><li>Вывести код найденной страницы и массив после <code>ParseComponentPath</code>.</li><li>Выполнить контрольный <code>GetList</code> с известным <code>IBLOCK_ID</code>, точным кодом и ограничением до двух строк.</li><li>Если результат один, добавить условия реального компонента по одному: раздел, даты, права и свойства.</li><li>Собрать обратный путь через <code>MakePathFromTemplate</code> и повторить разбор с теми же значениями.</li></ol>\n<h2>Ограничения и критерий готовности</h2>\n<p>Методика начинается после того, как запрос попал в PHP-компонент. Она не диагностирует сама по себе nginx, Apache, rewrite, кеш, права на инфоблок или логику родительского компонента. Если приложение получает другой URI, сначала исправляют внешний слой. Если путь распознан, но выборка пуста, проверяют данные и фильтр. Эти причины нельзя объединять под общим «сломался ЧПУ».</p>\n<p>Методика также не создаёт уникальность <code>CODE</code>. В некоторых каталогах одинаковый код допустим в разных разделах или витринах; тогда контекст должен быть частью адреса и фильтра. Не добавляйте <code>SECTION_ID</code> автоматически: сначала выясните, как конкретный компонент определяет идентичность элемента.</p>\n<p>В учебном сценарии исправление готово, когда один контрольный URL даёт <code>detail</code>, непустые <code>SECTION_CODE</code> и <code>ELEMENT_CODE</code>, ровно один ожидаемый элемент в фильтре компонента и обратный путь с теми же значениями. Для отрицательных адресов тест должен показать, на каком слое остановилась проверка. Только после этого решают, нужна ли правка шаблона, маршрута, фильтра или данных.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://dev.1c-bitrix.ru/api_help/main/reference/ccomponentengine/parsecomponentpath.php?print=Y\" target=\"_blank\" rel=\"noopener noreferrer\">Bitrix: CComponentEngine::ParseComponentPath</a> — сигнатура метода, папка ЧПУ, URL-шаблоны, восстановление переменных, версия аргумента <code>requestURL</code> и результат без найденного шаблона.</li><li><a href=\"https://dev.1c-bitrix.ru/api_help/main/reference/ccomponentengine/makepathfromtemplate.php?print=Y\" target=\"_blank\" rel=\"noopener noreferrer\">Bitrix: CComponentEngine::MakePathFromTemplate</a> — подстановка значений массива в маркеры шаблона и возвращаемая строка пути.</li><li><a href=\"https://dev.1c-bitrix.ru/api_help/iblock/classes/ciblockelement/getlist.php?print=Y\" target=\"_blank\" rel=\"noopener noreferrer\">Bitrix: CIBlockElement::GetList</a> — параметры сортировки, фильтра, ограничения и полей выборки; отдельно описаны поля <code>IBLOCK_ID</code>, <code>ACTIVE</code> и <code>CODE</code>.</li></ul>"
}