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 Framework na to myslí a vychází vývojářům plně vstříc.

Začněme trošku technicky. Router je objekt implementující rozhraní Nette\Routing\Router, který umí rozložit URL na pole parametrů (metoda match) a obráceně z pole parametrů sestavit URL (metoda constructUrl). Proto se taky říká, že router je obousměrný. Nette dává možnost velice elegantním způsobem definovat pravidla, jak přesně mají vypadat URL vaší aplikace.

Router hraje důležitou roli v Nette Application. Ta díky němu zjistí, který presenter a action má vykonat. A také využívá router pro generování URL v šabloně, kde zapíšeme třeba:

<a n:href="Product:detail $productId">detail produktu</a>

a router sestaví z těchto parametrů výslednou URL. Více se dočtete v kapitole vytváření odkazů.

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

Routování tedy představuje samostatnou a důmyslnou vrstvu aplikace, díky které lze tvary URL adres navrhovat nebo měnit klidně až ve chvíli, když je celá aplikace hotová, protože se to obejde bez jediného zásahu do kódu či šablon. Což dává vývojářům obrovskou svobodu.

Kolekce rout

Nejpříjemnější způsob, jak definovat podobu URL adres v aplikaci, nabízí třída Nette\Routing\RouteList, resp. její potomek Nette\Application\Routers\RouteList, který oproti rodiči doplňuje podporu pro presentery, což se nám hodí. 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.

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://jakákoliv-doména.com/rss.xml, zobrazí se presenter Feed s akcí rss, 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(...).

Je důležité, v jakém pořadí jsou routy definovány, protože se zkouší postupně odshora dolů. Platí pravidlo, že routy deklarujeme od specifických po obecné.

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\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:
	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
{
	...
}

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 Router::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>.

Globální filtry

Vedle filtrů určených pro konkrétní parametry můžeme definovat též globální filtry, které obdrží asociativní pole všech parametrů, které mohou jakkoliv modifikovat a poté je vrátí. Globální 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 { ... },
	],
]);

Globální 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 globální filtr, provede se vlastní FILTER_IN před globálním a naopak globální FILTER_OUT před vlastním. Tedy uvnitř globální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|>', ...);

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:

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

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

Routing Debugger

Nebudeme před vámi tajit, že routování je do jisté míry magie, a než do ní proniknete, bude vám dobrým pomocníkem Routing Debugger. Jde o panel zobrazující se v Tracy Baru, které poskytuje přehledný 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.

Cachování rout

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í 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 v parametru $httpRequest (ze kterého lze získat nejen URL) do pole obsahující jméno presenteru a jeho parametry. Pokud požadavek zpracovat neumí, vrátí null.

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

$router = new Nette\Application\Routers\RouteList;
$router->add(new 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());

Související články na blogu

Vylepšit tuto stránku