SimpleSeoRouter
- Jan Tvrdík
- Nette guru | 2595
Jak jsem již psal, tak
při používání pěkných URL nerad používám třídu Route, protože
presenter pak dostane např. řetězec moje-stránka
místo např.
čísla 7
(představující ID stránky v DB). Proto jsem si pro
tyto účely napsal vlastní velmi jednoduchý router.
Zdrojový kód
<?php
/**
* Jednoduchý SEO-friendly router
* ==============================
*
* @license New BSD license
* @copyright Copyright (c) 2009 Jan Tvrdík
* @author Jan Tvrdík (http://merxes.cz)
*/
/**
* Jednoduchý SEO router
*
* @author Jan Tvrdík (http://merxes.cz)
*/
abstract class SimpleSeoRouter implements IRouter
{
/** @var string Řetězec vkládaný před před $this->getPath() */
protected $pathPrefix;
/** @var string Jméno cílového presenteru */
protected $presenter;
/** @var string Jméno cílové akce */
protected $action;
/** @var string Jméno parametru předeného presenteru */
protected $identifierName = 'id';
/**
* Konstruktor
*
* @param string Řetězec vkládaný před před $this->getPath()
* @param string Jméno cílového presenteru
* @param string Jméno cílové akce
*/
public function __construct($pathPrefix, $presenter, $action)
{
$this->pathPrefix = $pathPrefix;
$this->presenter = $presenter;
$this->action = $action;
}
/**
* Zkusí přeložit http požadavek na PresenterRequest
*
* @param IHttpRequest
* @return PresenterRequest|NULL
*/
public function match(IHttpRequest $httpRequest)
{
$path = $httpRequest->getUri()->relativeUri;
if (!preg_match('#^' . preg_quote($this->pathPrefix, '#') . '(.+)$#', $path, $matches)) {
return NULL;
}
$url = & $matches[1];
$id = $this->getId($url);
if ($id === NULL) {
return NULL;
}
$params = array();
$params += $httpRequest->getQuery();
$params['action'] = $this->action;
$params[$this->identifierName] = $id;
return new PresenterRequest(
$this->presenter,
$httpRequest->getMethod(),
$params,
$httpRequest->getPost(),
$httpRequest->getFiles(),
array('secured' => $httpRequest->isSecured())
);
}
/**
* Zkusí vygenerovat URL pro daný PresenterRequest
*
* @param PresenterRequest
* @param IHttpRequest
* @return string|NULL
*/
public function constructUrl(PresenterRequest $request, IHttpRequest $context)
{
if ($request->getPresenterName() !== $this->presenter) {
return NULL;
}
$params = $request->getParams();
if ($params['action'] !== $this->action) {
return NULL;
}
if (!isset($params[$this->identifierName])) {
return NULL;
}
$path = $this->getPath($params[$this->identifierName]);
if ($path === NULL) {
return NULL;
}
unset($params['action']);
unset($params[$this->identifierName]);
$uri = new UriScript($context->getUri()->scheme . '://' . $context->getUri()->authority . $context->getUri()->basePath);
$uri->path .= $this->pathPrefix . $path;
$uri->query = http_build_query($params, '', '&');
return $uri->getAbsoluteUri();
}
/**
* Vrátí identifikátor podle fragmentu URL
*
* @param string Fragment URL
* @return mixed|NULL
*/
abstract protected function getId($url);
/**
* Vrátí fragment URL podle identifikátoru
*
* @param mixed Identifikátor
* @return string Fragment URL
*/
abstract protected function getPath($id);
}
Příklad použití
Struktura tabulky galleries
Sloupec | Typ | Nulový | Výchozí |
---|---|---|---|
id | int(3) | Ano | NULL |
name | varchar(100) | Ano | |
description | varchar(1000) | Ano | NULL |
url | varchar(100) | Ano |
Příklad routeru
class GalleriesRouter extends SimpleSeoRouter
{
protected function getId($url)
{
$query = dibi::query('
SELECT [galleries].[id]
FROM [galleries]
WHERE [url] = %s', $url
);
if ($query->rowCount() === 0) {
return NULL;
}
return $query->fetchSingle();
}
protected function getPath($id)
{
$query = dibi::query('
SELECT [galleries].[url]
FROM [galleries]
WHERE [galleries].[id] = %i', $id
);
if ($query->rowCount() === 0) {
return NULL;
}
return $query->fetchSingle();
}
}
Registrace v bootstrapu
$router[] = new GalleriesRouter('fotogalerie/', 'Front:Galleries', 'view');
Těším se na případnou kritiku a někdy příště se možná podíváme na další příklad routeru.
Editoval Jan Tvrdík (19. 5. 2009 16:35)
- Honza Marek
- Člen | 1664
Ono se to musí kouknout při každém generování odkazu do databáze, jestli tomu dobře rozumim. Zajímalo by mě, jestli je to poznat na rychlosti nebo ne. Jinak se mi to hodně líbí a ty abstraktní metody se dají koneckonců naimplementovat jinak.
- jasir
- Člen | 746
Honza M. napsal(a):
Ono se to musí kouknout při každém generování odkazu do databáze, jestli tomu dobře rozumim. Zajímalo by mě, jestli je to poznat na rychlosti nebo ne. Jinak se mi to hodně líbí a ty abstraktní metody se dají koneckonců naimplementovat jinak.
Jo, vypadá to moc hezky. Jinak bych řekl, že je to adept na caching :)
- Honza Marek
- Člen | 1664
Já tohle řešil tak, že jsem na začátku načetl všechny stránky a to hodil do pole.
- Jan Tvrdík
- Nette guru | 2595
ad Caching: čekal jsem, že se někdo zeptá :) Záleží na počtu url adres, které se musí na běžné stránce vygenerovat. Pokud je jich jen pár, tak nemá cachování smysl, protože ty dotazy jsou vysloveně ukázkou vysokorychlostních dotazů.
Pokud by se však generoval na jedné stránce větší množství url adres, tak se cache určitě vyplatí.
Osobně jsem zatím jednoúrovňový router použil jenom v takových situacích, že cachování nemělo smysl. U víceúrovňového routeru jsem se ale cachování nevyhnul.
- Jan Tvrdík
- Nette guru | 2595
Jsem první, komu právě došlu, že ten router se dá použít i pro víceúrovňovou strukturu?
- Honza Marek
- Člen | 1664
Mám ještě dva nápady.
- Myslím, že by dobré umožnit jinou action než výchozí (adresy /clanky/nazev-clanku?action=edit).
Pak ještě jeden nápad, který možná umožní umazat Simple z názvu :-D
- Určitě by se šikla jazyková proměnná, třeba ještě s nastavením defaultního jazyku (adresy /en/article a /nazev-clanku zároveň). Bez toho defaultního jazyka by to taky mohlo být jednoduché.
- Jakub Šulák
- Člen | 222
Vypadá to skvěle, ale nějak se mi nedaří to implementovat.
Musel jsem to trochu upravit, ale:
<?php
// bootstrap.php
$this->router[] = new CategoriesRouter('katalog/', 'Search', 'default', $this->defaultLanguage);
$this->router[] = new Route('katalog',array(
'presenter' => 'Search',
'action' => 'default'
));
// poznamka:
CategoriesRouter extends Simplerouter
//---
?>
Ten druhý router je tam pro případ, kdy to bude voláno bez parametru (http://www.neco.cz/katalog/).
Když ale zadám www.neco.cz/…y-text-v-db/ tak se sice zavolá správný router, dokonce to vrátí objekt PresenterRequest, ale následně dojde k přesměrování na http://localhost/katalog?…[category]=5.
Parametr lang je jazyk a je ošetřen tak, že se předává v kontruktoru a
pak v match se předá jako parametr.
Parametr limiter je pole, ve kterém mohu předávat různá omezení na výběr
produktů. $limiter[‚category‘]=5 je omezení na
product->id_kategorie=5.
Pole předávám snad také dobře, upravil jsem fnc getId(url) na konci:
<?php
return array('category'=>$query->fetchSingle());
?>
Můžete mně někdo prosím nakopnout, proč mi to dělá to
přesměrování?
Routování jsem ještě s nette neřešil, tak jsem nějak mimo.
díky
- Honza Marek
- Člen | 1664
Můžete mně někdo prosím nakopnout, proč mi to dělá to přesměrování?
Když router zpracuje url, tak se ji aplikace pokusí znovu vygenerovat, aby docházelo k přesměrování na jednu variantu adresy.
- PetrP
- Člen | 587
Jakub Šulák napsal(a):
Vypadá to skvěle, ale nějak se mi nedaří to implementovat.
Když jsem si to zjednodušil:
require_once dirname(__FILE__).'/0.9.php';
Debug::enable();
Debug::enableProfiler();
$application = Environment::getApplication();
$router = $application->getRouter();
abstract class SimpleSeoRouter implements IRouter
...
class CategoriesRouter extends SimpleSeoRouter
{
protected function getId($url)
{
//return $url=='name'?5:NULL;
return $url=='name'?array('category'=>'5'):NULL;
}
protected function getPath($id)
{
return $id==5?'name':NULL;
}
}
$router[] = new CategoriesRouter('katalog/', 'Default', 'default', 'cz');
$router[] = new Route('katalog',array(
'presenter' => 'Default',
'action' => 'default',
));
class DefaultPresenter extends Presenter {
function renderDefault($id)
{
Debug::dump($id);
$this->terminate();
}
}
$application->run();
Tak to mě adresa http://localhost/Nette-test/katalog/name
přesměrovává na
http://localhost/Nette-test/katalog?id%5Bcategory%5D=5
Když odstraním toto:
return $url=='name'?array('category'=>'5'):NULL;
a nahradím to jen za return $url=='name'?5:NULL;
Tak vše funguje
zprávně.
Takže se nemůže vracet pole. (Proč ho vlastně potřebuješ vracet?)
Editoval PetrP (19. 5. 2009 14:15)
- Jan Tvrdík
- Nette guru | 2595
WTF? Co to s mým routerem zkoušíte za hokusy pokusy? :D Čekal jsem, že se spíš dozvím, co zlepšit a ne, že se v něm někdo pohrabal a přestalo mu to fungovat.
Každopádně by to pole klidně vracet mohlo, ale muselo by ho to také detekovat (úpravou jste způsobili nekonzistenci routeru). Tohle by mělo fungovat:
class CategoriesRouter extends SimpleSeoRouter // Předpokládá můj (doufám) funkční SimpleSeoRouter
{
protected function getId($url)
{
return $url == 'name' ? array('category'=>'5') : NULL;
}
protected function getPath($id)
{
return $id == array('category'=>'5') ? 'name' : NULL;
}
}
- Jan Tvrdík
- Nette guru | 2595
Honza M. napsal(a):
- Myslím, že by dobré umožnit jinou action než výchozí (adresy /clanky/nazev-clanku?action=edit).
Dobrý nápad. Hotovo.
<?php
/**
* Jednoduchý SEO-friendly router
* ==============================
*
* @license New BSD license
* @copyright Copyright (c) 2009 Jan Tvrdík
* @author Jan Tvrdík (http://merxes.cz)
*/
/**
* Jednoduchý SEO router
*
* @author Jan Tvrdík (http://merxes.cz)
*/
abstract class SimpleSeoRouter implements IRouter
{
/** @var string Řetězec vkládaný před před $this->getPath() */
protected $pathPrefix;
/** @var string Jméno cílového presenteru */
protected $presenter;
/** @var string Jméno výchozí akce */
protected $defaultAction;
/** @var string Jméno parametru předeného presenteru */
protected $identifierName = 'id';
/** @var string Klíč použitý pro akci */
protected $actionKey = 'action';
/**
* Konstruktor
*
* @param string Řetězec vkládaný před před $this->getPath()
* @param string Jméno cílového presenteru
* @param string Jméno cílové akce
*/
public function __construct($pathPrefix, $presenter, $defaultAction)
{
$this->pathPrefix = $pathPrefix;
$this->presenter = $presenter;
$this->defaultAction = $defaultAction;
}
/**
* Zkusí přeložit http požadavek na PresenterRequest
*
* @param IHttpRequest
* @return PresenterRequest|NULL
*/
public function match(IHttpRequest $httpRequest)
{
$path = $httpRequest->getUri()->relativeUri;
if (!preg_match('#^' . preg_quote($this->pathPrefix, '#') . '(.+)$#', $path, $matches)) {
return NULL;
}
$url = & $matches[1];
$id = $this->getId($url);
if ($id === NULL) {
return NULL;
}
$params = array();
$params += $httpRequest->getQuery();
if (!isset($params[$this->actionKey])) {
$params['action'] = $this->defaultAction;
} elseif ($this->actionKey !== 'action') {
$params['action'] = $params[$this->actionKey];
unset($params[$this->actionKey]);
}
$params[$this->identifierName] = $id;
return new PresenterRequest(
$this->presenter,
$httpRequest->getMethod(),
$params,
$httpRequest->getPost(),
$httpRequest->getFiles(),
array('secured' => $httpRequest->isSecured())
);
}
/**
* Zkusí vygenerovat URL pro daný PresenterRequest
*
* @param PresenterRequest
* @param IHttpRequest
* @return string|NULL
*/
public function constructUrl(PresenterRequest $request, IHttpRequest $context)
{
if ($request->getPresenterName() !== $this->presenter) {
return NULL;
}
$params = $request->getParams();
if (!isset($params[$this->identifierName])) {
return NULL;
}
$path = $this->getPath($params[$this->identifierName]);
if ($path === NULL) {
return NULL;
}
if ($params['action'] == $this->defaultAction) {
unset($params['action']);
} elseif ($this->actionKey !== 'action') {
$params[$this->actionKey] = $params['action'];
unset($params['action']);
}
unset($params[$this->identifierName]);
$uri = new UriScript($context->getUri()->scheme . '://' . $context->getUri()->authority . $context->getUri()->basePath);
$uri->path .= $this->pathPrefix . $path;
$uri->query = http_build_query($params, '', '&');
return $uri->getAbsoluteUri();
}
/**
* Vrátí identifikátor podle fragmentu URL
*
* @param string Fragment URL
* @return mixed|NULL
*/
abstract protected function getId($url);
/**
* Vrátí fragment URL podle identifikátoru
*
* @param mixed Identifikátor
* @return string Fragment URL
*/
abstract protected function getPath($id);
}
- Určitě by se šikla jazyková proměnná, třeba ještě s nastavením defaultního jazyku (adresy /en/article a /nazev-clanku zároveň). Bez toho defaultního jazyka by to taky mohlo být jednoduché.
Nechceš tuhle představu nějak rozepsat, než se to rozhodnu napsat? Jak to
chceš zkombinovat se současnou proměnnou $pathPrefix
? Spíš mě
napadá, že máš pravdu s tím, že tenhle požadavek odstraní z názvu to
Simple
, protože to bude dost možná snažší řešit jako
abstract class SeoRouter extends Route
.
- PetrP
- Člen | 587
Jan Tvrdík napsal(a):
WTF? Co to s mým routerem zkoušíte za hokusy pokusy? :D Čekal jsem, že se spíš dozvím, co zlepšit a ne, že se v něm někdo pohrabal a přestalo mu to fungovat.
Tak já zkoušel to co zkoušel Jakub Šulák
;]
Každopádně by to pole klidně vracet mohlo, ale muselo by ho to také detekovat (úpravou jste způsobili nekonzistenci routeru). Tohle by mělo fungovat:
Tak v takovéhle formě by vracení pole smysl nemělo, jedině kdyby se jednotlivé položky přidávali jako params (ale nenapadá mě smyslulpné využití ;])
- PetrP
- Člen | 587
Spíš mam takovej dotaz co to vylepšuje nad použitím překladoveho slovníku?
Route::addStyle('#id');
Route::setStyleProperty('#id', Route::FILTER_IN, 'getId'); // to same co mas v GalleriesRouter
Route::setStyleProperty('#id', Route::FILTER_OUT, 'getPath');
$router[] = new Route('fotogalerie/<id #id>',array(
'presenter' => 'Front:Galleries',
'action' => 'view',
));
To totiž dává daleko větší možnosti, možná jsem ale SimpleSeoRouter nepochopil ;]
- Jan Tvrdík
- Nette guru | 2595
PetrP napsal(a):
Spíš mam takovej dotaz co to vylepšuje nad použitím překladoveho slovníku?
Přesně tohle jsem tady napsal v příspěvku, který jsem smazal, protože jsem si uvědomil, že to nefunguje tak dobře, jak se mi původně zdálo.
Jediný (!) rozdíl, který tam totiž je, je velmi klíčový – pokud
metoda getId
v SimpleSeoRouter
vrátí
NULL
, vrátí router NULL
. Pokud ale funkce
v překladovém slovníku vrátí NULL
, tak Route
NULL
nevrátí.
Přemýšlel jsem nad tím, jak to třídu Route
naučit a
zatím jsem skončil u představy založené na něčem jako
PATTERN_CALLBACK
.
- PetrP
- Člen | 587
Hmmm jen takovým jednoduchým zásahem na řádku 207 to vrací NULL
if (($x = call_user_func($meta[self::FILTER_IN], (string) $params[$name]))===NULL)
return NULL;
$params[$name] = $x;
To samé možná bude potřeba i u FILTER_OUT (ted mi to ale vraci url
správně; mozná tam bude ještě nějaká záludnost).
Asi to neni úplně ideální ;] možná to bude mít nějaké skrýté
vlastnosti.
Takže když použiju toto:
function getId($url){ return $url=='name'?5:NULL;}
function getPath($id){ return $id==5?'name':NULL;}
function getId2($url){ return $url=='xxx'?4:NULL;}
function getPath2($id){ return $id==4?'xxx':NULL;}
Route::addStyle('#id');
Route::setStyleProperty('#id', Route::FILTER_IN, 'getId');
Route::setStyleProperty('#id', Route::FILTER_OUT, 'getPath');
Route::addStyle('#id2');
Route::setStyleProperty('#id2', Route::FILTER_IN, 'getId2');
Route::setStyleProperty('#id2', Route::FILTER_OUT, 'getPath2');
$router[] = new Route('katalog/<id #id>',array(
'presenter' => 'Default',
'action' => 'default',
));
$router[] = new Route('katalog/<id #id2>',array(
'presenter' => 'Default',
'action' => 'default',
));
class DefaultPresenter extends Presenter {
function renderDefault($id)
{
Debug::dump($this->link('default',array('id'=>5)));
Debug::dump($this->link('default',array('id'=>4)));
Debug::dump($id);
$this->terminate();
}
}
Na http://localhost/Nette-test/katalog/xxx
vrací
string(24) "/Nette-test/katalog/name"
string(23) "/Nette-test/katalog/xxx"
int(4)
Na http://localhost/Nette-test/katalog/name
string(24) "/Nette-test/katalog/name"
string(23) "/Nette-test/katalog/xxx"
int(5)
Je to očekávané chováni?
Editoval PetrP (19. 5. 2009 18:48)
- Jan Tvrdík
- Nette guru | 2595
Ano, toto bude (pravděpodobně) fungovat identicky, jako SimpleSeoRouter, ale bohužel zásahy tohoto typu nejsou ideálním řešením :)
- PetrP
- Člen | 587
Jan Tvrdík napsal(a):
Ano, toto bude (pravděpodobně) fungovat identicky, jako SimpleSeoRouter, ale bohužel zásahy tohoto typu nejsou ideálním řešením :)
No identicky to nefunguje protože můzu mít více parametrů v jedné
routě ze slovníkem.
Jakou konkrétně nevýhodu to má? Možná jsem už moc unavenej ale nic moc
nevydím, nebo se slovník používá k něčemu při čem by to vadilo?
- Jan Tvrdík
- Nette guru | 2595
Je třeba si uvědomit, že třída Route
a
SimpleSeoRouter
mají zcela jiný účel. Route
je
třída pro obecné routování kdečeho, SimpleSeoRouter řeší velmi
specifickou situaci, kterou třída Route
řešit neumí
(přímá editace [ještě ke všemu nekompatibilní] zdrojového kódu
route.php není řešením).
Ostatní věci vyplývají ze specifičnosti SimpleSeoRouteru
.
Jako každá věc na světě, která je specializovaná na určitou činnost,
bude vždy lepší (!) než obecné řešení v případě, že řeší
přesně to, co řešit potřebuješ. Pokud potřebuješ řešit něco
jiného, než SimpleSeoRouter
řeší, pak nemá smysl, aby jsi ho
používal.
I kdyby se třída Route
naučil zpracovávat
PATTERN_CALLBACK
, tak pokud budeš potřebovat řešit to, co
SimpleSeoRouter
řeší, tak z obecných principů plyne, že
třída Route
nikdy nemůže (!) být na danou věc lepší než
SimpleSeoRouter
. Na druhou stranu pokud by třída
Route
uměla zpracovávat PATTERN_CALLBACK
, tak by
třída SimpleSeoRouter
pravděpodobně nikdy nevznikla.
- Jan Tvrdík
- Nette guru | 2595
Předtím, když nějaký filterIn vrátil NULL
, tak router
nevrátil NULL
. Pokud má někdo router, který spoléhá na
stávající chování, tak mu přestane fungovat. Mnohem víc mi ale vadí, že
by mi v kontextu třídy Route
přišlo takové chování
nelogické.
- PetrP
- Člen | 587
Pro to by možná mohl být nějaký příznak při nastavení setStyleProperty, připadně existovat jiný druh filtru. Nerozumim přesně proč by podobné chování mělo být nelogické, jen je to nepatrné rozšíření stávajících překladových slovníků (i ty ti připadají nelogické?). Vyřešila by se s tím poslední věc co routy neumí (původně jsem myslel že s tím byl slovník vyroben; ninejší chování pokládám spíš za chybu). A bylo by to mnohem použitelnější než SimpleSeoRouter (ze vší úctou).
- David Grudl
- Nette Core | 8218
Můžete zkusit ověřit, jestli by pomohla tato úprava v Route.php (od řádku 208)
} elseif (isset($meta[self::FILTER_IN])) { // applyies filterIn only to scalar parameters
$params[$name] = call_user_func($meta[self::FILTER_IN], (string) $params[$name]);
if ($params[$name] === NULL && isset($meta['fixity']) && $meta['fixity'] === self::CONSTANT) {
return NULL;
}
}
- Jan Tvrdík
- Nette guru | 2595
David Grudl napsal(a):
Můžete zkusit ověřit, jestli by pomohla tato úprava v Route.php (od řádku 208)
Neprojde to podmínkou isset($meta['fixity'])
. Upřímně ani
netuším, k čemu má $meta['fixity']
sloužit.
- PetrP
- Člen | 587
Tak fixity může nabývat 4 stavů
Route::OPTIONAL, Route::PATH_OPTIONAL, Route::CONSTANT, NULL
NULL znamená podle mě povinný protože třeba:
$router[] = new Route('katalog/<id>',array(
));
je fixity NULL
Route::PATH_OPTIONAL je nepovinný:
$router[] = new Route('katalog/<id>',array(
'id' => NULL,
));
Route::CONSTANT je když není uvedena v masce, ale má defaultní neměnej parametr
$router[] = new Route('katalog',array(
'id' => null,
));
Route::OPTIONAL je když je parametr v query a ma defaultni hodnotu
$router[] = new Route('katalog ? <id>',array(
'id' => null,
));
Proto nerozumím proč je tam $meta['fixity'] === self::CONSTANT
protože to se možnost filtrem zastavit match dá použít jen na hotnoty
které nejsou v url ;] a nemyslím si ze by někdo na to používal filtry.
Takže asi navrhuju změnit podmínku na
if ($params[$name] === NULL && !isset($meta['fixity'])) {
tedy že by se to dalo zastavit jen na povinné parametry, bez defaultni hodnoty.
- David Grudl
- Nette Core | 8218
Díky za shrnutí významu fixity, když něco dělám po delší době
s třídou Route, tak nad tím vždycky dumám a tohle se mi bude hodit ;) Což
byl ostatně i tento případ, správná je tva formuluace s
!isset($meta['fixity'])
.
Přidávám to do Route.
Ještě si říkám, že by se možná mohly výsledky z FILTER_OUT kešovat.
- David Grudl
- Nette Core | 8218
Chtěl jsem se zeptat, jak je to aktuálně se SimpleSeoRouter – je svou funkcionalitou nahraditelný pomocí Route, nebo naopak by bylo vhodné ho zařadit do extras?
- Jan Tvrdík
- Nette guru | 2595
V praxi jsem ho ještě třídou Route
nahradit nezkoušel, ale
mělo by to bez problémů fungovat. Teď už má SimpleSeoRouter
spíš smysl jako jeden z příkladů, jak může vypadat vlastní router.