Routování

Router má na starosti vše okolo URL adres, aby vy už jste nad nimi nemuseli přemýšlet. Ukážeme si:

  • jak nastavit router, aby URL byly podle představ
  • povíme si o SEO a přesměrování
  • a ukážeme si, jak napsat vlastní router

Lidštější URL (nebo taky cool či pretty URL) jsou použitelnější, zapamatovatelnější a pozitivně přispívají k SEO. Nette na to myslí a vychází vývojářům plně vstříc. Můžete si pro svou aplikaci navrhnout přesně takovou strukturu URL adres, jakou budete chtít. Můžete ji navrhnout dokonce až ve chvíli, když už je aplikace hotová, protože se to obejde bez zásahů do kódu či šablon. Definuje se totiž elegantním způsobem na jednom jediném místě, v routeru, a není tak roztroušen ve formě anotací ve všech presenterech.

Router v Nette je mimořádný tím, že je obousměrný. Umí jak dekódovat URL v HTTP požadavku, tak i odkazy vytvářet. Hraje tedy zásadní roli v Nette Application, protože jednak rozhoduje o tom, který presenter a action bude vykonávat aktuální požadavek, ale také se využívá pro generování URL v šabloně atd.

Ovšem router není limitován jen pro tohle využití, můžete jej použít v aplikacích, kde se vůbec presentery nepoužívají, pro REST API, atd. Více v části samostatné použití.

Kolekce rout

Nejpříjemnější způsob, jak definovat podobu URL adres v aplikaci, nabízí třída Nette\Application\Routers\RouteList. Definice je tvořena seznamem tzv. rout, tedy masek URL adres a k nim přidružených presenterů a akcí pomocí jednoduchého API. Routy nemusíme nijak pojmenovávat.

$router = new Nette\Application\Routers\RouteList;
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('article/<id>', 'Article:view');
// ...

Ukázka říká, že pokud v prohlížeči otevřeme https://domain.com/rss.xml, zobrazí se presenter Feed s akcí rss, pokud https://domain.com/article/12, zobrazí se presenter Article s akcí view atd. V případě nenalezení vhodné routy reaguje Nette Application vyhozením výjimky BadRequestException, která se uživateli zobrazí jako chybová stránka 404 Not Found.

V Nette 2.x se místo $router->addRoute(...) používalo $router[] = new Route(...).

Pořadí rout

Zcela klíčové je pořadí, v jakém jsou jednotlivé routy uvedeny, protože se vyhodnocují postupně odshora dolů. Platí pravidlo, že routy deklarujeme od specifických po obecné:

// ŠPATNĚ: 'rss.xml' zachytí první routa a chápe tento řetězec jako <slug>
$router->addRoute('<slug>', 'Article:view');
$router->addRoute('rss.xml', 'Feed:rss');

// DOBŘE
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('<slug>', 'Article:view');

Routy se vyhodnocují odshora dolů také při generování odkazů:

// ŠPATNĚ: odkaz na 'Feed:rss' vygeneruje jako 'admin/feed/rss'
$router->addRoute('admin/<presenter>/<action>', 'Admin:default');
$router->addRoute('rss.xml', 'Feed:rss');

// DOBŘE
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('admin/<presenter>/<action>', 'Admin:default');

Nebudeme před vámi tajit, že správné sestavení rout vyžaduje jistou dovednost. Než do ní proniknete, bude vám užitečným pomocníkem routovací panel.

Maska a parametry

Maska popisuje relativní cestu od kořenového adresáře webu. Nejjednodušší maskou je statická URL:

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

Často masky obsahují tzv. parametry. Ty jsou uvedeny ve špičatých závorkách (např. <year>) a jsou předány do cílového presenteru, například metodě renderShow(int $year) nebo do persistentního parametru $year:

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

Ukázka říká, že pokud v prohlížeči otevřeme https://example.com/chronicle/2020, zobrazí se presenter History s akcí show a parametrem year => 2020.

Parametrům můžeme určit výchozí hodnotu přímo v masce a tím se stanou volitelné:

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

Routa bude nyní akceptovat i URL https://example.com/chronicle/, které opět zobrazí History:show s parametrem year => 2020.

Parametrem může být samozřejmě i jméno presenteru a akce. Třeba takto:

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

Uvedená routa akceptuje např. URL ve tvaru /article/edit nebo také /catalog/list a chápe je jako presentery a akce Article:edit a Catalog:list.

