Routování URL

Routování je obousměrné překládání mezi URL a akcí presenteru. Obousměrným myslíme možnost z URL odvodit akci presenteru, ale také obráceně, k akci vygenerovat odpovídající URL. Ukážeme si:

  • jak zapisovat routy a vytvářet odkazy
  • povíme si o SEO přesměrování
  • jak routy debugovat
  • jak napsat vlastní router

Co je routování?

Routování je obousměrné překládání mezi URL a aplikačním požadavkem.

  • Nette\Http\IRequest (obsahuje URL) → Nette\Application\Request
  • Nette\Application\Request → absolutní URL

Díky obousměrnému routování už nemusíme do šablon natvrdo zapisovat URL, odkazujeme se pouze na akce presenterů a framework nám už URL vygeneruje sám:

{* vytvoří odkaz na presenter presenter 'Product' a jeho akci 'detail' *}
<a n:href="Product:detail $productId">detail produktu</a>

Více se dozvíte na stránce o vytváření odkazů.

Routování představuje samostatnou vrstvu aplikace. Tvar URL můžeme vymýšlet klidně až ve chvíli, když je celá aplikace hotová. Stejně tak je velmi snadné je kdykoliv změnit. A přitom nejen zachovat původní adresy v platnosti, ale automaticky je nechat přesměrovávat na URL nová. Ruku na srdce, kdo z vás to má? :-)

SimpleRouter

Požadovaný tvar URL adres určuje router. Nejjednodušším případem routeru 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.

Generované adresy budou zhruba v tomto tvaru:

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

Prvním parametrem konstruktoru SimpleRouteru je výchozí akce presenteru, tj. akce, která se má provést, pokud otevřeme stránku např. http://example.com/ bez dalších parametrů.

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

Druhým a nepovinným parametrem konstruktoru je možnost předat dodatečné příznaky (SimpleRouter::ONE_WAY pro jednosměrné routy).

Doporučovaným způsobem konfigurace rout v aplikaci je zaregistrování SimpleRouteru v konfiguračním souboru (e.g. config.neon):

services:
    application.router: Nette\Application\Routers\SimpleRouter('Homepage:default')

Route: za hezčí URL

Lidštější URL (nebo taky cool, pretty či user-friendly URL) bývají použitelnější a zapamatovatelnější a pozitivně přispívají k SEO. Nette Framework na současné trendy myslí a vychází v tomto vývojářům plně vstříc.

Všechny požadavky musí být zpracovávány souborem index.php. Toho můžeme dosáhnout napříkad konfigurací mod_rewrite při použití Apache nebo direktivou try_files pokud používáme Nginx (viz jak nastavit server pro hezká URL) .

Třídu Route je možné použít k vytváření adres takřka libovolného tvaru. Začněme příkladem, který bude generovat pro akci Product:detail s id = 123 URL v tomto tvaru:

http://example.com/product/detail/123

Vytvoříme objekt Route (tzv. routu), jehož prvním parametrem je maska cesty a druhým výchozí akce presenteru zapsaná jako řetězec nebo pole. Třetím parametrem mohou být dodatečné příznaky.

// výchozí akcí bude presenter Homepage a akce default
$route = new Route('<presenter>/<action>[/<id>]', 'Homepage:default');

// alternativně zapsáno polem
$route = new Route('<presenter>/<action>[/<id>]', [
    'presenter' => 'Homepage',
    'action'    => 'default'
]);

Uvedená routa je použitelná pro libovolné presentery a akce. Akceptuje cestu např. ve tvaru /article/edit/10 nebo také /catalog/list, protože část s parameterem id je uzavřena do hranatých závorek, čímž říkáme, že je nepovinná.

Protože i ostatní parametry (presenter, action) mají výchozí hodnotu (tj. Homepage a default), jsou volitelné. Pokud jejich hodnota bude shodná s výchozí, v URL se vynechají. Takže odkaz na Product:default vygeneruje cestu http://example.com/product, odkaz na výchozí Homepage:default cestu http://example.com/.

