Nette Documentation Preview

syntax
Маршрутизация
*************

<div class=perex>

Маршрутизатор берёт на себя всё, что связано с URL-адресами, так что вам о них думать не приходится. Мы покажем:

- как настроить маршрутизатор, чтобы URL выглядели так, как вы хотите
- обсудим SEO и перенаправления
- и покажем, как написать собственный маршрутизатор

</div>


Более дружелюбные к человеку URL (их называют также красивыми) удобнее, лучше запоминаются и положительно влияют на SEO. Nette помнит об этом и полностью идёт навстречу потребностям разработчиков. Вы можете спроектировать для своего приложения ровно ту структуру URL, которую хотите. Вы можете спроектировать её даже тогда, когда приложение уже готово, потому что это не требует изменений ни в коде, ни в шаблонах. Она изящно задаётся в [одном месте |#Встраивание], в маршрутизаторе, а не разбросана аннотациями по всем презентерам.

Маршрутизатор в Nette исключителен тем, что он **двунаправленный**. Он умеет и расшифровывать URL из HTTP-запросов, и создавать ссылки. Тем самым он играет ключевую роль в [Nette Application |how-it-works#Nette Application], потому что не только решает, какой презентер и действие выполнят текущий запрос, но и используется для [порождения URL |creating-links] в шаблонах и не только.

Однако маршрутизатор не ограничивается таким применением: вы можете использовать его в приложениях, где презентеры вообще не применяются, для REST API и прочего. Подробности в разделе [#Самостоятельное использование].


Набор маршрутов
===============

Самый приятный способ задать структуру URL-адресов приложения предлагает класс [api:Nette\Application\Routers\RouteList]. Определение состоит из списка так называемых маршрутов, то есть масок URL-адресов и связанных с ними презентеров и действий, через простой API. Никак называть маршруты не нужно.

```php
$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 |api:Nette\Application\BadRequestException], которое показывается пользователю как страница ошибки 404 Not Found.


Порядок маршрутов
-----------------

**Порядок**, в котором перечислены отдельные маршруты, совершенно **решающий**, потому что они проверяются по очереди сверху вниз. Правило в том, что маршруты объявляются **от частных к общим**:

```php
// НЕВЕРНО: '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');
```

Маршруты проверяются сверху вниз и при порождении ссылок:

```php
// НЕВЕРНО: ссылка на '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:

```php
$router->addRoute('products', 'Products:default');
```

Часто маски содержат так называемые **параметры**. Они заключаются в угловые скобки (например, `<year>`) и передаются целевому презентеру, например в метод `renderShow(int $year)` или в постоянный параметр `$year`:

```php
$router->addRoute('chronicle/<year>', 'History:show');
```

Пример показывает, что если мы откроем в браузере `https://example.com/chronicle/2020`, отобразится презентер `History` с действием `show` и параметром `year: 2020`.

Значение по умолчанию для параметров можно задать прямо в маске, и тогда они станут необязательными:

```php
$router->addRoute('chronicle/<year=2020>', 'History:show');
```

Теперь маршрут примет и URL `https://example.com/chronicle/`, который снова отобразит `History:show` с параметром `year: 2020`.

Разумеется, параметрами могут быть и имена презентера и действия. Например:

```php
$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, если начинается с двух слешей:

```php
// относительно корня документов
$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>', /* ... */);
```


Проверочные выражения
---------------------

Для каждого параметра можно задать условие проверки с помощью [регулярного выражения|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. Например, для параметра `id` мы указываем, что он может содержать только цифры, регулярным выражением `\d+`:

```php
$router->addRoute('<presenter>/<action>[/<id \d+>]', /* ... */);
```

Регулярное выражение по умолчанию для всех параметров - `[^/]+`, то есть всё, кроме слеша. Если параметр должен принимать и слеши, мы задаём выражение `.+`:

```php
// принимает https://example.com/a/b/c, путь будет 'a/b/c'
$router->addRoute('<path .+>', /* ... */);
```


Необязательные последовательности
---------------------------------

В маске необязательные части можно пометить квадратными скобками. Необязательной может быть любая часть маски, и она может содержать параметры:

```php
$router->addRoute('[<lang [a-z]{2}>/]<name>', /* ... */);

// Принимает пути:
//    /en/download  => lang => en, name => download
//    /download     => lang => null, name => download
```

Когда параметр входит в необязательную последовательность, он, естественно, тоже становится необязательным. Если у него не задано значение по умолчанию, он будет null.

Необязательные части могут быть и в домене:

```php
$router->addRoute('//[<lang=en>.]example.com/<presenter>/<action>', /* ... */);
```

Последовательности можно вкладывать и сочетать как угодно:

```php
$router->addRoute(
	'[<lang [a-z]{2}>[-<sublang>]/]<name>[/page-<page=0>]',
	'Home:default',
);

// Принимает пути:
// 	/en/hello
// 	/en-us/hello
// 	/hello
// 	/hello/page-12
```

При порождении URL предпочитается самый короткий вариант, поэтому всё, что можно опустить, опускается. Так, например, маршрут `index[.html]` порождает путь `/index`. Это поведение можно перевернуть, поставив после левой квадратной скобки восклицательный знак:

```php
// принимает /hello и /hello.html, порождает /hello
$router->addRoute('<name>[.html]', /* ... */);

// принимает /hello и /hello.html, порождает /hello.html
$router->addRoute('<name>[!.html]', /* ... */);
```

Необязательные параметры (то есть параметры со значением по умолчанию) без квадратных скобок по сути ведут себя так, будто заключены следующим образом:

```php
$router->addRoute('<presenter=Home>/<action=default>/<id=>', /* ... */);

// соответствует этому:
$router->addRoute('[<presenter=Home>/[<action=default>/[<id>]]]', /* ... */);
```

Если мы хотим повлиять на поведение завершающего слеша, чтобы, например, порождался `/home` вместо `/home/`, этого можно добиться так:

```php
$router->addRoute('[<presenter=Home>[/<action=default>[/<id>]]]', /* ... */);
```


Подстановочные знаки
--------------------

В маске абсолютного URL мы можем использовать следующие подстановочные знаки, чтобы, например, не приходилось писать в маске домен, который может различаться в среде разработки и в производственной среде:

- `%tld%` = домен верхнего уровня, например `com` или `org`
- `%sld%` = домен второго уровня, например `example`
- `%domain%` = домен без поддоменов, например `example.com`
- `%host%` = весь хост, например `www.example.com`
- `%basePath%` = путь к корневому каталогу

```php
$router->addRoute('//www.%domain%/%basePath%/<presenter>/<action>', /* ... */);
$router->addRoute('//www.%sld%.%tld%/%basePath%/<presenter>/<action>', /* ... */);
```


Расширенная запись
------------------

Цель маршрута, обычно записываемую в формате `Презентер:действие`, можно записать и массивом, задающим отдельные параметры и их значения по умолчанию:

```php
$router->addRoute('<presenter>/<action>[/<id \d+>]', [
	'presenter' => 'Home',
	'action' => 'default',
]);
```

Для более подробного описания можно использовать ещё более развёрнутую форму, где, помимо значений по умолчанию, можно задать другие свойства параметров, например проверочное регулярное выражение (см. параметр `id`):

```php
use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>[/<id>]', [
	'presenter' => [
		Route::Value => 'Home',
	],
	'action' => [
		Route::Value => 'default',
	],
	'id' => [
		Route::Pattern => '\d+',
	],
]);
```

Важно отметить, что если параметры, заданные в массиве, не перечислены в маске пути, их значения изменить нельзя, даже параметрами запроса, указанными после вопросительного знака в URL.

Это полезно для **фиксированных параметров**: чтобы дать конкретной странице короткий запоминающийся URL. Например, чтобы `/tos` всегда открывал `Article:view` с `id: 123`:

```php
$router->addRoute('tos', [
	'presenter' => 'Article',
	'action' => 'view',
	'id' => 123,
]);
```


Фильтры и переводы
------------------

Исходный код приложения мы пишем по-английски, но если у сайта должны быть чешские URL, то простая маршрутизация вида:

```php
$router->addRoute('<presenter>/<action>', 'Home:default');
```

будет порождать английские URL вроде `/product/123` или `/cart`. Если мы хотим, чтобы презентеры и действия в URL были представлены чешскими словами (например, `/produkt/123` или `/kosik`), мы можем использовать словарь переводов. Для его записи нам уже нужен "более многословный" вариант второго параметра:

```php
use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>', [
	'presenter' => [
		Route::Value => 'Home',
		Route::FilterTable => [
			// строка в URL => презентер
			'produkt' => 'Product',
			'kosik' => 'Cart',
			'katalog' => 'Catalog',
		],
	],
	'action' => [
		Route::Value => 'default',
		Route::FilterTable => [
			'seznam' => 'list',
		],
	],
]);
```

Несколько ключей словаря переводов могут вести к одному презентеру. Так у него возникают разные псевдонимы. Последний ключ считается каноническим вариантом (то есть тем, который окажется в порождённом URL).

Таблицу переводов можно так использовать для любого параметра. Если перевода нет, берётся исходное значение. Это поведение можно изменить, добавив `Route::FilterStrict => true`, и тогда маршрут отклонит URL, если значения нет в словаре.

Помимо словаря переводов в виде массива можно применять и собственные функции перевода.

```php
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. Значение параметров по умолчанию записывается в том виде, в каком оно передаётся приложению (PascalCase для презентера и модуля, camelCase для действия), поэтому, например, в случае презентера мы пишем `<presenter=ProductEdit>`, а не `<presenter=product-edit>`.


