Routování#
Jak se z adresy stane presenter a z parametrů zpátky hezká adresa. Jádro:
app/Core/Routers/.
Tahle kapitola je návod: v první polovině je, jak to funguje, ve druhé postupy pro tři nejčastější úkoly — přidat routu, přidat hezký slug a najít, proč odkaz vede jinam, než měl.
Dvě poloviny routeru#
| Část | Kde se definuje | Příklad |
|---|---|---|
| Statické sekce | v kódu, přes factory v DI | /admin/…, /api/…, /cron/…, /script/… |
| Dynamické routy frontu | v databázi (cms_system_routes) |
/inzeraty/<kategorie>, /clanky/<slug> |
Masky frontových rout NEJSOU v kódu
Tohle je první věc, o kterou se člověk uhodí. Když hledáte, proč má výpis
inzerátů zrovna takovou adresu, v app/ to nenajdete — je to řádek v databázi.
V kódu je jen továrna, která z masky postaví routu a doplní jí překlady.
Router z databáze nečte přímo: postaví se snapshot tabulky rout (RouteTableBuilder)
a ten se cachuje. Při zásahu do cache je sestavení routeru bez jediného SQL dotazu.
požadavek
│
▼
RouterDispatcher ──► statické sekce (factory z DI)
│
└─────────────► dynamické routy z RouteTableSnapshot
├─ cache-hit → 0 SQL
└─ miss/off → RouteTableBuilder → DB
Co dělá továrna routy#
Maska sama o sobě neumí hezké adresy — jen parametry. Továrna k ní přidá překlady:
$route = new RestrictedTranslatableRoute($mask, [
'module' => $module, 'presenter' => $presenter, 'action' => $action,
]);
// id stránky ⇄ slug v adrese
$route->addTranslation('slug',
fn(int $id, string $loc) => $this->pageSlugTranslator->idToSlug($id, $loc),
fn(string $slug, string $loc) => $this->pageSlugTranslator->slugToId($slug, $loc),
);
// `name` se v adrese objeví, ale odvozuje se z `id` — do odkazu se nepředává
$route->addDerivedParameter('name', 'id',
fn(int $id) => $this->advertNameTranslator->idToName($id),
);
| Prvek | K čemu je |
|---|---|
addTranslation |
obousměrný převod parametru: id → slug při skládání odkazu, slug → id při čtení adresy |
addDerivedParameter |
parametr, který se do odkazu nepředává — dopočítá se z jiného (typicky název z id) |
applyWhitelist |
omezí routu na stránky, kterým patří |
Odvozený parametr = kratší volání odkazu
Šablona linkuje jen id; název inzerátu v adrese doplní router sám. Kdyby se
předával, musela by ho každá šablona odněkud vzít — a při přejmenování by se
rozešly.
Postup: přidávám novou frontovou routu#
- Řádek do
cms_system_routess maskou, modulem, presenterem a akcí. Masku piš tak, jak má vypadat adresa:inzeraty/<kategorie>[/p-<page>]. - Zvol továrnu. Když stačí prosté parametry, použij existující obecnou; když
má být v adrese slug nebo název, potřebuješ továrnu s překladem
(
app/Core/Routers/Factories/<Modul>/). - SQL do
docs/sql/jako idempotentní skript — routa je konfigurace, ne migrace schématu, ale patří k nasazení. - Smaž cache rout (nebo počkej na TTL) — jinak jede pořád starý snapshot.
- Prokliknij obojí směry: adresu zadanou ručně (čte se) i odkaz vygenerovaný šablonou (skládá se). Nestačí jedno.
Výchozí hodnota v masce se z adresy VYPUSTÍ
o-<order=0> znamená, že /o-0 v adrese nikdy neuvidíte — Nette segment
s výchozí hodnotou odstraní a příchozí /o-0 přesměruje (301) na tvar bez
něj. Je to užitečné (jde tím vyjádřit „výchozí řazení = relevance“), ale kdo to
neví, hledá chybu v presenteru.
Literál shodný s výchozí hodnotou routa spolkne
Když maska má <akce=detail> a někdo chce adresu …/detail, segment zmizí.
Řeší se to jinou výchozí hodnotou, ne jinou šablonou.
Postup: chci v adrese hezký název místo id#
- Napiš překladač do
app/Core/Routers/Translators/<Modul>/— podědíAbstractTranslatora umíidToName(), případně obousměrněidToSlug()/slugToId(). - Zaregistruj ho v továrně routy: v konstruktoru
$translator->addTargetFactory(self::class)a vcreate()ho zapoj přesaddTranslation()neboaddDerivedParameter(). - Ověř obě strany. Skládání odkazu (id → text) selže tiše — vznikne adresa s číslem místo názvu. Čtení (text → id) selže hlasitě, 404.
Překladač musí umět i neznámou hodnotu
Smazaný nebo přejmenovaný záznam znamená, že starý odkaz odněkud zvenčí dorazí
s hodnotou, kterou překladač nezná. Vrátit null je správně (skončí to 404);
spadnout na výjimce ne.
Předehřátí překladače před linkováním
Komponenta, která skládá hodně odkazů, si překladač předehřeje — jinak se sáhne do databáze pro každý řádek výpisu zvlášť.
Postup: odkaz vede jinam, než jsem čekal#
Od nejlevnějšího:
- Podívej se na masku v databázi, ne do kódu. Devět z deseti překvapení je tam.
- Zkontroluj výchozí hodnoty v masce — segment s výchozí hodnotou se vypouští a příchozí adresa se na kanonický tvar přesměrovává.
- Ověř, jestli nepřebíjí jiná routa. Pořadí rozhoduje; routa s obecnější maskou a nižším pořadím spolkne adresu dřív, než se dostane na tu tvou.
- Smaž cache rout a zkus znovu — po zásahu do tabulky jede starý snapshot až do vypršení TTL.
- Teprve pak hledej v presenteru.
Osiřelá routa s obecnou maskou spolkne KAŽDOU adresu
Routa bez navázaných stránek dostane prázdný whitelist. Když má obecnou masku a nízké pořadí, chytí všechno na webu — a projeví se to jako „náhodně padají různé stránky", ne jako chyba routování.
Persistentní parametry se propisují do odkazů
Parametr označený jako persistentní se objeví i v odkazech, kde ho nikdo nechtěl. Vypadá to jako chyba routy, ale je to vlastnost presenteru.
Kanonizace a jazyk#
| Věc | Chování |
|---|---|
| Adresa v jiném než kanonickém tvaru | 301 na kanonický (výchozí hodnoty vypuštěné) |
| Jazyk | LocaleResolver + mapa locale → jazyk ze snapshotu |
| Hledaný výraz | je v cestě (/clanky/s-<dotaz>), ne v query stringu |
?search=… router zahodí
Přesměruje dřív, než se dostane k presenteru. Kdo přidává hledání, musí ho dát do masky — jinak parametr zmizí a nikde se to nezaloguje.
Cache rout#
Vlastní vypínač routerEnabled v config/Shared/cache.neon, nezávislý na hlavním
vypínači cache. TTL routerTtl (výchozí 300 s) s rozptylem ±10 %.
TTL je strop staleness, ne řešení
Zápis do tabulky rout mimo aplikaci (ruční SQL) snapshotem nehne. Cache proto drží nanejvýš do vypršení TTL — a po ruční změně je čistší ji smazat, než čekat.
Kam sáhnout#
| Chci | Kde |
|---|---|
| změnit tvar adresy | řádek v cms_system_routes |
| přidat překlad id ⇄ text | app/Core/Routers/Translators/<Modul>/ |
| zapojit překlad do routy | app/Core/Routers/Factories/<Modul>/ |
| statické sekce (admin/api/cron) | app/Core/Routers/Factories/*RouteFactory.php |
| chování cache rout | config/Shared/cache.neon → routerEnabled, routerTtl |