Маршрутизация
Маршрутизатор отвечает за все, что связано с URL-адресами, чтобы вам больше не приходилось о них думать. Мы покажем:
- как настроить маршрутизатор, чтобы URL были такими, как вы хотите
- расскажем о SEO и перенаправлении
- и покажем, как написать собственный маршрутизатор
Более человечные URL (или также крутые или красивые URL) более удобны в использовании, запоминаемы и положительно влияют на SEO. Nette учитывает это и полностью идет навстречу разработчикам. Вы можете спроектировать для своего приложения именно такую структуру URL-адресов, какую захотите. Вы можете спроектировать ее даже тогда, когда приложение уже готово, потому что это не потребует изменений в коде или шаблонах. Она определяется элегантным способом в одном единственном месте, в маршрутизаторе, и таким образом не разбросана в виде аннотаций во всех презентерах.
Маршрутизатор в Nette уникален тем, что он двусторонний. Он умеет как декодировать URL в HTTP-запросе, так и создавать ссылки. Таким образом, он играет ключевую роль в Nette Application, поскольку не только решает, какой презентер и действие будут выполнять текущий запрос, но также используется для генерации URL в шаблоне и т. д.
Однако маршрутизатор не ограничен только этим использованием, вы можете использовать его в приложениях, где презентеры вообще не используются, для REST API и т. д. Подробнее в разделе Самостоятельное использование.
Коллекция маршрутов
Самый приятный способ определения вида URL-адресов в приложении предлагает класс Nette\Application\Routers\RouteList. Определение состоит из списка так называемых маршрутов, то есть масок URL-адресов и связанных с ними презентеров и действий, с помощью простого API. Маршруты не нужно никак именовать.
$router = new Nette\Application\Routers\RouteList;
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('article/<id>', 'Article:view');
// ...
Пример говорит, что если в браузере открыть https://domain.com/rss.xml
,
отобразится презентер Feed
с действием rss
, если
https://domain.com/article/12
, отобразится презентер Article
с действием
view
и т. д. В случае ненахождения подходящего маршрута Nette Application
реагирует выбрасыванием исключения BadRequestException, которое
отображается пользователю как страница ошибки 404 Not Found.
Порядок маршрутов
Ключевым является порядок, в котором перечислены отдельные маршруты, поскольку они оцениваются последовательно сверху вниз. Действует правило, что маршруты объявляются от специфических к общим:
// НЕПРАВИЛЬНО: 'rss.xml' перехватит первый маршрут и поймет эту строку как <slug>
$router->addRoute('<slug>', 'Article:view');
$router->addRoute('rss.xml', 'Feed:rss');
// ПРАВИЛЬНО
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('<slug>', 'Article:view');
Маршруты оцениваются сверху вниз также при генерации ссылок:
// НЕПРАВИЛЬНО: ссылка на 'Feed:rss' сгенерируется как 'admin/feed/rss'
$router->addRoute('admin/<presenter>/<action>', 'Admin:default');
$router->addRoute('rss.xml', 'Feed:rss');
// ПРАВИЛЬНО
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('admin/<presenter>/<action>', 'Admin:default');
Мы не будем скрывать от вас, что правильное составление маршрутов требует определенного навыка. Пока вы не освоите его, полезным помощником будет панель маршрутизации.
Маска и параметры
Маска описывает относительный путь от корневого каталога сайта. Самой простой маской является статический URL:
$router->addRoute('products', 'Products:default');
Часто маски содержат так называемые параметры. Они указываются в
угловых скобках (например, <year>
) и передаются в целевой
презентер, например, методу renderShow(int $year)
или в персистентный
параметр $year
:
$router->addRoute('chronicle/<year>', 'History:show');
Пример говорит, что если в браузере открыть
https://example.com/chronicle/2020
, отобразится презентер History
с
действием show
и параметром year: 2020
.
Параметрам можно определить значение по умолчанию прямо в маске, и тем самым они станут необязательными:
$router->addRoute('chronicle/<year=2020>', 'History:show');
Маршрут теперь будет принимать и URL https://example.com/chronicle/
, который
снова отобразит History:show
с параметром year: 2020
.
Параметром может быть, конечно, и имя презентера и действия. Например, так:
$router->addRoute('<presenter>/<action>', 'Home:default');
Указанный маршрут принимает, например, URL вида /article/edit
или
/catalog/list
и понимает их как презентеры и действия Article:edit
и
Catalog:list
.
Одновременно он дает параметрам presenter
и action
значения по
умолчанию Home
и default
, и они, следовательно, также
необязательны. Так что маршрут принимает и URL вида /article
и
понимает его как Article:default
. Или наоборот, ссылка на
Product:default
сгенерирует путь /product
, ссылка на стандартный
Home:default
путь /
.
Маска может описывать не только относительный путь от корневого каталога сайта, но и абсолютный путь, если начинается со слеша, или даже полный абсолютный URL, если начинается с двух слешей:
// относительно document root
$router->addRoute('<presenter>/<action>', /* ... */);
// абсолютный путь (относительно домена)
$router->addRoute('/<presenter>/<action>', /* ... */);
// абсолютный URL включая домен (относительно схемы)
$router->addRoute('//<lang>.example.com/<presenter>/<action>', /* ... */);
// абсолютный URL включая схему
$router->addRoute('https://<lang>.example.com/<presenter>/<action>', /* ... */);
Выражения валидации
Для каждого параметра можно установить условие валидации с помощью регулярного выражения.
Например, для параметра id
мы укажем, что он может принимать
только цифры с помощью регулярного выражения \d+
:
$router->addRoute('<presenter>/<action>[/<id \d+>]', /* ... */);
Стандартным регулярным выражением для всех параметров является
[^/]+
, т. е. все, кроме слеша. Если параметр должен принимать и
слеши, укажем выражение .+
:
// принимает https://example.com/a/b/c, path будет 'a/b/c'
$router->addRoute('<path .+>', /* ... */);
Необязательные последовательности
В маске можно обозначать необязательные части с помощью квадратных скобок. Необязательной может быть любая часть маски, в ней могут находиться и параметры:
$router->addRoute('[<lang [a-z]{2}>/]<name>', /* ... */);
// Принимает пути:
// /cs/download => lang => cs, name => download
// /download => lang => null, name => download
Когда параметр является частью необязательной последовательности, он, разумеется, также становится необязательным. Если у него нет указанного значения по умолчанию, то он будет null.
Необязательные части могут быть и в домене:
$router->addRoute('//[<lang=en>.]example.com/<presenter>/<action>', /* ... */);
Последовательности можно произвольно вкладывать и комбинировать:
$router->addRoute(
'[<lang [a-z]{2}>[-<sublang>]/]<name>[/page-<page=0>]',
'Home:default',
);
// Принимает пути:
// /cs/hello
// /en-us/hello
// /hello
// /hello/page-12
При генерации URL стремимся к кратчайшему варианту, поэтому все, что
можно опустить, опускается. Поэтому, например, маршрут index[.html]
генерирует путь /index
. Изменить поведение можно, указав
восклицательный знак после левой квадратной скобки:
// принимает /hello и /hello.html, генерирует /hello
$router->addRoute('<name>[.html]', /* ... */);
// принимает /hello и /hello.html, генерирует /hello.html
$router->addRoute('<name>[!.html]', /* ... */);
Необязательные параметры (т. е. параметры, имеющие значение по умолчанию) без квадратных скобок ведут себя по сути так, как если бы они были заключены в скобки следующим образом:
$router->addRoute('<presenter=Home>/<action=default>/<id=>', /* ... */);
// соответствует этому:
$router->addRoute('[<presenter=Home>/[<action=default>/[<id>]]]', /* ... */);
Если мы хотим повлиять на поведение конечного слеша, чтобы, например,
вместо /home/
генерировалось только /home
, этого можно
достичь так:
$router->addRoute('[<presenter=Home>[/<action=default>[/<id>]]]', /* ... */);
Подстановочные знаки
В маске абсолютного пути мы можем использовать следующие подстановочные знаки и избежать, например, необходимости записывать в маску домен, который может отличаться в среде разработки и production:
%tld%
= домен верхнего уровня, напримерcom
илиorg
%sld%
= домен второго уровня, напримерexample
%domain%
= домен без поддоменов, напримерexample.com
%host%
= весь хост, напримерwww.example.com
%basePath%
= путь к корневому каталогу
$router->addRoute('//www.%domain%/%basePath%/<presenter>/<action>', /* ... */);
$router->addRoute('//www.%sld%.%tld%/%basePath%/<presenter>/<action', /* ... */);
Расширенная запись
Цель маршрута, обычно записываемая в виде Presenter:action
, может быть
также записана с помощью массива, который определяет отдельные
параметры и их значения по умолчанию:
$router->addRoute('<presenter>/<action>[/<id \d+>]', [
'presenter' => 'Home',
'action' => 'default',
]);
Для более детальной спецификации можно использовать еще более
расширенную форму, где кроме значений по умолчанию можно настроить и
другие свойства параметров, например, валидационное регулярное
выражение (см. параметр id
):
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>[/<id>]', [
'presenter' => [
Route::Value => 'Home',
],
'action' => [
Route::Value => 'default',
],
'id' => [
Route::Pattern => '\d+',
],
]);
Важно отметить, что если параметры, определенные в массиве, не указаны в маске пути, их значения нельзя изменить, даже с помощью query-параметров, указанных после вопросительного знака в URL.
Фильтры и переводы
Исходные коды приложения мы пишем на английском языке, но если сайт должен иметь русские URL, то простая маршрутизация типа:
$router->addRoute('<presenter>/<action>', 'Home:default');
будет генерировать английские URL, например /product/123
или
/cart
. Если мы хотим, чтобы презентеры и действия в URL были
представлены русскими словами (например, /продукт/123
или
/корзина
), мы можем использовать словарь перевода. Для его
записи уже нужна “более многословная” версия второго параметра:
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>', [
'presenter' => [
Route::Value => 'Home',
Route::FilterTable => [
// строка в URL => презентер
'produkt' => 'Product',
'korzina' => 'Cart',
'katalog' => 'Catalog',
],
],
'action' => [
Route::Value => 'default',
Route::FilterTable => [
'spisok' => 'list',
],
],
]);
Несколько ключей словаря перевода могут вести на один и тот же презентер. Таким образом, к нему создаются различные псевдонимы. Каноническим вариантом (то есть тем, который будет в сгенерированном URL) считается последний ключ.
Таблицу перевода можно таким образом использовать для любого
параметра. При этом, если перевод не существует, берется исходное
значение. Это поведение можно изменить, добавив
Route::FilterStrict => true
, и маршрут тогда отклонит URL, если значение
отсутствует в словаре.
Кроме словаря перевода в виде массива, можно применить и собственные функции перевода.
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>/<id>', [
'presenter' => [
Route::Value => 'Home',
Route::FilterIn => function (string $s): string { /* ... */ },
Route::FilterOut => function (string $s): string { /* ... */ },
],
'action' => 'default',
'id' => null,
]);
Функция Route::FilterIn
преобразует параметр в URL в строку, которая
затем передается в презентер, функция FilterOut
обеспечивает
преобразование в обратном направлении.
Параметры presenter
, action
и module
уже имеют
предопределенные фильтры, которые преобразуют между стилем PascalCase или
camelCase и kebab-case, используемым в URL. Значение по умолчанию параметров
записывается уже в преобразованном виде, поэтому, например, в случае
презентера пишем <presenter=ProductEdit>
, а не
<presenter=product-edit>
.
Общие фильтры
Помимо фильтров, предназначенных для конкретных параметров, мы можем
определить также общие фильтры, которые получают ассоциативный массив
всех параметров, которые могут как угодно модифицировать и затем
вернуть. Общие фильтры определяем под ключом null
.
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>', [
'presenter' => 'Home',
'action' => 'default',
null => [
Route::FilterIn => function (array $params): array { /* ... */ },
Route::FilterOut => function (array $params): array { /* ... */ },
],
]);
Общие фильтры дают возможность настроить поведение маршрута
абсолютно любым способом. Мы можем использовать их, например, для
модификации параметров на основе других параметров. Например, перевод
<presenter>
и <action>
на основе текущего значения
параметра <lang>
.
Если у параметра определен собственный фильтр и одновременно
существует общий фильтр, выполняется собственный FilterIn
перед
общим и, наоборот, общий FilterOut
перед собственным. То есть внутри
общего фильтра значения параметров presenter
или action
записаны в стиле PascalCase или camelCase.
Односторонние маршруты OneWay
Односторонние маршруты используются для сохранения
функциональности старых URL, которые приложение уже не генерирует, но
все еще принимает. Мы помечаем их флагом OneWay
:
// старый URL /product-info?id=123
$router->addRoute('product-info', 'Product:detail', $router::ONE_WAY);
// новый URL /product/123
$router->addRoute('product/<id>', 'Product:detail');
При доступе к старому URL презентер автоматически перенаправляет на новый URL, так что поисковые системы не проиндексируют эти страницы дважды (см. SEO и канонизация).
Динамическая маршрутизация с callback-функциями
Динамическая маршрутизация с callback-функциями позволяет вам напрямую назначать маршрутам функции (callback), которые будут выполнены при посещении данного пути. Эта гибкая функциональность позволяет быстро и эффективно создавать различные конечные точки (endpoints) для вашего приложения:
$router->addRoute('test', function () {
echo 'вы находитесь по адресу /test';
});
Вы также можете определить в маске параметры, которые автоматически передадутся в ваш callback:
$router->addRoute('<lang cs|en>', function (string $lang) {
echo match ($lang) {
'cs' => 'Добро пожаловать на чешскую версию нашего сайта!',
'en' => 'Welcome to the English version of our website!',
};
});
Модули
Если у нас есть несколько маршрутов, относящихся к общему модулю, мы
используем withModule()
:
$router = new RouteList;
$router->withModule('Forum') // следующие маршруты являются частью модуля Forum
->addRoute('rss', 'Feed:rss') // презентер будет Forum:Feed
->addRoute('<presenter>/<action>')
->withModule('Admin') // следующие маршруты являются частью модуля Forum:Admin
->addRoute('sign:in', 'Sign:in');
Альтернативой является использование параметра module
:
// URL manage/dashboard/default отображается на презентер Admin:Dashboard
$router->addRoute('manage/<presenter>/<action>', [
'module' => 'Admin',
]);
Поддомены
Коллекции маршрутов можно разделять по поддоменам:
$router = new RouteList;
$router->withDomain('example.com')
->addRoute('rss', 'Feed:rss')
->addRoute('<presenter>/<action>');
В имени домена можно использовать и Подстановочные знаки:
$router = new RouteList;
$router->withDomain('example.%tld%')
// ...
Префикс пути
Коллекции маршрутов можно разделять по пути в URL:
$router = new RouteList;
$router->withPath('eshop')
->addRoute('rss', 'Feed:rss') // ловит URL /eshop/rss
->addRoute('<presenter>/<action>'); // ловит URL /eshop/<presenter>/<action>
Комбинации
Вышеупомянутые разделения можно взаимно комбинировать:
$router = (new RouteList)
->withDomain('admin.example.com')
->withModule('Admin')
->addRoute(/* ... */)
->addRoute(/* ... */)
->end()
->withModule('Images')
->addRoute(/* ... */)
->end()
->end()
->withDomain('example.com')
->withPath('export')
->addRoute(/* ... */)
// ...
Query-параметры
Маски могут также содержать query-параметры (параметры после вопросительного знака в URL). Для них нельзя определить валидационное выражение, но можно изменить имя, под которым они передадутся в презентер:
// query-параметр 'cat' мы хотим использовать в приложении под именем 'categoryId'
$router->addRoute('product ? id=<productId> & cat=<categoryId>', /* ... */);
Foo-параметры
Теперь мы углубляемся. Foo-параметры — это, по сути, безымянные
параметры, которые позволяют сопоставлять регулярное выражение.
Примером является маршрут, принимающий /index
, /index.html
,
/index.htm
и /index.php
:
$router->addRoute('index<? \.html?|\.php|>', /* ... */);
Можно также явно определить строку, которая будет использоваться при
генерации URL. Строка должна быть размещена непосредственно за
вопросительным знаком. Следующий маршрут похож на предыдущий, но
генерирует /index.html
вместо /index
, потому что строка
.html
установлена как генерируемое значение:
$router->addRoute('index<?.html \.html?|\.php|>', /* ... */);
Включение в приложение
Чтобы подключить созданный маршрутизатор к приложению, мы должны
сообщить о нем DI-контейнеру. Самый простой способ — подготовить
фабрику, которая создаст объект маршрутизатора, и сообщить в
конфигурации контейнера, что ее нужно использовать. Допустим, для этой
цели мы напишем метод App\Core\RouterFactory::createRouter()
:
namespace App\Core;
use Nette\Application\Routers\RouteList;
class RouterFactory
{
public static function createRouter(): RouteList
{
$router = new RouteList;
$router->addRoute(/* ... */);
return $router;
}
}
В конфигурацию затем запишем:
services:
- App\Core\RouterFactory::createRouter
Любые зависимости, например, от базы данных и т. д., передаются фабричному методу как его параметры с помощью autowiring:
public static function createRouter(Nette\Database\Connection $db): RouteList
{
// ...
}
SimpleRouter
Гораздо более простым маршрутизатором, чем коллекция маршрутов,
является SimpleRouter.
Мы используем его тогда, когда у нас нет особых требований к форме URL,
если недоступен mod_rewrite
(или его альтернативы) или если мы пока не
хотим заниматься красивыми URL.
Генерирует адреса примерно в таком виде:
http://example.com/?presenter=Product&action=detail&id=123
Параметром конструктора SimpleRouter является стандартный презентер и
действие, на который нужно направлять, если мы открываем страницу без
параметров, например, http://example.com/
.
// стандартным презентером будет 'Home' и действие 'default'
$router = new Nette\Application\Routers\SimpleRouter('Home:default');
Рекомендуем SimpleRouter напрямую определить в конфигурации:
services:
- Nette\Application\Routers\SimpleRouter('Home:default')
SEO и канонизация
Фреймворк способствует SEO (оптимизации для поисковых систем),
предотвращая дублирование контента на разных URL. Если к определенной
цели ведет несколько адресов, например, /index
и /index.html
,
фреймворк определяет первый из них как основной (канонический) и
остальные перенаправляет на него с помощью HTTP-кода 301. Благодаря этому
поисковые системы не индексируют страницы дважды и не размывают их
page rank.
Этот процесс называется канонизацией. Каноническим URL является тот, который генерирует маршрутизатор, то есть первый подходящий маршрут в коллекции без флага OneWay. Поэтому в коллекции мы указываем основные маршруты первыми.
Канонизацию выполняет презентер, подробнее в главе канонизация.
HTTPS
Чтобы использовать протокол HTTPS, необходимо включить его на хостинге и правильно настроить сервер.
Перенаправление всего сайта на HTTPS необходимо настроить на уровне сервера, например, с помощью файла .htaccess в корневом каталоге нашего приложения, и это с HTTP-кодом 301. Настройки могут отличаться в зависимости от хостинга и выглядят примерно так:
<IfModule mod_rewrite.c>
RewriteEngine On
...
RewriteCond %{HTTPS} off
RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
...
</IfModule>
Маршрутизатор генерирует URL с тем же протоколом, с которым была загружена страница, так что больше ничего настраивать не нужно.
Однако, если нам в исключительных случаях нужно, чтобы разные маршруты работали под разными протоколами, мы укажем его в маске маршрута:
// Будет генерировать адрес с HTTP
$router->addRoute('http://%host%/<presenter>/<action>', /* ... */);
// Будет генерировать адрес с HTTPS
$router->addRoute('https://%host%/<presenter>/<action>', /* ... */);
Отладка маршрутизатора
Панель маршрутизации, отображаемая в Tracy Bar, является полезным помощником, который показывает список маршрутов, а также параметры, которые маршрутизатор получил из URL.
Зеленая полоса с символом ✓ представляет маршрут, который обработал текущий URL, синим цветом и символом ≈ обозначены маршруты, которые также обработали бы URL, если бы зеленый их не опередил. Далее мы видим текущий презентер и действие.