Общие фильтры
-------------

Помимо фильтров, предназначенных для конкретных параметров, мы можем задать и общие фильтры, которые получают ассоциативный массив всех параметров, могут как угодно его изменить и вернуть. Общие фильтры задаются под пустым ключом.

```php
use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>', [
	'presenter' => 'Home',
	'action' => 'default',
	'' => [
		Route::FilterIn => function (array $params): array { /* ... */ },
		Route::FilterOut => function (array $params): array { /* ... */ },
	],
]);
```

Общие фильтры дают возможность совершенно как угодно изменить поведение маршрута. Мы можем использовать их, например, чтобы менять параметры на основе других параметров. Скажем, переводить `<presenter>` и `<action>` по текущему значению параметра `<lang>`.

Если у параметра задан собственный фильтр и при этом существует общий, свой `FilterIn` выполняется раньше общего, и наоборот, общий `FilterOut` выполняется раньше своего. Таким образом, внутри общего фильтра значения параметров `presenter` и `action` записаны в стиле PascalCase или camelCase соответственно.

О практическом применении этих фильтров, порождении дружественных к SEO URL вроде `/article/123-how-to-bake-bread` без изменения шаблонов, см. [Красивые URL со слагами |best-practices:pretty-urls].


Флаг OneWay
-----------

