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.

Kolekce rout

Nejpříjemnější způsob, jak definovat podobu URL adres v aplikaci, nabízí třídy Nette\Application\Routers\RouteList a Nette\Application\Routers\Route. 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.

use Nette\Application\Routers\Route;

$router = new Nette\Application\Routers\RouteList;
$router[] = new Route('rss.xml', 'Feed:rss');
$router[] = new Route('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.

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 řetězec jako <slug>
$router[] = new Route('<slug>', 'Article:view');
$router[] = new Route('rss.xml', 'Feed:rss');

// DOBŘE
$router[] = new Route('rss.xml', 'Feed:rss');
$router[] = new Route('<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[] = new Route('admin/<presenter>/<action>', 'Admin:default');
$router[] = new Route('rss.xml', 'Feed:rss');

// DOBŘE
$router[] = new Route('rss.xml', 'Feed:rss');
$router[] = new Route('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[] = new Route('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[] = new Route('chronicle/<year>', 'History:show');

http://example.com/…t/detail/123


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

```php
$router[] = new Route('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[] = new Route('<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[] = new Route('<presenter>/<action>', /* ... */);

// absolutní cesta (relativní k doméně)
$router[] = new Route('/<presenter>/<action>', /* ... */);

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

// absolutní URL včetně schématu
$router[] = new Route('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[] = new Route('<presenter>/<action>[<id \d+>|doc: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[] = new Route('<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[] = new Route('[<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[] = new Route('//[<lang=en>.|doc:lang-en]example.com/<presenter>/<action>', /* ... */);

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

$router[] = new Route(
	'[<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[] = new Route('<name>[.html|doc:html]', /* ... */);

// akceptuje /hello i /hello.html, generuje /hello.html
$router[] = new Route('<name>[!.html|doc: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[] = new Route('<presenter=Homepage>/<action=default>/<id=>', /* ... */);

// odpovídá tomuto:
$router[] = new Route('[<presenter=Homepage>/[<action=default>/[<id>|/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[] = new Route('[<presenter=Homepage>[/<action=default>[<id>|/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[] = new Route('//www.%domain%/%basePath%/<presenter>/<action>', /* ... */);
$router[] = new Route('//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[] = new Route('<presenter>/<action>[<id \d+>|doc:id-d]', [
	'presenter' => 'Homepage',
	'action' => 'default',
|doc: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:

$router[] = new Route('<presenter>/<action>[<id>|/id]', [
	'presenter' => [
		Route::VALUE => 'Homepage',
	|doc:route-value-homepage],
	'action' => [
		Route::VALUE => 'default',
	|doc:route-value-default],
	'id' => [
		Route::PATTERN => '\d+',
	|doc: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[] = new Route('<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:

$router[] = new Route('<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',
		|doc: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.

$router[] = new Route('<presenter>/<action>/<id>', [
	'presenter' => [
		Route::VALUE => 'Homepage',
		Route::FILTER_IN => function ($s) { /* ... */ },
		Route::FILTER_OUT => function ($s) { /* ... */ },
	],
	'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.

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

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[] = new Route('product-info', 'Product:detail', Route::ONE_WAY);
// nové URL /product/123
$router[] = new Route('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é chceme zařadit do jednoho modulu, můžeme využít RouteList s názvem modulu v konstruktoru:

class RouterFactory
{
	/**
	 * @return Nette\Application\IRouter
	 */
	public static function createRouter()
	{
		$router = new RouteList;
		$router[] = self::createForumRouter();
		// ...
		$router[] = new Route('<presenter>/<action>', 'Homepage:default');
		return $router;
	}

	public static function createForumRouter()
	{
		$router = new RouteList('Forum');
		// http://forum.example.com/homepage/default se mapuje na presenter Forum:Homepage
		$router[] = new Route('//forum.example.com/<presenter>/<action>');
		return $router;
	}
}

Nebo můžeme využít parametr module u třídy Route:

// URL manage/dashboard/default se mapuje na presenter Admin:Dashboard
new Route('manage/<presenter>/<action>', [
	'module' => 'Admin'
|doc:module-admin]);

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[] = new Route('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[] = new Route('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[] = new Route('index<?.html \.html?|\.php|>', /* ... */);

Začlenění do aplikace

Abychom takto 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\RouterFactory::createRouter():

namespace App;

use Nette\Application\Routers\RouteList;
use Nette\Application\Routers\Route;

class RouterFactory
{
	/**
	 * @return Nette\Application\IRouter
	 */
	public static function createRouter()
	{
		$router = new RouteList;
		$router[] = new Route(/* ... */);
		return $router;
	}
}

Do konfigurace pak zapíšeme:

services:
	router: App\RouterFactory::createRouter

Velkou výhodou je, že celý router se definuje na jednom místě a není tak roztroušen ve formě anotací ve všech presenterech.

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

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

// Použije se stejný protokol, s jakým byla stránka načtena
$router[] = new Route('//%host%/<presenter>/<action>', /* ... */);

// Bude generovat adresu s HTTP
$router[] = new Route('http://%host%/<presenter>/<action>', /* ... */);

// Bude generovat adresu s HTTPs
$router[] = new Route('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\Application\IRouter se dvěma metodami:

use Nette\Application\Request as AppRequest;
use Nette\Http\IRequest as HttpRequest;
use Nette\Http\Url;

class MyRouter implements Nette\Application\IRouter
{
	public function match(HttpRequest $httpRequest)
	{
		// ...
	}

	public function constructUrl(AppRequest $appRequest, Url $refUrl)
	{
		// ...
	}
}

Metoda match zpracuje aktuální požadavek $httpRequest (ze kterého lze získat nejen URL) do interního požadavku Nette\Application\Request obsahující jméno presenteru a jeho parametry. Pokud požadavek zpracovat neumí, vrátí null.

Metoda constructUrl naopak sestaví z interního požadavku výsledné absolutní URL. K tomu může využít informace z parametru $refUrl.

verze: 4.0 3.x 2.x