D7-роутинг в 1С-Битриксе: отказ от физических index.php
Зачем уходить от физических файлов
В Битриксе часто устроено так: каждому адресу страницы соответствует свой PHP-файл. Адресу /blog/ — файл blog/index.php, адресу /blog/hello-world/ — файл blog/hello-world/index.php. Внутри каждого файла одно и то же: подключается шаблон сайта, вызывается компонент, прописываются заголовок и описание. Такой подход работает, но у него есть слабые места:
- Адрес страницы жёстко привязан к файловой структуре. Переименовали папку — сломались ссылки.
- На каждую новую страницу приходится создавать ещё один файл с повторяющимся кодом.
- Заголовок, описание и ссылку на «каноническую» версию страницы (canonical) приходится прописывать в каждом файле отдельно.
- Нет единой точки входа — каждый файл живёт сам по себе.
D7-роутинг эти проблемы убирает: правила для всех страниц лежат в одном файле, вместо множества одинаковых файлов — несколько методов, а заголовки и описания задаются в одном месте.
Как это работает по умолчанию
При стандартной схеме запрос проходит такой путь:
- Веб-сервер получает адрес страницы.
- Файл
urlrewrite.phpсравнивает адрес с правилами и ищет для него физический файл. - Выполняется
blog/index.php(или другой найденный файл). - Файл подключает шаблон сайта и вызывает
$APPLICATION->IncludeComponent().
Чем больше страниц, тем больше однотипных файлов, которые отличаются только названием компонента и парой параметров. Поддерживать это с каждым разом тяжелее.
Переключаемся на D7-роутинг
Включить роутинг — это одна настройка в файле bitrix/.settings.php:
'routing' => [
'config' => ['web.php'],
],
Ядро читает эту настройку, когда инициализирует роутер. Теперь запрос, для которого не нашлось физического файла, попадает в routing_index.php. Там адрес сравнивается с правилами из web.php, и управление передаётся нужному контроллеру.
Файл urlrewrite.php в корне сайта остаётся с ЧПУ-правилами. Если ЧПУ-правил нет (что типично при переводе всего на D7-роутинг), в нём будет пустой массив:
<?php
$arUrlRewrite = [];
Важно: очищать urlrewrite.php можно только тогда, когда в нём и так нет правил. Если правила есть (например, для инфоблоков, каталога или лендингов) — их трогать нельзя, по этим адресам запрос перехватывать не нужно. D7-роутинг работает как «fallback»: сначала запрос подходит к ЧПУ-правилам из urlrewrite.php, и только если ни одно правило не сработало и физического файла нет — управление передаётся роутеру (routing_index.php).
Структура файла маршрутов
Все правила страниц хранятся в local/routes/web.php. Вот наглядный пример для блога:
<?php
use Bitrix\Main\Loader;
use Bitrix\Main\Routing\RoutingConfigurator;
return static function (RoutingConfigurator $routes) {
// Главная страница
$routes
->get('/', [SiteController::class, 'home'])
->name('site.home');
// Блог: список, пагинация и детальная запись
$routes
->prefix('blog')
->name('blog.')
->group(static function (RoutingConfigurator $routes) {
// Список записей (первая страница)
$routes
->get('', [BlogController::class, 'list'])
->name('list');
// Пагинация — {page} только цифры
$routes
->get('page/{page}/', [BlogController::class, 'page'])
->where('page', '\d+')
->name('page');
// Детальная запись — {code} символьный код
$routes
->get('{code}/', [BlogController::class, 'view'])
->where('code', '[\w\-]+')
->name('post');
});
};
Пройдёмся по ключевым строкам:
->get('/blog/', ...)— правило, которое срабатывает, когда адрес начинается с/blog/и запрос выполнен методом GET.->prefix('blog')— общая приставка для группы: все правила внутри автоматически получают/blog/в начале.->name('blog.post')— даёт правилу имя. По имени потом удобно строить ссылки (об этом ниже).{page}и{code}— части адреса, которые меняются. В фигурных скобках — их названия, роутер вытащит реальные значения из адреса.->where('page', '\d+')— ограничение:{page}должен быть только цифрами. Это нужно, чтобы правило пагинации не спорило с правилом детальной записи: цифра — это номер страницы, а не код записи.
Например, /blog/page/2/ попадёт в метод page, а /blog/hello-world/ — в view. Роутер сам разберёт параметры и передаст их в метод контроллера.
Важный момент: Loader::includeModule()
Если ваш сайт построен на собственном модуле (классы контроллеров лежат в local/modules/...), этот модуль нужно подключить до того, как будут объявлены маршруты:
Loader::includeModule('my.custom.module');
return static function (RoutingConfigurator $routes) {
// правила страниц...
};
Почему важен порядок? Когда ядро разбирает маршруты, оно загружает класс контроллера автоматически — по имени класса. Автозагрузка классов модуля включается именно в момент вызова Loader::includeModule(). Если модуль подключить внутри замыкания (или вообще забыть это сделать), автозагрузчик не успеет заработать, и ядро не найдёт класс:
Class 'MyNamespace\Controller\BlogController' not found
Ошибка коварная: на одних страницах она может не появляться, а на других — вылезать, и это легко сбивает с толку. Поэтому модуль всегда подключается в самом верху файла с правилами.
Контроллер: замена физической страницы
Вместо отдельных файлов используются классы-контроллеры. Контроллер возвращает специальный объект, и Битрикс сам подставляет его содержимое внутрь шаблона сайта — рядом с шапкой, меню и подвалом. Вот пример:
<?php
namespace MyNamespace\Controller;
use Bitrix\Main\Engine\Controller;
use Bitrix\Main\Engine\Response\Render\Component;
final class BlogController extends Controller
{
public function configureActions(): array
{
return [
'list' => ['prefilters' => []],
'page' => ['prefilters' => []],
'view' => ['prefilters' => []],
];
}
public function listAction(): Component
{
// Заголовок и описание страницы ставим ДО renderComponent
global $APPLICATION;
$APPLICATION->SetTitle('Блог');
$APPLICATION->SetPageProperty('description', 'Описание страницы блога');
return $this->renderComponent('my:blog.list', '', [
'PAGE_SIZE' => 5,
'CACHE_TYPE' => 'A',
'CACHE_TIME' => 3600,
]);
}
}
Обратите внимание на две вещи:
1. Метод configureActions(). Здесь настраиваются так называемые префильтры — проверки, которые Битрикс выполняет перед запуском действия. На публичной части по умолчанию жёсткой проверки авторизации и сессии нет — она включается в административном разделе. Для открытых GET-страниц можно оставить пустой список ['prefilters' => []], и никакой сессии не потребуется. Но не сбрасывайте префильтры «вслепую»: если на странице появятся POST-формы, отключайте фильтры точечно (например, только проверку авторизации), разобравшись, за что отвечает каждый.
2. Метод listAction(). Он возвращает объект Response\Render\Component (через renderComponent()). Это не готовая строка HTML, а описание того, что нужно отрисовать. Ядро берёт компонент my:blog.list и рендерит его внутри шаблона сайта: шапка → компонент → подвал.
Почему заголовок страницы ставится в контроллере
Тут есть один нюанс, о котором легко забыть. Когда контроллер вызывает renderComponent(), шапка страницы — с тегом <title>, описанием и canonical — собирается сразу, прямо в момент этого вызова. То есть шапка готова раньше, чем выполнится код самого компонента.
Поэтому, если вы вызовете SetTitle() или SetPageProperty() внутри компонента, они попадут в шапку слишком поздно — она уже собрана.
Правильный порядок: метаданные (заголовок, описание, canonical, хлебные крошки) задаются в методе контроллера, до вызова renderComponent(). Компонент отвечает только за содержимое страницы, а всё «окружение» страницы — за контроллером.
Ссылки через именованные маршруты
При D7-роутинге ссылки удобно строить через роутер, а не склеивать из строк:
$router = \Bitrix\Main\Application::getInstance()->getRouter();
// В шаблоне или контроллере:
$url = $router->route('blog.post', ['code' => $elementCode]);
В чём выгода? Правило страницы имеет имя (мы задали его через ->name('blog.post')). Если позже адрес поменяется — например, /blog/{code}/ станет /articles/{code}/ — все ссылки, построенные через route(), обновятся сами, потому что адрес берётся из правила, а не из строки. При склейке руками пришлось бы править каждый файл.
Учтите нюанс: route() возвращает относительный путь (например, /blog/hello-world/). Для тега canonical или других мест, где нужен абсолютный адрес с доменом, этого мало. Соберите полный URL через тот же роутер и класс \Bitrix\Main\Web\Uri:
$router = \Bitrix\Main\Application::getInstance()->getRouter();
// Относительный путь
$path = $router->route('blog.post', ['code' => $code]);
// Полный адрес для canonical
$absolute = (new \Bitrix\Main\Web\Uri($path))->getLocator();
Запросы через AJAX: рендер без шаблона
Частая задача — пагинация без перезагрузки страницы, когда сервер должен вернуть только фрагмент (например, список записей), а не всю страницу целиком. Для этого у renderComponent() есть четвёртый параметр:
if ($this->getRequest()->isAjaxRequest()) {
// Только содержимое компонента, без шапки и подвала
return $this->renderComponent('my:blog.list', '', $params, false);
}
// Обычный запрос — полная страница
return $this->renderComponent('my:blog.list', '', $params);
Метод isAjaxRequest() определяет, что запрос пришёл через AJAX, по служебному заголовку X-Requested-With: XMLHttpRequest, который браузер шлёт при таких запросах. Если в четвёртый параметр передано false, компонент рендерится без шаблона сайта — отдаётся только HTML-фрагмент. Дальше скрипт на странице подставляет этот фрагмент в нужное место (например, в контейнер со списком записей).
Пагинация: совместная работа c классическим компонентом
У Битрикса есть готовый компонент постраничной навигации bitrix:system.pagenavigation. Но он умеет работать только с классическим объектом CDBResult, а не с более новой D7-навигацией PageNavigation. Чтобы использовать готовую навигацию вместе с D7-запросами, приходится вручную собрать нужный объект:
$navObject = new CDBResult();
$navObject->NavRecordCount = $total; // всего записей
$navObject->NavPageSize = $pageSize; // записей на странице
$navObject->NavPageNomer = $page; // текущая страница
$navObject->NavPageCount = max(1, (int)ceil($total / $pageSize));
$navObject->NavNum = 1;
$navObject->nPageWindow = 5;
$navObject->bShowAll = false;
$navObject->bDescPageNumbering = false;
$navObject->bSavePage = false;
$this->arResult['NAV_OBJECT'] = $navObject;
Стоит знать и об альтернативе: компонент bitrix:main.pagenavigation умеет работать напрямую с D7-объектом \Bitrix\Main\UI\PageNavigation, без ручного CDBResult. Он удобен, когда достаточно штатных постраничных ссылок. Здесь выбран bitrix:system.pagenavigation с кастомным шаблоном: он строит аккуратные SEF-ссылки на маршрут пагинации (например, /blog/page/2/) вместо ?PAGEN_1=2.
Страница 404 и кэширование: abortResultCache()
Распространённая ошибка при работе с кэшем. Битрикс может показать страницу «не найдено» и при этом сохранить в кэш пустой результат. Если запись появится чуть позже, пользователь до окончания срока кэша (TTL) всё равно будет видеть «не найдено», хотя страница уже существует.
Решение — отменить запись в кэш, когда данных нет:
if (!$row) {
$this->abortResultCache(); // не сохраняем пустой результат
$this->set404();
return;
}
Метод abortResultCache() отменяет сохранение результата. Благодаря этому следующий запрос к странице снова обратится к базе данных и покажет актуальную информацию.
Итого: как добавить новую страницу
Добавление страницы при D7-роутинге — это три шага:
- Описать правило в
local/routes/web.php— какой адрес и какому методу контроллера соответствует. - Написать метод в контроллере — он задаёт метаданные страницы и возвращает результат
renderComponent(). - Создать компонент — в нём логика и шаблон содержимого страницы.
Отдельный физический файл при этом не создаётся — страницу собирает роутер.
Короткий чек-лист перед запуском
Loader::includeModule()стоит до объявления правил, но после строк сuse.configureActions()отключает проверку авторизации и CSRF для публичных страниц.- Заголовок, описание и canonical задаются в контроллере, а не в компоненте.
- Внутренние ссылки строятся через
$router->route(), а не склейкой строк. - Для AJAX-запросов используется
renderComponent(..., false). - При отсутствии записи вызывается
abortResultCache()передset404(). - Файл
urlrewrite.phpпустой.