Maska cesty

Nejjednodušší maskou je statická URL s uvedenou cílovou akcí presenteru.

$route = new Route('products', 'Products:default');

Většina reálných masek však obsahuje proměnlivé parametry. Ty jsou uvedeny ve špičatých závorkách (např. <year>) a jsou předány do cílového presenteru.

$route = new Route('history/<year>', 'History:view');

Masky mohou také obsahovat tradiční GET parametry (parametry za otazníkem v URL). Pro tyto však nelze použít validační (např. regulární výraz) a další složitější struktury, nicméně můžeme určit, který parametr bude použit pod jakým názvem v aplikaci:

// říkáme, že GET parameter "cat" chceme v aplikaci použít pod názvem "categoryId"
$route = new Route('<presenter>/<action> ? id=<productId> & cat=<categoryId>', ...);

Parametry, které se v URL nachází před otazníkem, nazýváme parametry cesty, parametry za otazníkem jako parametry dotazu.

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 (složka www)
$route = new Route('<presenter>/<action>', ...);

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

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

// absolutní URL včetně schématu
$route = new Route('https://<subdomain>.example.com/<presenter>/<action>', ...);

V masce absolutní cesty můžeme použít následující proměnné:

  • %tld% = top level domain, např. com nebo org
  • %sld% = second level domain, např. u example.com vrací example
  • %domain% = doména, např. example.com
  • %basePath%
$route = new Route('//www.%domain%/%basePath%/<presenter>/<action>', ...);
$route = new Route('//www.%sld%.%tld/%basePath%/<presenter>/<action', ...);

Kolekce rout

Protože rout budeme zpravidla definovat více než jednu, zabalíme je do RouteList. A opět rovnou ukázka použítí v souboru bootstrap.php: ($container je systémový DI kontejner)

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

$router = new RouteList;
$router[] = new Route('article/<id>', 'Article:view');
$router[] = new Route('rss.xml', 'Feed:rss');
$router[] = new Route('<presenter>/<action>[/<id>]', 'Homepage:default');

$container->addService('router', $router);

Výhoda Nette Framework oproti jiným frameworkům je v tom, že nemusíme routy pojmenovávat.

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

Pamatujte na to, že počet rout má vliv na rychlost aplikace, zejména při generování odkazů. Proto se vyplatí routovací tabulku zjednodušit.

V případě nenalezení routy se vyhodí výjimka BadRequestException, která se uživateli zobrazí jako stránka 404 Not Found.

Výchozí hodnoty

Jednotlivým parametrům můžeme určit výchozí hodnotu přímo v masce:

$route = new Route('<presenter=Homepage>/<action=default>/<id=>');

Nebo pomocí pole:

$route = new Route('<presenter>/<action>/<id>', [
    'presenter' => 'Homepage',
    'action' => 'default',
    'id' => null,
]);

Validační výrazy

Dále lze určit validační podmínku pomocí regulárního výrazu. Například parametru id určíme, že může nabývat pouze číslic pomocí validační podmínky [0-9]+:

$route = new Route('<presenter>/<action>[/<id [0-9]+>]',
    'Homepage:default');

Výchozí validační podmínkou je [^/]+, tj. vše kromě lomítka. Pokud má parametr přijímat i lomítka, uvedeme podmínku .+.

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:

$route = new Route('[<lang [a-z]{2}>/]<name>', 'Article:view');

// 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 s výchozí hodnotou null. Sekvence zároveň definuje jeho okolí, v tomto případě znak lomítka, které musí za parametrem, je-li uveden, následovat. Této techniky lze využít například u volitelných subdomén s jazykem:

$route = new Route('//[<lang=en>.]example.com/<presenter>/<action>',
    'Homepage:default');

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

$route = 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ší URL, 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
$route = new Route('<name>[.html]');