Одновременно, если происходит неожиданное перенаправление из-за канонизации, полезно посмотреть в панель в строке redirect, где вы узнаете, как маршрутизатор изначально понял URL и почему перенаправил.
При отладке маршрутизатора рекомендуем открыть в браузере Developer Tools (Ctrl+Shift+I или Cmd+Option+I) и в панели Network отключить кеш, чтобы в него не сохранялись перенаправления.
Производительность
Количество маршрутов влияет на скорость маршрутизатора. Их число определенно не должно превышать нескольких десятков. Если у вашего сайта слишком сложная структура URL, вы можете написать собственный Собственный маршрутизатор под свои нужды.
Если маршрутизатор не имеет зависимостей, например, от базы данных, и его фабрика не принимает никаких аргументов, мы можем его скомпилированную форму сериализовать прямо в DI-контейнер и тем самым немного ускорить приложение.
routing:
cache: true
Собственный маршрутизатор
Следующие строки предназначены для очень продвинутых пользователей. Вы можете создать собственный маршрутизатор и совершенно естественно включить его в коллекцию маршрутов. Маршрутизатор — это реализация интерфейса Nette\Routing\Router с двумя методами:
use Nette\Http\IRequest as HttpRequest;
use Nette\Http\UrlScript;
class MyRouter implements Nette\Routing\Router
{
public function match(HttpRequest $httpRequest): ?array
{
// ...
}
public function constructUrl(array $params, UrlScript $refUrl): ?string
{
// ...
}
}
Метод match
обрабатывает текущий запрос $httpRequest, из которого можно получить не только
URL, но и заголовки и т. д., в массив, содержащий имя презентера и его
параметры. Если он не может обработать запрос, возвращает null. При
обработке запроса мы должны вернуть как минимум презентер и действие.
Имя презентера является полным и содержит также возможные модули:
[
'presenter' => 'Front:Home',
'action' => 'default',
]
Метод constructUrl
, наоборот, составляет из массива параметров
итоговый абсолютный URL. Для этого он может использовать информацию из
параметра $refUrl
, который
является текущим URL.
В коллекцию маршрутов его добавите с помощью add()
:
$router = new Nette\Application\Routers\RouteList;
$router->add($myRouter);
$router->addRoute(/* ... */);
// ...
Самостоятельное использование
Под самостоятельным использованием мы подразумеваем использование возможностей маршрутизатора в приложении, которое не использует Nette Application и презентеры. Для него действует почти все, что мы показали в этой главе, со следующими отличиями:
- для коллекций маршрутов мы используем класс Nette\Routing\RouteList
- в качестве простого маршрутизатора класс Nette\Routing\SimpleRouter
- поскольку не существует пары
Presenter:action
, мы используем Расширенная запись
Итак, мы снова создаем метод, который нам составит маршрутизатор, например:
namespace App\Core;
use Nette\Routing\RouteList;
class RouterFactory
{
public static function createRouter(): RouteList
{
$router = new RouteList;
$router->addRoute('rss.xml', [
'controller' => 'RssFeedController',
]);
$router->addRoute('article/<id \d+>', [
'controller' => 'ArticleController',
]);
// ...
return $router;
}
}
Если вы используете DI-контейнер, что мы рекомендуем, снова добавляем метод в конфигурацию, а затем получаем маршрутизатор вместе с HTTP-запросом из контейнера:
$router = $container->getByType(Nette\Routing\Router::class);
$httpRequest = $container->getByType(Nette\Http\IRequest::class);
Или объекты создаем напрямую:
$router = App\Core\RouterFactory::createRouter();
$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals();
Теперь остается только запустить маршрутизатор в работу:
$params = $router->match($httpRequest);
if ($params === null) {
// не найден подходящий маршрут, отправляем ошибку 404
exit;
}
// обрабатываем полученные параметры
$controller = $params['controller'];
// ...
И наоборот, используем маршрутизатор для составления ссылки:
$params = ['controller' => 'ArticleController', 'id' => 123];
$url = $router->constructUrl($params, $httpRequest->getUrl());