Zaroveň dává parametrům presenter a action výchozí hodnoty Homepage a default a jsou tedy také volitelné. Takže routa akceptuje i URL ve tvaru /article a chápe ji jako Article:default. Nebo obráceně, odkaz na Product:default vygeneruje cestu /product, odkaz na výchozí Homepage:default cestu /.

Maska může popisovat nejen relativní cestu od kořenového adresáře webu, ale také absolutní cestu, pokud začíná lomítkem, nebo dokonce celé absolutní URL, začíná-li dvěma lomítky:

// relativně k document rootu
$router->addRoute('<presenter>/<action>', /* ... */);

// absolutní cesta (relativní k doméně)
$router->addRoute('/<presenter>/<action>', /* ... */);

// absolutní URL včetně domény (relativní k schématu)
$router->addRoute('//<lang>.example.com/<presenter>/<action>', /* ... */);

// absolutní URL včetně schématu
$router->addRoute('https://<lang>.example.com/<presenter>/<action>', /* ... */);

Validační výrazy

Pro každý parametr lze stanovit validační podmínku pomocí regulárního výrazu. Například parametru id určíme, že může nabývat pouze číslic pomocí reguláru \d+:

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

Výchozím regulárním výrazem pro všechny parametry je [^/]+, tj. vše kromě lomítka. Pokud má parametr přijímat i lomítka, uvedeme výraz .+:

// akceptuje https://example.com/a/b/c, path bude 'a/b/c'
$router->addRoute('<path .+>', /* ... */);

Volitelné sekvence

V masce lze označovat volitelné části pomocí hranatých závorek. Volitelná může být libovolná část masky, mohou se v ní nacházet i parametry:

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

// Akceptuje cesty:
//    /cs/download  => lang => cs, name => download
//    /download     => lang => null, name => download

Když je parametr součásti volitelné sekvence, stává se pochopitelně také volitelným. Pokud nemá uvedenou výchozí hodnotu, tak bude null.

Volitelné části mohou být i v doméně:

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

Sekvence je možné libovolně zanořovat a kombinovat:

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

// Akceptuje cesty:
// 	/cs/hello
// 	/en-us/hello
// 	/hello
// 	/hello/page-12

Při generování URL se usiluje o nejkratší variantu, takže všechno, co lze vynechat, se vynechá. Proto třeba routa index[.html] generuje cestu /index. Obrátit chování je možné uvedením vykřičníku za levou hranatou závorkou:

// akceptuje /hello i /hello.html, generuje /hello
$router->addRoute('<name>[.html]', /* ... */);

// akceptuje /hello i /hello.html, generuje /hello.html
$router->addRoute('<name>[!.html]', /* ... */);

Volitelné parametry (tj. parametry mající výchozí hodnotu) bez hranatých závorek se chovají v podstatě tak, jako by byly uzávorkovány následujícím způsobem:

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

// odpovídá tomuto:
$router->addRoute('[<presenter=Homepage>/[<action=default>/[<id>]]]', /* ... */);

Pokud bychom chtěli ovlivnit chování koncového lomítka, aby se např. místo /homepage/ generovalo jen /homepage, lze toho docílit takto:

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

Zástupné znaky

V masce absolutní cesty můžeme použít následující zástupné znaky a vyhnout se tak např. nutnosti zapisovat do masky doménu, která se může lišit ve vývojovém a produkčním prostředí:

  • %tld% = top level domain, např. com nebo org
  • %sld% = second level domain, např. example
  • %domain% = doména bez subdomén, např. example.com
  • %host% = celý host, např. www.example.com
  • %basePath% = cesta ke kořenovému adresáři
$router->addRoute('//www.%domain%/%basePath%/<presenter>/<action>', /* ... */);
$router->addRoute('//www.%sld%.%tld%/%basePath%/<presenter>/<action', /* ... */);

Rozšířený zápis

Druhý parametr routy, který často zapisujeme ve formátu Presenter:action, je zkratkou, kterou můžeme zapsat také ve formě pole, kde přímo uvádíme (výchozí) hodnoty jednotlivých parametrů:

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

Nebo můžeme použít tuto formu, všimněte si přepisu validačního regulárního výrazu:

use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>[/<id>]', [
	'presenter' => [
		Route::VALUE => 'Homepage',
	],
	'action' => [
		Route::VALUE => 'default',
	],
	'id' => [
		Route::PATTERN => '\d+',
	],
]);

