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
neboorg
%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
Cíl routy obvykle zapisovaný ve tvaru Presenter:action
může být také zapsát pomocí pole, které definuje
jednotlivé parametry a jejich výchozí hodnoty:
$router->addRoute('<presenter>/<action>[/<id \d+>]', [
'presenter' => 'Homepage',
'action' => 'default',
]);
Pro detailnější specifikaci lze použít ještě rozšířenější formu, kde kromě výchozích hodnot můžeme nastavit
i další vlastnosti parametrů, jako třeba validační regulární výraz (viz parametr id
):
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>[/<id>]', [
'presenter' => [
Route::VALUE => 'Homepage',
],
'action' => [
Route::VALUE => 'default',
],
'id' => [
Route::PATTERN => '\d+',
],
]);
Je důležité poznamenat, že pokud parametry definované v poli nejsou uvedeny v masce cesty, jejich hodnoty nelze změnit, ani pomocí query parametrů uvedených za otazníkem v URL.
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\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;
}
}
Do konfigurace pak zapíšeme:
services:
- App\Core\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()
:
$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:
- pro kolekce rout používáme třídu Nette\Routing\RouteList
- jako simple router třídu Nette\Routing\SimpleRouter
- protože neexistuje dvojice
Presenter:action
, používáme rozšířený zápis
Takže opět si vytvoříme metodu, která nám sestaví router, např.:
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;
}
}
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\Core\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());