// akceptuje /hello i /hello.html, generuje /hello.html
$route = new Route('<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:

$route = new Route('<presenter=Homepage>/<action=default>/<id=>');

// odpovídá tomuto:
$route = new Route('[<presenter=Homepage>/[<action=default>/[<id>]]]');

Pokud bychom chtěli ovlivnit chování zpětných lomítek, aby se např. místo '/homepage/' generovalo jen /homepage, lze toho docílit takto:

$route = new Route('[<presenter=Homepage>[/<action=default>[/<id>]]]');

Foo parametry

Foo parametry jsou podobné volitelným sekvencím, slouží však k tomu, aby bylo možné do masky přidat regulární výraz. Příkladem je routa akceptující /index, /index.html, /index.htm a /index.php:

$route = new Route('index<? \.html?|\.php|>', 'Homepage:default');

Lze také explicitně definovat řetězec, který bude při generování cesty použit (obdoba výchozí hodnoty u skutečných parametrů).

Jednosměrky ONE_WAY

Jednosměrné routy se používají zejména pro zachování funkčnosti starých URL, když tvar URL v aplikaci předěláme do novější podoby. Příznakem ONE_WAY tedy označíme routy, které se už nepoužívají pro generování URL.

// 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');

Navíc dojde k automatickému přesměrování na nový tvar URL, takže vám tyto stránky vyhledávače nezaindexují dvakrát (viz SEO a kanonizace).

HTTPS

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

Přesměrování všech adres u již zaběhnuté aplikace lze docílit pomocí souboru .htaccess v kořenovém adresáři naší aplikace za použití permanentního přesměrování s kódem 301.

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

Routy generují URL se stejným protokolem, s jakým byla stránka načtena. Potřebujeme-li, aby na určité adresy byl použit jiný protokol, použijeme ho v masce routy.

// Použije se stejný protokol, s jakým byla stránka načtena
$route = new Route('//%host%/<presenter>/<action>','Homepage:default');

// Přesměrování na HTTP protokol
$route = new Route('http://%host%/<presenter>/<action>','Homepage:default');

// Přesměrování na HTTPS protokol
$route = new Route('https://%host%/<presenter>/<action>','Admin:default');

HTTPS musí podporovat váš hosting.

Transformace a překlady

Je záhodno psát zdrojové kódy v angličtině, ale co když má web běžet v českém prostředí? Pak jednoduché routování typu:

$route = new Route('<presenter>/<action>/<id>', [
    'presenter' => 'Homepage',
    'action' => 'default',
    'id' => null,
]);

bude generovat anglické URL, jako třeba /product/123, /cart nebo /catalog/view apod. 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. Definice v poli rozšíříme:

$route = new Route('<presenter>/<action>/<id>', [
    'presenter' => [
        Route::VALUE => 'Homepage',
        Route::FILTER_TABLE => [
            // řetězec v URL => presenter
            'produkt' => 'Product',
            'kosik' => 'Cart',
            'katalog' => 'Catalog',
        ],
    ],
    'action' => 'default',
    'id' => null,
]);

Jeden presenter může být uveden pod více různými klíči. Pak k němu povedou všechny varianty (tedy vytvoří se aliasy) s tím, že za kanonickou se považuje ta poslední.

Překladovou tabulku lze tímto způsobem aplikovat na jakýkoliv parametr. Přičemž nefunguje jako filtr, tj. pokud překlad existuje, přeloží se, a pokud neexistuje, bere se původní hodnota.

Pokud chceme v dané routě použít pouze překladový slovník, přídáme k FILTER_TABLE ještě FILTER_STRICT ⇒ true. Tím docílíme toho, že se budou brát pouze hodnoty které uvedeme.

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

$route = new Route('<presenter>/<action>/<id>', [
    'presenter' => [
        Route::VALUE => 'Homepage',
        Route::FILTER_IN => 'filterInFunc',
        Route::FILTER_OUT => 'filterOutFunc',
    ],
    'action' => 'default',
    'id' => null,
]);

Kde filterInFunc a filterOutFunc jsou funkce či metody, jenž převádí mezi parametrem v URL a tvarem, který se předává do presenteru. Každá z nich zajišťuje převod opačným směrem.

Implicitním in-filterem je funkce rawurldecode a out-filterem je funkce, která escapuje speciální znaky (například lomítko či mezeru) pro použití v URL.

Jsou situace, kdy toto chování budeme chtít změnit, například pokud použijeme parametr path, který může obsahovat i lomítka. Aby se nekonvertovala na sekvence %2F, zrušíme filtry:

// akceptuje http://files.example.com/path/to/my/file

$route = new Route('//files.example.com/<path .+>', [
    'presenter' => 'File',
    'action' => 'default',
    'path' => [
        Route::VALUE => null,
        Route::FILTER_IN => null,
        Route::FILTER_OUT => null,
    ],
]);

Globální filtry

Vedle filtrů určených pro konkrétní parametry (<presenter>, <action>, …) můžeme definovat též globální filtry, které akceptují asociativní pole se všemi parametry a vrací pole vyfiltrovaných parametrů. Globální filtry definujeme pod klíčem null.

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

Globální filtry můžeme použít též pro filtrování parameterů na základě jiných parametrů. Například přeložení <presenter> a <action> na základě parametru <lang>.

Moduly

Presentery v Nette se mohou dělit do modulů. A proto potřebujeme i v routách pracovat s moduly. Pro to můžeme využít parametr module u třídy Route:

new Route('admin/<presenter>/<action>', [
    'module' => 'Admin'
]);

Pokud máme více rout, které chceme zařadit do modulu, můžeme využít RouteList s uvedeným prvním parametrem:

$adminRouter = new RouteList('Admin');
$adminRouter[] = new Route('admin/<presenter>/<action>');

Routing Debugger

Nebudeme před vámi tajit, že routování je do jisté míry magie a než ji pokoříte, bude vám dobrým pomocníkem Routing Debugger. Jde o panel do Debugger bar, poskytující přehledný seznam parametrů, které router získal z URL, a také seznamem jednotlivých definovaných rout. Zapíná se automaticky.

Zobrazí, na kterém presenteru & view se aktuálně nacházíte, jaké mu byly předány parametry a také zobrazí tabulku s přehledem všech definovaných cest včetně příznaků, jestli pasují i na aktuální URL:

SEO a kanonizace

Framework přispívá k SEO (optimalizaci nalezitelnosti na internetu) tím, že zabraňuje existenci duplicitních URL vedoucích na stejný obsah. Pokud k určitému cíli vede více adres, např. /index a /index.html, framework první z nich určí za výchozí (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. Výchozí (kanonickou) URL je ta, kterou vygeneruje router, tj. první routa v kolekci bez příznaku ONE_WAY.

Kanonizaci provádí Presenter a ve výchozím nastavení je zapnutá. Lze ji vypnout přes $presenter->autoCanonicalize = false.

K přesměrování nedojde při AJAXovém nebo POST požadavku, protože by došlo ke ztrátě dat nebo by to nemělo přidanou hodnotu z hlediska SEO.

Modularita

Pokud chceme do naší aplikace přidat např. modul „fórum“, stačí, aby fórum nabídlo svou routovací tabulku například funkcí createRoutes():

class Forum
{
    static function createRoutes($router, $prefix)
    {
        $router[] = new Route($prefix . 'index.php', 'Forum:Homepage:default');
        $router[] = new Route($prefix . 'admin.php', 'Forum:Admin:default');
        ...
    }
}

„Zaroutování“ fóra do existující aplikace lze pak dosáhnout například takto: (bootstrap.php):

$router = new RouteList;
// přidáme své routy
...

// přidáme modul forum
Forum::createRoutes($router, '//forum.example.com/');

$container->addService('router', $router);

Vlastní router

Pokud vám nabízené routery nedostačují, můžete si vytvořit router vlastní a zcela přirozeně ho začlenit do kolekce rout. Router je implementací rozhraní 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
{
    function match(HttpRequest $httpRequest)
    {
        // ...
    }

    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.

Možnosti vlastního routeru jsou prakticky neomezené, lze třeba implementovat router, který bude routovat na základě databázové tabulky.