Tyto upovídanější formáty se hodí pro doplnění dalších metadat.

Filtry a překlady

Zdrojové kódy aplikace píšeme v angličtině, ale pokud má mít web české URL, pak jednoduché routování typu:

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

bude generovat anglické URL, jako třeba /product/123 nebo /cart. Pokud chceme mít presentery a akce v URL reprezentované českými slovy (např. /produkt/123 nebo /kosik), můžeme využít překladového slovníku. Pro jeho zápis už potřebujeme „upovídanější“ variantu druhého parametru:

use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>', [
	'presenter' => [
		Route::VALUE => 'Homepage',
		Route::FILTER_TABLE => [
			// řetězec v URL => presenter
			'produkt' => 'Product',
			'kosik' => 'Cart',
			'katalog' => 'Catalog',
		],
	],
	'action' => [
		Route::VALUE => 'default',
		Route::FILTER_TABLE => [
			'seznam' => 'list',
		],
	],
]);

Více klíčů překladového slovníku může vést na tentýž presenter. Tím se k němu vytvoří různé aliasy. Za kanonickou variantu (tedy tu, která bude ve vygenerovaném URL) se považuje poslední klíč.

Překladovou tabulku lze tímto způsobem použít na jakýkoliv parametr. Přičemž pokud překlad neexistuje, bere se původní hodnota. Tohle chování můžeme změnit doplněním Route::FILTER_STRICT => true a routa pak odmítne URL, pokud hodnota není ve slovníku.

Kromě překladového slovníku v podobě pole lze nasadit i vlastní překladové funkce.

use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>/<id>', [
	'presenter' => [
		Route::VALUE => 'Homepage',
		Route::FILTER_IN => function (string $s): string { /* ... */ },
		Route::FILTER_OUT => function (string $s): string { /* ... */ },
	],
	'action' => 'default',
	'id' => null,
]);

Funkce Route::FILTER_IN převádí mezi parametrem v URL a řetězcem, který se poté předá do presenteru, funkce FILTER_OUT zajišťuje převod opačným směrem.

Parametry presenter, action a module už mají předdefinované filtry, které převádějí mezi stylem PascalCase resp. camelCase a kebab-case používaným v URL. Výchozí hodnota parametrů se zapisuje už v transformované podobě, takže třeba v případě presenteru píšeme <presenter=ProductEdit>, nikoliv <presenter=product-edit>.

Obecné filtry

Vedle filtrů určených pro konkrétní parametry můžeme definovat též obecné filtry, které obdrží asociativní pole všech parametrů, které mohou jakkoliv modifikovat a poté je vrátí. Obecné filtry definujeme pod klíčem null.

use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>', [
	'presenter' => 'Homepage',
	'action' => 'default',
	null => [
		Route::FILTER_IN => function (array $params): array { /* ... */ },
		Route::FILTER_OUT => function (array $params): array { /* ... */ },
	],
]);

Obecné filtry dávají možnost upravit chování routy naprosto jakýmkoliv způsobem. Můžeme je použít třeba pro modifikaci parametrů na základě jiných parametrů. Například přeložení <presenter> a <action> na základě aktuální hodnoty parametru <lang>.

Pokud má parametr definovaný vlastní filtr a současně existuje obecný filtr, provede se vlastní FILTER_IN před obecným a naopak obecný FILTER_OUT před vlastním. Tedy uvnitř obecného filtru jsou hodnoty parametrů presenter resp. action zapsané ve stylu PascalCase resp. camelCase.

Jednosměrky ONE_WAY

Jednosměrné routy se používají pro zachování funkčnosti starých URL, které už aplikace negeneruje, ale stále přijímá. Označíme je příznakem ONE_WAY:

// staré URL /product-info?id=123
$router->addRoute('product-info', 'Product:detail', $router::ONE_WAY);
// nové URL /product/123
$router->addRoute('product/<id>', 'Product:detail');

Při přístupu na starou URL presenter automaticky přesměruje na nové URL, takže vám tyto stránky vyhledávače nezaindexují dvakrát (viz SEO a kanonizace).

Moduly

Pokud máme více rout, které spadají do společného modulu, využijeme withModule():

