Je sice hezké, když se nám podaří z adresy získat pole parametrů, ale ještě hezčí je, pokud dokážeme z pole parametrů získat zpátky adresu. Tento článek navazuje na a zároveň rozšiřuje ten předchozí, proto si doporučuji nejdříve přečíst „první část“.
Používat na generování adres router oproti přímému výpisu má jednu nespornou výhodu – se změnou rout se změní zároveň i všechny adresy v aplikaci, což je prostě paráda v porovnání s tím, když by se adresy musely měnit růčo – ono by to bylo poznat i u těch malých aplikací, navíc, komu by se zo chtělo dělat? :o)
Takže princip fungování rout zůstává stejný z předchozího článku, ale jelikož při generování adresy je potřeba mít k dispozici všechny routy, rozhodl jsem se, že bude lepší zapouzdřit funkčnost do nějaké třídy – v našem případě Router. Bude obsahovat pouze jednu vlastnost – privátní pole rout routes. Dále pak tři veřejné metody – addRoute() k přidání nové routy, match() k získání pole parametrů z adresy a uri() k získání adresy z pole parametrů – a ještě privátní parseRoute(), jejíž účel je doufám jasný :o)
addRoute() a match() jsou nudné.
public function addRoute($route, array $defaults = array())
{
$strf = '';
$regex = '~^';
$map = array();
foreach ($this->parseRoute($route) as $x => $part) {
if ($x % 2 == 0) {
$strf .= $part;
$regex .= preg_quote($part, '~');
} else {
list($name, ) = explode("\037", $part);
$strf .= '%s';
$regex .= '(?<' . $name . '>' .
(empty($part_regex) ? '[A-Za-z0-9-_\\.]+' : $part_regex) .
')';
$map[] = $name;
}
}
$regex .= '$~';
$from_route = array();
if (count($map) > 0) {
$from_route = array_combine(
array_values($map),
array_fill(0, count($map), null)
);
}
$this->routes[] = array(
'regex' => $regex,
'strf' => $strf,
'map' => array_flip($map),
'params' => array_merge($defaults, $from_route)
);
}
addRoute() prostě jen naparsuje routu, vytvoří regulár a „zpáteční“ formátovací řetězec pro funkci vsprintf(), která je pak používána k získávání adresy v uri(). Bohužel, ale tady je potřeba již použít mapu kvůli tomu, aby mohly být parametry klíč-hodnota předávány uri() v libovolném pořadí. Netěší mě to, ale je to tak. Další možností by bylo si uschovat naparsovanou routu, ale to už se mi ta mapa zdá elegantnější :o)
public function match($uri)
{
foreach ($this->routes as $route) {
if (preg_match($route['regex'], $uri, $params)) {
return array_merge($route['params'], $params);
}
}
return null;
}
match() je to vlastně route_match() z předchozího článku, akorátež používá již udělané reguláry z addRoute() a prochází všechny routy jednu po druhé. Je tzv. „first-fit“, nebo-li najde první vyhovující routu. Ale teď již k tomu hlavnímu – k uri().
public function uri(array $params)
{
foreach ($this->routes as $route) {
$r_keys = array_keys($route['params']);
sort($r_keys);
$p_keys = array_keys($params);
sort($p_keys);
if (!(count(array_diff($r_keys, $p_keys)) < 1 &&
count(array_diff($p_keys, $r_keys)) < 1 &&
array_reduce(array_diff($route['params'], $params),
create_function('$a, $b', 'return ($a === null && $b === null) ? null : true;'
)) === null)) {
continue;
}
$vsparams = array();
foreach ($params as $k => $v) {
if (isset($route['map'][$k])) {
$vsparams[$route['map'][$k]] = $v;
}
}
ksort($vsparams);
$url = vsprintf($route['strf'], $vsparams);
if (preg_match($route['regex'], $url)) {
return $url;
}
}
return null;
}
Dlouho jsem přemýšlel nad tím, jakou nejlepší strategii pro vytváření adres zvolit. Některé frameworky si routy prostě pojmenují (Zend Framework), u jiných se rozhoduje podle pár parametrů (např. pomocí kombinace presenteru a view v Nette (pokud se nepletu); odhaduju, že v takových Rails to bude fungovat na podobném principu). Jenže tohle není součást žádného frameworku, takže se nelze specializovat jen na určitý okruh parametrů – chce to porovnávat všechny :o)
V addRoute() bylo kromě regulárů a „zpětného“ formátovacího řetězce vytvořeno ještě pole všech parametrů, které ta která routa může mít. U parametrů získávaných z routy byla počáteční hodnota nastavena na null. Musím říct, že jsem si s ověřováním parametrů k vytváření adresy hrál dlouho a pořád si nejsem jistý, jestli to nakonec vždycky udělá to, co se od toho očekává. Ale v podstatě existují tyto pravidla:
-
Musí být předány všechny parametry, které routa má.
-
Pokud má routa nastaveny nějaké výchozí parametry, tak se musí shodovat.
Nakonec jsem to vyřešil několika voláními array_diff() :o) Pak už je to jenom o tom, že se musí zmapovat parametry do pole pro vsprintf(). A nakonec ještě poslední ověření, jestli nově vytvořená routa odpovídá reguláru.
Doufám, že někomu tenhle článek pomohl, či alespoň ukázal možnost řešení routování v PHP. Jak je vidět, není to vůbec těžké :o) Pokud by někdo přišel s lepším řešením, nechť se podělí, ať odkazem, či přímo, v komentářích.
Komentáře
$router->addRoute('/:modul/:kontroler/:akce');<a href="<?php echo $router->uri(array('modul' => 'default', 'kontroler' => '...', 'akce' => '...')); ?>">...</a>$router->addRoute('/:kontroler/:akce', array('modul' => 'default'));Přidat komentář