Односторонние маршруты служат для сохранения работоспособности старых URL, которые приложение больше не порождает, но всё ещё принимает. Мы помечаем их флагом `OneWay`:

```php
// старый URL /product-info?id=123
$router->addRoute('product-info', 'Product:detail', oneWay: true);
// новый URL /product/123
$router->addRoute('product/<id>', 'Product:detail');
```

При обращении к старому URL презентер автоматически перенаправляет на новый, поэтому поисковые системы не проиндексируют эти страницы дважды (см. [#SEO и канонизация]).


Динамическая маршрутизация с callback-функциями
-----------------------------------------------

Динамическая маршрутизация с callback-функциями позволяет напрямую сопоставить маршрутам функции (callback), которые выполняются при посещении данного пути. Эта гибкая возможность позволяет быстро и эффективно создавать разные точки входа в ваше приложение:

```php
$router->addRoute('test', function () {
	echo 'You are at the /test address';
});
```

Вы можете задать в маске и параметры, которые автоматически передаются в ваш callback:

```php
$router->addRoute('<lang cs|en>', function (string $lang) {
	echo match ($lang) {
		'cs' => 'Welcome to the Czech version of our website!',
		'en' => 'Welcome to the English version of our website!',
	};
});
```

Помимо параметров из маски callback может получать и сервисы из DI-контейнера. Они передаются по типу параметра. Кроме того, параметр `$presenter` получает экземпляр [MicroPresenter |api:NetteModule\MicroPresenter], обрабатывающего маршрут:

```php
$router->addRoute('<lang cs|en>', function (string $lang, Nette\Http\Request $httpRequest, NetteModule\MicroPresenter $presenter) {
	// ...
});
```


Модули
------

Если у нас несколько маршрутов, относящихся к общему [модулю |directory-structure#Презентеры и шаблоны], мы используем `withModule()`. Указанный модуль автоматически подставляется перед презентером каждого маршрута группы и полностью исчезает из URL:

```php
$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:

```php
// URL manage/dashboard/default отображается на презентер Admin:Dashboard
$router->addRoute('manage/<presenter>/<action>', [
	'module' => 'Admin',
]);
```

Имя каждого презентера полно только вместе с его модулем, например `Front:Admin:ProductList`. Всякий раз, когда такое полное имя попадает в параметр URL, маршрутизатор кодирует его по двум простым правилам: каждое двоеточие `:` (разделитель модулей) становится **точкой**, а каждая граница слов в имени PascalCase - **дефисом**. Так `Front:Admin:ProductList` появляется в URL как `front.admin.product-list` и расшифровывается обратно тем же способом. Именно поэтому модульное приложение без перечисленных выше средств порождает URL, полные точек.

И `withModule()`, и параметр `module` этого избегают именно потому, что убирают известный префикс модуля из имени презентера прежде, чем оно попадёт в URL: раз модуль постоянен, кодировать его вообще не нужно.

Иногда мы хотим, чтобы сам модуль менялся и появлялся в URL, и тогда мы берём `<module>` прямо в маску. Осторожно с одной принципиальной деталью: **`<module>` захватывает весь путь модуля**, то есть всё до последнего двоеточия в имени презентера. Для презентера `Shop:Admin:Product` это означает модуль `Shop:Admin` и презентер `Product`, а поскольку двоеточия становятся точками, мы получаем:


Поддомены
---------

Наборы маршрутов можно разделять по поддоменам:

```php
$router = new RouteList;
$router->withDomain('example.com')
	->addRoute('rss', 'Feed:rss')
	->addRoute('<presenter>/<action>');
```

В имени домена тоже можно использовать [#Подстановочные знаки]:

```php
$router = new RouteList;
$router->withDomain('example.%tld%')
	// ...
```


Префикс пути
------------

Наборы маршрутов можно разделять по пути в URL:

```php
$router = new RouteList;
$router->withPath('eshop')
	->addRoute('rss', 'Feed:rss') // соответствует URL /eshop/rss
	->addRoute('<presenter>/<action>'); // соответствует URL /eshop/<presenter>/<action>
```


Сочетания
---------

Перечисленные группировки можно сочетать друг с другом:

```php
$router = (new RouteList)
	->withDomain('admin.example.com')
		->withModule('Admin')
			->addRoute(/* ... */)
			->addRoute(/* ... */)
		->end()
		->withModule('Images')
			->addRoute(/* ... */)
		->end()
	->end()
	->withDomain('example.com')
		->withPath('export')
			->addRoute(/* ... */)
			// ...
```


Параметры запроса
-----------------

Маски могут содержать и параметры запроса (параметры после вопросительного знака в URL). Задать для них проверочное выражение нельзя, но можно изменить имя, под которым они передаются презентеру:

```php
// мы хотим использовать параметр запроса 'cat' в приложении под именем 'categoryId'
$router->addRoute('product ? id=<productId> & cat=<categoryId>', /* ... */);
```


Параметры Foo
-------------

Теперь копнём глубже. Параметры Foo - по сути безымянные параметры, позволяющие сопоставить регулярное выражение. Пример - маршрут, принимающий `/index`, `/index.html`, `/index.htm` и `/index.php`:

```php
$router->addRoute('index<? \.html?|\.php|>', /* ... */);
```

Можно и явно задать строку, которая будет использоваться при порождении URL. Строку нужно поместить сразу после вопросительного знака. Следующий маршрут похож на предыдущий, но порождает `/index.html` вместо `/index`, потому что как значение для порождения задана строка `.html`:

```php
$router->addRoute('index<?.html \.html?|\.php|>', /* ... */);
```


Встраивание
===========

Чтобы встроить созданный маршрутизатор в приложение, нам нужно сообщить о нём DI-контейнеру. Проще всего подготовить фабрику, которая создаст объект маршрутизатора, и сказать контейнеру в конфигурации использовать её. Допустим, для этого мы пишем метод `App\Core\RouterFactory::createRouter()`:

```php
namespace App\Core;

use Nette\Application\Routers\RouteList;

class RouterFactory
{
	public static function createRouter(): RouteList
	{
		$router = new RouteList;
		$router->addRoute(/* ... */);
		return $router;
	}
}
```

Затем мы пишем в [конфигурации |dependency-injection:services]:

```neon
services:
	- App\Core\RouterFactory::createRouter
```

Любые зависимости, например от базы данных, передаются в фабричный метод его параметрами через [autowiring|dependency-injection:autowiring]:

```php
public static function createRouter(Nette\Database\Connection $db): RouteList
{
	// ...
}
```


SimpleRouter
============

Гораздо более простой маршрутизатор, чем набор маршрутов, - [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Мы используем его, когда у нас нет особых требований к формату URL, если `mod_rewrite` (или его аналоги) недоступен либо если мы пока не хотим возиться с красивыми URL.

Он порождает адреса примерно такого вида:

```
http://example.com/?presenter=Product&action=detail&id=123
```

Параметром конструктора `SimpleRouter` служит презентер и действие по умолчанию, то есть действие, которое выполнится, если мы откроем, например, `http://example.com/` без дополнительных параметров.

```php
// презентером по умолчанию будет 'Home', а действием 'default'
$router = new Nette\Application\Routers\SimpleRouter('Home:default');
```

Мы рекомендуем задавать SimpleRouter прямо в [конфигурации |dependency-injection:services]:

```neon
services:
	- Nette\Application\Routers\SimpleRouter('Home:default')
```


SEO и канонизация
=================

Фреймворк способствует SEO (поисковой оптимизации), предотвращая существование одинакового содержимого под разными URL. Если к определённой цели ведут несколько адресов, например `/index` и `/index.html`, фреймворк объявляет первый основным (каноническим) и перенаправляет остальные на него HTTP-кодом 301. Благодаря этому поисковые системы не индексируют страницы дважды и не размывают их вес.

Этот процесс называется канонизацией. Канонический URL - тот, который порождает маршрутизатор, то есть первый подходящий маршрут в наборе без флага OneWay. Поэтому в наборе мы перечисляем **основные маршруты первыми**.

Канонизацию выполняет презентер, подробнее в главе [канонизация |presenters#Канонизация].


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 с тем же протоколом, по которому была загружена страница, поэтому больше ничего задавать не нужно.

Однако если нам в виде исключения нужно, чтобы разные маршруты работали по разным протоколам, мы указываем это в маске маршрута:

```php
// Будет порождать HTTP-адрес
$router->addRoute('http://%host%/<presenter>/<action>', /* ... */);

// Будет порождать HTTPS-адрес
$router->addRoute('https://%host%/<presenter>/<action>', /* ... */);
```


Отладка маршрутизатора
======================

Панель маршрутизации, отображаемая в [Tracy Bar |tracy:], - полезный помощник, показывающий список маршрутов, а также параметры, которые маршрутизатор получил из URL.

Зелёная полоса с символом ✓ обозначает маршрут, который обработал текущий URL; синий цвет и символ ≈ указывают маршруты, которые тоже обработали бы URL, если бы зелёный их не опередил. Далее мы видим текущие презентер и действие.

[* routing-debugger.webp *]

Кроме того, если из-за [канонизации |#SEO и канонизация] происходит неожиданное перенаправление, полезно посмотреть на панель в полосе *redirect*, где можно выяснить, как маршрутизатор изначально понял URL и почему он перенаправил.

.[note]
При отладке маршрутизатора мы рекомендуем открыть инструменты разработчика в браузере (Ctrl+Shift+I или Cmd+Option+I) и отключить кеш в панели Network, чтобы перенаправления в нём не сохранялись.


Производительность
==================

Число маршрутов влияет на скорость работы маршрутизатора. Их число точно не должно превышать нескольких десятков. Если у вашего сайта слишком сложная структура URL, вы можете написать собственный [#Собственный маршрутизатор].

Если у маршрутизатора нет зависимостей, например от базы данных, а его фабрика не принимает аргументов, мы можем сериализовать его скомпилированный вид прямо в DI-контейнер и тем самым немного ускорить приложение.

```neon
routing:
	cache: true
```


Собственный маршрутизатор
=========================

Следующие строки предназначены очень продвинутым пользователям. Вы можете создать собственный маршрутизатор и естественным образом встроить его в набор маршрутов. Маршрутизатор - реализация интерфейса [api:Nette\Routing\Router] с двумя методами:

```php
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 |http:request], из которого можно получить не только URL, но и заголовки и прочее, в массив с именем презентера и его параметрами. Если он не может обработать запрос, он возвращает null. При обработке запроса мы должны вернуть как минимум презентер; действие необязательно и по умолчанию равно `default`, если не указано. Имя презентера полное и включает все модули:

```php
[
	'presenter' => 'Front:Home',
	'action' => 'default',
]
```

Метод `constructUrl`, наоборот, собирает итоговый абсолютный URL из массива параметров. Он может использовать сведения из параметра [`$refUrl`|api:Nette\Http\UrlScript], которым служит текущий URL.

Добавьте его в набор маршрутов методом `add()`:

```php
$router = new Nette\Application\Routers\RouteList;
$router->add($myRouter);
$router->addRoute(/* ... */);
// ...
```


Самостоятельное использование
=============================

Под самостоятельным использованием мы понимаем применение возможностей маршрутизатора в приложении, которое не использует Nette Application и презентеры. К нему относится почти всё, что мы показали в этой главе, со следующими отличиями:

- для наборов маршрутов мы используем класс [api:Nette\Routing\RouteList]
- как простой маршрутизатор - класс [api:Nette\Routing\SimpleRouter]
- поскольку пары `Презентер:действие` не существует, мы используем [#Расширенная запись]

Итак, снова создаём метод, который соберёт нам маршрутизатор, например:

```php
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-запросом из контейнера:

```php
$router = $container->getByType(Nette\Routing\Router::class);
$httpRequest = $container->getByType(Nette\Http\IRequest::class);
```

Или создайте объекты напрямую:

```php
$router = App\Core\RouterFactory::createRouter();
$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals();
```

Теперь остаётся дать маршрутизатору сделать свою работу:

```php
$params = $router->match($httpRequest);
if ($params === null) {
	// подходящий маршрут не найден, отправляем ошибку 404
	exit;
}

// обрабатываем полученные параметры
$controller = $params['controller'];
// ...
```

И наоборот, используйте маршрутизатор для сборки ссылки:

```php
$params = ['controller' => 'ArticleController', 'id' => 123];
$url = $router->constructUrl($params, $httpRequest->getUrl());
```


{{composer: nette/routing}}

Маршрутизация

Маршрутизатор берёт на себя всё, что связано с URL-адресами, так что вам о них думать не приходится. Мы покажем:

  • как настроить маршрутизатор, чтобы URL выглядели так, как вы хотите
  • обсудим SEO и перенаправления
  • и покажем, как написать собственный маршрутизатор

Более дружелюбные к человеку 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, если начинается с двух слешей:

// относительно корня документов
$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, путь будет 'a/b/c'
$router->addRoute('<path .+>', /* ... */);

Необязательные последовательности

В маске необязательные части можно пометить квадратными скобками. Необязательной может быть любая часть маски, и она может содержать параметры:

$router->addRoute('[<lang [a-z]{2}>/]<name>', /* ... */);

// Принимает пути:
//    /en/download  => lang => en, 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',
);

// Принимает пути:
// 	/en/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>]]]', /* ... */);

Подстановочные знаки

В маске абсолютного URL мы можем использовать следующие подстановочные знаки, чтобы, например, не приходилось писать в маске домен, который может различаться в среде разработки и в производственной среде:

  • %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>', /* ... */);

Расширенная запись

Цель маршрута, обычно записываемую в формате Презентер:действие, можно записать и массивом, задающим отдельные параметры и их значения по умолчанию:

$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+',
	],
]);

Важно отметить, что если параметры, заданные в массиве, не перечислены в маске пути, их значения изменить нельзя, даже параметрами запроса, указанными после вопросительного знака в URL.

Это полезно для фиксированных параметров: чтобы дать конкретной странице короткий запоминающийся URL. Например, чтобы /tos всегда открывал Article:view с id: 123:

$router->addRoute('tos', [
	'presenter' => 'Article',
	'action' => 'view',
	'id' => 123,
]);

Фильтры и переводы

Исходный код приложения мы пишем по-английски, но если у сайта должны быть чешские URL, то простая маршрутизация вида:

$router->addRoute('<presenter>/<action>', 'Home:default');

будет порождать английские URL вроде /product/123 или /cart. Если мы хотим, чтобы презентеры и действия в URL были представлены чешскими словами (например, /produkt/123 или /kosik), мы можем использовать словарь переводов. Для его записи нам уже нужен „более многословный“ вариант второго параметра:

use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>', [
	'presenter' => [
		Route::Value => 'Home',
		Route::FilterTable => [
			// строка в URL => презентер
			'produkt' => 'Product',
			'kosik' => 'Cart',
			'katalog' => 'Catalog',
		],
	],
	'action' => [
		Route::Value => 'default',
		Route::FilterTable => [
			'seznam' => '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. Значение параметров по умолчанию записывается в том виде, в каком оно передаётся приложению (PascalCase для презентера и модуля, camelCase для действия), поэтому, например, в случае презентера мы пишем <presenter=ProductEdit>, а не <presenter=product-edit>.

Общие фильтры

Помимо фильтров, предназначенных для конкретных параметров, мы можем задать и общие фильтры, которые получают ассоциативный массив всех параметров, могут как угодно его изменить и вернуть. Общие фильтры задаются под пустым ключом.

use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>', [
	'presenter' => 'Home',
	'action' => 'default',
	'' => [
		Route::FilterIn => function (array $params): array { /* ... */ },
		Route::FilterOut => function (array $params): array { /* ... */ },
	],
]);

Общие фильтры дают возможность совершенно как угодно изменить поведение маршрута. Мы можем использовать их, например, чтобы менять параметры на основе других параметров. Скажем, переводить <presenter> и <action> по текущему значению параметра <lang>.

Если у параметра задан собственный фильтр и при этом существует общий, свой FilterIn выполняется раньше общего, и наоборот, общий FilterOut выполняется раньше своего. Таким образом, внутри общего фильтра значения параметров presenter и action записаны в стиле PascalCase или camelCase соответственно.

О практическом применении этих фильтров, порождении дружественных к SEO URL вроде /article/123-how-to-bake-bread без изменения шаблонов, см. Красивые URL со слагами.

Флаг OneWay

Односторонние маршруты служат для сохранения работоспособности старых URL, которые приложение больше не порождает, но всё ещё принимает. Мы помечаем их флагом OneWay:

// старый URL /product-info?id=123
$router->addRoute('product-info', 'Product:detail', oneWay: true);
// новый URL /product/123
$router->addRoute('product/<id>', 'Product:detail');

При обращении к старому URL презентер автоматически перенаправляет на новый, поэтому поисковые системы не проиндексируют эти страницы дважды (см. SEO и канонизация).

Динамическая маршрутизация с callback-функциями

Динамическая маршрутизация с callback-функциями позволяет напрямую сопоставить маршрутам функции (callback), которые выполняются при посещении данного пути. Эта гибкая возможность позволяет быстро и эффективно создавать разные точки входа в ваше приложение:

$router->addRoute('test', function () {
	echo 'You are at the /test address';
});

Вы можете задать в маске и параметры, которые автоматически передаются в ваш callback:

$router->addRoute('<lang cs|en>', function (string $lang) {
	echo match ($lang) {
		'cs' => 'Welcome to the Czech version of our website!',
		'en' => 'Welcome to the English version of our website!',
	};
});

Помимо параметров из маски callback может получать и сервисы из DI-контейнера. Они передаются по типу параметра. Кроме того, параметр $presenter получает экземпляр MicroPresenter, обрабатывающего маршрут:

$router->addRoute('<lang cs|en>', function (string $lang, Nette\Http\Request $httpRequest, NetteModule\MicroPresenter $presenter) {
	// ...
});

Модули

Если у нас несколько маршрутов, относящихся к общему модулю, мы используем withModule(). Указанный модуль автоматически подставляется перед презентером каждого маршрута группы и полностью исчезает из URL:

$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:

// URL manage/dashboard/default отображается на презентер Admin:Dashboard
$router->addRoute('manage/<presenter>/<action>', [
	'module' => 'Admin',
]);

Имя каждого презентера полно только вместе с его модулем, например Front:Admin:ProductList. Всякий раз, когда такое полное имя попадает в параметр URL, маршрутизатор кодирует его по двум простым правилам: каждое двоеточие : (разделитель модулей) становится точкой, а каждая граница слов в имени PascalCase – дефисом. Так Front:Admin:ProductList появляется в URL как front.admin.product-list и расшифровывается обратно тем же способом. Именно поэтому модульное приложение без перечисленных выше средств порождает URL, полные точек.

И withModule(), и параметр module этого избегают именно потому, что убирают известный префикс модуля из имени презентера прежде, чем оно попадёт в URL: раз модуль постоянен, кодировать его вообще не нужно.

Иногда мы хотим, чтобы сам модуль менялся и появлялся в URL, и тогда мы берём <module> прямо в маску. Осторожно с одной принципиальной деталью: <module> захватывает весь путь модуля, то есть всё до последнего двоеточия в имени презентера. Для презентера Shop:Admin:Product это означает модуль Shop:Admin и презентер Product, а поскольку двоеточия становятся точками, мы получаем:

Поддомены

Наборы маршрутов можно разделять по поддоменам:

$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(/* ... */)
			// ...

Параметры запроса

Маски могут содержать и параметры запроса (параметры после вопросительного знака в URL). Задать для них проверочное выражение нельзя, но можно изменить имя, под которым они передаются презентеру:

// мы хотим использовать параметр запроса '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. Благодаря этому поисковые системы не индексируют страницы дважды и не размывают их вес.

Этот процесс называется канонизацией. Канонический 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 и почему он перенаправил.

При отладке маршрутизатора мы рекомендуем открыть инструменты разработчика в браузере (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. При обработке запроса мы должны вернуть как минимум презентер; действие необязательно и по умолчанию равно default, если не указано. Имя презентера полное и включает все модули:

[
	'presenter' => 'Front:Home',
	'action' => 'default',
]

Метод constructUrl, наоборот, собирает итоговый абсолютный URL из массива параметров. Он может использовать сведения из параметра $refUrl, которым служит текущий URL.

Добавьте его в набор маршрутов методом add():

$router = new Nette\Application\Routers\RouteList;
$router->add($myRouter);
$router->addRoute(/* ... */);
// ...

Самостоятельное использование

Под самостоятельным использованием мы понимаем применение возможностей маршрутизатора в приложении, которое не использует Nette Application и презентеры. К нему относится почти всё, что мы показали в этой главе, со следующими отличиями:

Итак, снова создаём метод, который соберёт нам маршрутизатор, например:

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());