$router = new RouteList;
$router->withModule('Forum') // následující routy jsou součástí modulu Forum
	->addRoute('rss', 'Feed:rss') // presenter bude Forum:Feed
	->addRoute('<presenter>/<action>')

	->withModule('Admin') // následující routy jsou součástí modulu Forum:Admin
		->addRoute('sign:in', 'Sign:in');

Alternativou je použití parametru module:

// URL manage/dashboard/default se mapuje na presenter Admin:Dashboard
$router->addRoute('manage/<presenter>/<action>', [
	'module' => 'Admin'
]);

Subdomény

Kolekce rout můžeme členit podle subdomén:

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

V názvu domény lze použít i zástupné znaky:

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

Prefix cesty

Kolekce rout můžeme členit podle cesty v URL:

$router = new RouteList;
$router->withPath('eshop')
	->addRoute('rss', 'Feed:rss') // chytá URL /eshop/rss
	->addRoute('<presenter>/<action>'); // chytá URL /eshop/<presenter>/<action>

Kombinace

Výše uvedené členění můžeme vzájemně kombinovat:

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

Query parametry

Masky mohou také obsahovat query parametry (parametry za otazníkem v URL). Těm nelze definovat validační výraz, ale lze změnit název, pod kterým se předají do presenteru:

// query parametr 'cat' chceme v aplikaci použít pod názvem 'categoryId'
$router->addRoute('product ? id=<productId> & cat=<categoryId>', /* ... */);

Foo parametry

Nyní už jdeme hlouběji. Foo parametry jsou v podstatě nepojmenované parametry, které umožňují matchovat regulární výraz. Příkladem je routa akceptující /index, /index.html, /index.htm a /index.php:

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

Lze také explicitně definovat řetězec, který bude použit při generování URL. Řetězec musí být umístěn přímo za otazníkem. Následující routa je podobná předchozí, ale generuje /index.html namísto /index, protože řetězec .html je nastaven jako generovací hodnota:

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

Začlenění do aplikace

Abychom vytvořený router zapojili do aplikace, musíme o něm říci DI kontejneru. Nejsnazší cesta je připravit továrnu, která objekt routeru vyrobí, a sdělit v konfiguraci kontejneru, že ji má použít. Dejme tomu, že k tomu účelu napíšeme metodu App\Router\RouterFactory::createRouter():

namespace App\Router;

use Nette\Application\Routers\RouteList;

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

Do konfigurace pak zapíšeme:

services:
	- App\Router\RouterFactory::createRouter

V případě Nette 3.0:

services:
	router: App\Router\RouterFactory::createRouter

Jakékoliv závislosti, třeba na databázi atd, se předají tovární metodě jako její parametry pomocí autowiringu:

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

SimpleRouter

Mnohem jednodušším routerem než kolekce rout je SimpleRouter. Použijeme jej tehdy, pokud nemáme zvláštní nároky na tvar URL, pokud není k dispozici mod_rewrite (nebo jeho alternativy) nebo pokud zatím nechceme řešit hezké URL.

Generuje adresy zhruba v tomto tvaru:

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

Parametrem konstruktoru SimpleRouteru je výchozí presenter & akce, na který se má směřovat, pokud otevřeme stránku bez parametrů, např. http://example.com/.

// výchozím presenterem bude 'Homepage' a akce 'default'
$router = new Nette\Application\Routers\SimpleRouter('Homepage:default');

Doporučujeme SimpleRouter přímo definovat v konfiguraci:

services:
	- Nette\Application\Routers\SimpleRouter('Homepage:default')

SEO a kanonizace

Framework přispívá k SEO (optimalizaci nalezitelnosti na internetu) tím, že zabraňuje duplicitě obsahu na různých URL. Pokud k určitému cíli vede více adres, např. /index a /index.html, framework první z nich určí za primární (kanonickou) a ostatní na ni přesměruje pomocí HTTP kódu 301. Díky tomu vám vyhledávače stránky nezaindexují dvakrát a nerozmělní jejich page rank.

Tomuto procesu se říká kanonizace. Kanonickou URL je ta, kterou vygeneruje router, tj. první vyhovující routa v kolekci bez příznaku ONE_WAY. Proto v kolekci uvádíme primární routy jako první.

Kanonizaci provádí presenter, více v kapitole kanonizace.

HTTPS

Abychom mohli používat HTTPS protokol, je nutné ho povolit na hostingu a správně si nakonfigurovat server.

Přesměrování celého webu na HTTPS je nutné nastavit na úrovni serveru, například pomocí souboru .htaccess v kořenovém adresáři naší aplikace, a to s HTTP kódem 301. Nastavení se může lišit podle hostingu a vypadá cca takto:

<IfModule mod_rewrite.c>
	RewriteEngine On
	...
	RewriteCond %{HTTPS} off
	RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
	...
</IfModule>

Router generuje URL se stejným protokolem, s jakým byla stránka načtena, takže nic víc není pořeba nastavovat.

Pokud ale výjimečně potřebujeme, aby různé routy běžely pod různými protokoly, uvedeme ho v masce routy:

// Bude generovat adresu s HTTP
$router->addRoute('http://%host%/<presenter>/<action>', /* ... */);

// Bude generovat adresu s HTTPs
$router->addRoute('https://%host%/<presenter>/<action>', /* ... */);

Ladění routeru

Routovací panel zobrazující se v Tracy Baru je užitečným pomocníkem, který zobrazuje seznam rout a také parametrů, které router získal z URL.

Zelený pruh se symbolem ✓ představuje routu, která zpracovala aktuální URL, modrou barvou a symbolem ≈ jsou označené routy, které by také URL zpracovaly, kdyby je zelená nepředběhla. Dále vidíme aktuální presenter & akci.

Zároveň pokud dojde k neočekávanému přesměrování kvůli kanonizaci, je užitečné se podívat do panelu v liště redirect, kde zjistíte, jak router URL původně pochopil a proč přesměroval.

Při ladění routeru doporučujeme otevřít v prohlížeči Developer Tools (Ctrl+Shift+I nebo Cmd+Option+I) a v panelu Network vypnout cache, aby se do ní neukládaly přesměrování.

Výkonnost

Počet rout má vliv na rychlost routeru. Jejich počet by rozhodně neměl přesáhnout několik desítek. Pokud má váš web příliš komplikovanou strukturu URL, můžete si napsat na míru vlastní router.

Pokud router nemá žádné závislosti, například na databázi, a jeho továrna nepřijímá žádné argumenty, můžeme jeho sestavenou podobu serializovat přímo do DI kontejneru a tím aplikaci mírně zrychlit.

routing:
	cache: true

Vlastní router

Následující řádky jsou určeny pro velmi pokročilé uživatele. Můžete si vytvořit router vlastní a zcela přirozeně ho začlenit do kolekce rout. Router je implementací rozhraní Nette\Routing\Router se dvěma metodami:

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
	{
		// ...
	}
}

Metoda match zpracuje aktuální požadavek $httpRequest, ze kterého lze získat nejen URL, ale i hlavičky atd., do pole obsahující název presenteru a jeho parametry. Pokud požadavek zpracovat neumí, vrátí null. Při zpracování požadavku musíme vrátit minimálně presenter a akci. Název presenteru je úplný a obsahuje i případné moduly:

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

Metoda constructUrl naopak sestaví z pole parametrů výsledné absolutní URL. K tomu může využít informace z parametru $refUrl, což je aktuální URL.

Do kolekce rout ho přidáte pomocí add():

$myRouter = new MyRouter; // nebo jej získáme přes dependency injection
$router = new Nette\Application\Routers\RouteList;
$router->add($myRouter);
$router->addRoute(/* ... */);
// ...

Samostatné použití

Samostatným použitím myslíme využití schopností routeru v aplikaci, která nevyužívá Nette Application a presentery. Platí pro něj téměř vše, co jsme si v této kapitole ukázali, s těmito odlišnostmi:

Takže opět si vytvoříme metodu, která nám sestaví router, např.:

namespace App\Router;

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;
	}
}

Pokud používáte DI kontejner, což doporučujeme, opět metodu přidáme do konfigurace a poté router společne s HTTP požadavkem získáme z kontejneru:

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

Anebo objekty přímo vyrobíme:

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

Teď už zbývá pustit router k práci:

$params = $router->match($httpRequest);
if ($params === null) {
	// nebyla nalezena vyhovující routa, odešleme chybu 404
	exit;
}

// zpracujeme získané parametry
$controller = $params['controller'];
// ...

A obráceně použijeme router k sestavení odkazu:

$params = ['controller' => 'ArticleController', 'id' => 123];
$url = $router->constructUrl($params, $httpRequest->getUrl());
verze: 4.0 3.x 2.x