Professional Documents
Culture Documents
Prirucka-Programatora
Prirucka-Programatora
Prirucka-Programatora
Nette\Object je společným předkem všech tříd Nette Frameworku. V jiných programovacích jazycích takové třídy
existují automaticky (v Pascalu TObject, DOT.NET má System.Object a Java nebo Ruby mají Object), ale v PHP
nic takového neexistuje. Jde o třídu dostatečně transparentní, neměla by způsobovat žádné kolize, proto ji můžete
jako základní třídu pro své třídy používat i vy.
Reflexe
Základní třída umožňuje snadný přístup k sebereflexi pomocí metody getReflection() (vrací Nette\Reflecti-
on\ClassReflection):
Většinou se přitom jedná o překlepy, které bývají zdrojem obtížně nalezitelných chyb.
Objekty tříd, které jsou potomky Nette\Object, vždy při přístupu k nedeklarovanému členu vyhazují výjimku
MemberAccessException.
Gettery a settery
Termínem property se označují speciální členy tříd, které umožňují pracovat s metodami tak, jako by to byly
proměnné. U Nette\Objectu se jedná o gettery a settery. Zvenku vypadají jako obyčejná proměnná, ale přístup
k ní máme plně pod kontrolou. Můžeme tak validovat vstupy nebo generovat výstupy až v případě potřeby.
● Getter i setter musí být veřejná metoda.
● Getter je povinný, setter volitelný (v tom případě pak získáme read-only proměnnou, write-only proměnné
nejsou podporovány).
● Getter nepřijímá žádný parametr, setter právě jeden – novou hodnotu.
● Názvy jsou citlivé na velikost písmen (case-sensitive). První písmeno property může být malé i velké, mělo by
to ale být anglické písmeno případně podtržítko.
Kromě toho, přidáme-li kruhu property vyplněn/nevyplněn, přirozenější než $circle->getFilled() je volání
$circle->isFilled(). I proto akceptuje Nette obě formy, přičemž pokud existuje getFilled(), zavolá se
ona – má přednost.
Jedná se jen o syntaktické pozlátko, jehož významem je pouhé zpřehlednění kódu. Pokud nechcete, tak properties
nemusíte používat.
Události
Pokud potřebuji volat v nějakém okamžiku více funkcí se stejnými parametry, mohou se mi hodit události
(events).
function kocka($zvuk)
{
echo $zvuk === 'haf' ? 'lek' : '';
}
function pes($zvuk)
{
echo $zvuk === 'mňau' || $zvuk === 'haf' ? 'haf' : '';
}
// nastavím handlery
$zahrada->onSound[] = 'kocka';
$zahrada->onSound[] = 'pes';
$zahrada->onSound[] = function ($zvuk) { //handlerem může být i anonymní funkce
echo strlen($zvuk) > 2 ? 'haf' : 'lek';
};
MyClass::extensionMethod('join', 'MyClass_join');
Viz také:
Spojení s rodičovskou komponentou tvoří základ hierarchie. Rodičovská komponenta kromě rozhraní
Nette\IComponent implementuje i Nette\IComponentContainer, které obsahuje metody pro přidání, odebrání,
získání a iteraci nad komponentami. (TODO: je otázka, jestli toto neimplementovat přímo jako ArrayAccess +
getIterator, má to svá pro i proti). Celý strom komponent je tedy tvořen větvemi v podobě objektů
Nette\IComponentContainer a listů Nette\IComponent.
Hledá se rodič!
Nette\Component disponuje několika užitečnými metodami:
Blízkou metodou je Nette\Component::lookupPath($type), která vrací tzv. cestu, což řetězec vzniklý
spojením jmen všech komponent na cestě mezi aktuální a hledanou komponentou. Takže např.
$component->lookupPath('Nette\Application\Presenter') vrací jedinečný identifikátor komponenty
vůči spansenteru.
Monitorování změn
Jak poznat, kdy byla komponenta připojena do stromu spansenteru? Sledovat změnu rodiče nestačí, protože
k spansenteru mohl být připojen třeba rodič rodiče. Pomůže metoda Nette\Component::monitor($type).
Každá komponenta může monitorovat libovolný počet tříd/rozhraní. Připojení nebo odpojení je ohlášeno zavoláním
metody attached($obj) resp. detached($obj), kde $obj je objekt sledované třídy.
Pro lepší pochopení příklad: třída FileUpload, respanzentující formulářový prvek pro upload souborů
v Nette\Forms, musí formuláři nastavit atribut enctype na hodnotu multipart/form-data. V době vytvoření
objektu ale k žádnému formuláři připojena být nemusí (leda by se v konstruktoru předal $parent, ale ani ten
nemusí být formulářem či kontejnerem připojeným k formuláři). Ve kterém okamžiku tedy formulář modifikovat?
Řešení je jednoduché – v konstruktoru se požádá o monitoring:
Monitorování a dohledávání komponent nebo cest přes lookup je velmi pečlivě optimalizované pro
maximální výkon.
Viz také:
Ta si umí navíc, kromě toho že se umí vykreslit, zapamatovat, jestli při subrequestu došlo ke změnám, které si
vyžadují jej překreslit. K tomu slouží triptych metod invalidateControl(), validateControl() a
isControlInvalid(), což je základem AJAXu v Nette.
Nette však nabízí ještě jemnější rozlišení, než na úrovni Controlů, a to tzv. snippetů neboli ústřižků.
Poznámka: pro jednoduchost k pochopení budu v následujících odstavcích brát Control jako komponentu. To, že
je vykreslitelná, vyplývá z překreslovací vlastnosti snippetů.
Lze tedy invalidovat/validovat na úrovni těchto snippetů (každá komponenta může mít libovolné množství
snippetů). Pokud se invaliduje celou komponentu, tak je i každý snippet považován za invalidní. Komponenta je
invalidní i tehdy, pokud je invalidní některá její subkomponenta. Komponenta, která přijímá signál, je automaticky
označena za invalidní.
Díky invalidaci snippetů přesně víme, které části kterých prvků bude potřeba překreslit.
Komponenta (tedy přesněji vykreslitelná komponenta Control) nepředstavuje pravoúhlou oblast ve stránce, ale
logickou komponentu, která se může renderovat i do více podob. Každá komponenta může být navíc na stránce
vykreslena vícekrát, nebo podmíněně, nebo pokaždé s jinou šablonou atd.
Komponenta Control ač má mnoho společných rysů s Presenterem nemá svůj životní cyklus v pravém slova
smyslu. Jen metody attached() a detached() umožňují detekovat, kdy byl Control připojen k nebo odpojen
od rodiče (spansenteru či jiné komponenty).
O zachycení signálu se stará spansenter, ten ho odevzdá komponentě, která je jeho příjemcem. Protože
spansenter je sám o sobě komponentou, tak ho klidně odevzdá i sám sobě.
Šablony
Třída Control obsahuje továrničku createTemplate() na svou šablonu. Ta standardně vytvoří šablonu, předá
ji některé základní proměnné a zaregistruje standardní helpery:
{**
* Template's description.
*
* @param Presenter $spansenter
* @param Control $control
* @param Template $template
* @param array $flashes
* @param string $baseUri Environment::getVariable('baseUri');
*
* You can use these helpers:
* escape, escapeJs, escapeCss, cache,
* snippet, lower, upper, capitalize, stripTags,
* strip, date, nl2br, truncate, bytes
*}
Pro pohodlnější práci v šablonách (hlavně v při vaší další práci v budoucnu) by každá šablona měla obsahovat
hlavičku popisující proměnné, které v ní lze využívat.
Strom komponent
Každá komponenta děděná z třídy Control má jako první parametr konstruktoru rodiče (
IComponentContainer) v hierarchii stromu komponent. Rodičem může být Presenter, nějaká komponenta nebo
jakýkoliv jiný objekt implementující rozhraní IComponentContainer. Hierarchie pak může vypadat i nějak takto:
Presenter | --Control { implementuje IComponentContainer => může být rodičem } | --Component | --Component {
neimplementuje IComponentContainer => nemůže být rodičem } | --Control | --Component
Protože Control samotný neumí generovat odkazy, ale jen signály, potřebujeme jej svázat s Presenterem. Podle
struktury a zanoření hierarchie se můžeme k spansenteru dovolat i z podkomponent a používat jeho veřejné
metody.
$spansenter = $control->getPresenter();
Hierarchie vzniká připojováním komponent do stromu komponent. Při vytváření komponenty uvedeme
v parametru konstruktoru instanci jejího rodiče a řetězec se jménem komponenty. Uvedením stejné komponenty
pod různými jmény se dá dosáhnout například zobrazení jedné komponenty na stránce vícekrát.
Komponentový model Nette umožňuje velmi dynamickou práci se stromem (komponenty můžeme vyjímat,
přesouvat, přidávat), proto by byla chyba se spoléhat na to, že po vytvoření komponenty je hned znám rodič, rodič
rodiče atd. Nemusí být.
Flash zprávy
Vizuální komponenta Control má své vlastní úložiště flash zpráv nezávislé na spansenteru (metoda
flashMessage patří třídě Control).
Jde o zprávy, které např. informují o výsledku operace a uživateli se zobrazí až po přesměrování. Zasílání
obstarává metoda flashMessage() třídy Control (tj. je možno zasílat zprávy i na úrovni komponent).
$this->redirect('default');
}
Metoda flashMessage() zprávu vkládá přímo do šablony do parametru flashes (jako pole objektů stdClass
). Šablona pro vypsání zpráv pak může vypadat třeba takto:
Jako druhý parametr metody lze uvést typ zprávy (výchozí hodnota je „info“) nebo její kód.
Nejdůležitejší samozřejmě je, že pokud po uložení zprávy flashMessage() následuje přesměrování, bude
i v dalším požadavku v šabloně existovat stejný parametr flashes. Zprávy zůstanou poté živé další 3 sekundy –
například pro případ, že by z důvodu chybného přenosu uživatel stránku dal obnovit. Pokud někdo dvakrát za
sebou obnoví stránku (F5), tak mu zpráva tedy nezmizí, pokud klikne jinam, tak ji už neuvidí.
Viz také:
Volání Debug::enable() aktivuje laděnku, která sama detekuje, zda běží na vývojovém nebo produkčním servru.
Na vývojovém zobrazuje výjimky uživateli-vývojáři, na produkčním chyby loguje, případně posílá informaci na
e-mail. Na zjištění, jestli beží Nette na produkčním servru použijte metodu Environment::isProduction().
Další informace najdete v API třídy Debug.
Zpráva o nezachycené výjimce nebo chybě poskytuje vývojáři důležitou informaci o tom, kde a proč k ní došlo.
Standardní výstup v PHP vypadá asi takto:
Pusťme však ke slovu Laděnku. Po aktivaci příkazem Debug::enable() nám předvede svou nejvíce sexy
podobu:
Nezachycená výjimka v provedení Nette\Debug
Takto vypadá výjimka, takto vypadá vygenerovaná chyba. To je pak jiné ladění, co? Takto zobrazeny jsou
automaticky všechny fatální chyby. Pro ještě hlubší odvšivování lze zapnout striktnější mód. Pak budou stejným
způsobem zobrazeny i chyby nižších úrovní jako E_NOTICE a E_WARNING.
Debug::$strictMode = TRUE;
Jsou ale situace, kdy si určité části kódu chceme ošetřit přes try/catch a ne každé vyhození vyjímky musí
následovat ukončením skriptu. Metoda processException() má za úkol zobrazit/zalogovat výjimku a předat řízení
zpět aplikaci, narozdíl od exceptionHandler(), který po zpracování vyjímky činnost aplikace ukončí.
Variable dump
Každý ladič je dobrým kamarádem s funkcí var_dump, která podrobně vypíše obsah proměnné. Bohužel
v prostředí HTML výpis pozbude formátování a slije se do jednoho řádku, o sanitizaci HTML kódu ani nemluvě.
V praxi je nezbytné var_dump nahradit šikovnější funkcí. Tou je právě Debug::dump()
$arr = array(10, 20.2, TRUE, NULL, 'hello');
Debug::dump($arr);
// včetně jmenného prostoru Nette\Debug::dump($arr);
vygeneruje výstup:
<span><span style="color:gray">array</span>(5) {
[0] => <span style="color:gray">int</span>(10)
[1] => <span style="color:gray">float</span>(20.2)
[2] => <span style="color:gray">bool</span>(true)
[3] => <span style="color:gray">NULL</span>
[4] => <span style="color:gray">string</span>(5) "hello"
}
</span>
Měření času
Dalším užitečným nástrojem ladiče jsou stopky s přesností na mikrosekundy:
Debug::timer();
$elapsed = Debug::timer();
// $elapsed ≈ 2
Debug::timer('page-generating');
// nějaký kód
Debug::timer('rss-generating');
// nějaký kód
$rssElapsed = Debug::timer('rss-generating');
$pageElapsed = Debug::timer('page-generating');
Profiler
Profiler se zapíná pomocí příkazu Debug::enableProfiler();
Má své API a podporuje přetahování myší. Přidat do něj další informace se dá velmi snadno:
Debug::$counters['Last SQL query'] = & dibi::$sql;
Debug::$counters['Nette version'] = Framework::VERSION . ' ' . Framework::REVISION;
Debug::addColophon(array('dibi', 'getColophon'));
Debug::disableProfiler();
Viz také:
Autodetekce
Laděnka tedy funguje buď v režimu zobrazování nebo logování chyb. To se přepíná prvním parametrem a
hodnotou Debug::PRODUCTION nebo Debug::DEVELOPMENT. Pokud jej však neuvedeme nebo má hodnotu NULL
či Debug::DETECT, detekuje se režim podle IP adresy serveru – je-li na adrese veřejné, půjde o produkční režim,
je-li na lokální, tak o vývojářský režim.
V produkčním režimu Laděnka úplně ztichne a funguje jen logování do souboru. Filtrování citlivých dat je nyní
bezpředmětné – v produkčním režimu (což nutně neznamená „na produkčním serveru“) se nezobrazuje nic. Pokud
nejsme ve vývojovém režimu, zvolí logování do souboru %logDir%/php_error.log tudíž v Nette\Environment
musí být nastavena proměnná prostředí logDir nebo appDir. (nicméně Nette\Debug lze používat stále
samostatně, závislost na třídě Environment je jen volitelná).
variable.logDir = %appDir%/log
Poslední volitelný parametr metody Debug::enable() je emailová adresa nebo pole hlaviček, kam bude
zasílána notifikace o vzniku chyby (včetně fatálních nezachytitelných chyb). Takto odchytávány jsou již chyby
úrovně notice. V případě chyby se pošle jen jeden email, takže nehrozí zaplavení adminovy schránky – k této
detekci slouží soubor {logfile}.monitor, který se vytvoří v případě úspěšného odeslání emailu s chybou.
Pokud nelze soubor {logfile}.monitor vytvořit či pokud existuje, email se nepošle.
$emailHeaders = array(
'From' => 'web@example.com',
'To' => 'admin@example.com',
'Subject' => 'Chyba na serveru %host%',
'Body' => '%date% - %message%. Pro více informací shlédněte error log.',
);
Debug::enable(Debug::DETECT, 'php_error.log', $emailHeaders);
Jako %message% se doplní případná zpráva vyjímky.
Protože Nette\Debug komunikuje s Firebugem přes HTTP hlavičky, je nutné volat logovací funkce ještě před tím,
než PHP skript cokoliv vypíše. Také je možné zapnout output buffering a tím výstup oddálit.
use Nette\Debug;
Debug::fireLog(
array('2 SQL queries took 0.06 seconds', // table title
array(
array('SQL Statement', 'Time', 'Result'), // table header
array('SELECT * FROM Foo', '0.02', array('row1', 'row2')), // 1. row
array('SELECT * FROM Bar', '0.04', array('row1', 'row2')) // 2. row
)
), 'TABLE');
try {
throw new Exception('Test Exception');
} catch(Exception $e) {
Debug::fireLog($e);
}
Výsledek vypadá asi takto:
Kromě konzole lze vypisovat proměnné do záložky „Server“ pod záložkou „Net“. Zde je připraven inspektor, který
umí rozbalovat a sbalovat jednotlivé větve proměnné. Každé dumpované proměnné musí být přiřazen jedinečný
klíč (druhý parameter):
Viz také:
● FirePHP
A teď se podíváme na celou věc pozorněji. Získání přístupu do jmenného prostoru counter:
$session = Environment::getSession();
$namespace = $session->getNamespace('counter');
nebo stručněji
$namespace = Environment::getSession('counter');
echo $namespace->a;
Zrušení proměnné:
unset($namespace->a);
Nastavení expirace
Proměnná a bude smazána po 5 sekundách:
$namespace->setExpiration(5, 'a');
$namespace->setExpiration(60);
Celý jmenný prostor bude zrušen v okamžiku, kdy uživatel zavře okno prohlížeče.
$namespace->setExpiration(0);
Zrušení expirace celého jmenného prostoru (neovlivní explicitně nastavenou expiraci klíče a)
$namespace->removeExpiration();
$namespace->removeExpiration('a');
$session = Environment::getSession();
$session = Environment::getSession();
$session->hasNamespace('test'); // TRUE
Konfigurace session
Pro dlouhodobé uchování session proměnných je nutné mít nastavenou expiraci celé session a to
nejlépe v bootstrapu.
Pokud se neprovede toto nastavení, všechny session proměnné vyexpirují v momentě zavření okna prohlížeče.
Uchování session i po zavření prohlížeče se hodí například pro dlouhodobé přihlášení uživatele.
$session = Environment::getSession();
Environment::getSession()->setOptions(array(
'cookie_path ' => '/', // cookie is available within the entire domain
'cookie_domain' => '.example.com', // cookie is available on all subdomains
));
Nastavení parametrů cookie musí být provedeno před tím, než je sezení otevřeno (lze zjistit přes
Environment::getSession()->isStarted(). Pozdější volání nemá efekt. Sezení se otevírá například při
přístupu ke jmennému prostoru (třeba voláním $namespace = Environment::getSession('myapp'), nebo
používáním třídy User).
Session není možné startovat dvakrát (tedy je, ale musí se předtím manuálně uzavřít). Tudíž místo
$session->start() je vhodnější používat třeba na:
if (!$session->isStarted()) $session->start();
Viz také:
Příklad použití:
$mail->setFrom('Franta <franta@example.com>')->addTo('petr@example.com')->send();
$mail->addAttachment('example.zip');
Pokud HTML email nemá nastavený subjekt, bude vzat z elementu <title>.
$template->registerFilter(new LatteFilter);
// 1. varianta
Mail::$defaultMailer = 'MyMailer'; // nebo new MyMailer
// 2. varianta
$mail = new Mail;
$mail->setMailer(new MyMailer);
Viz také:
Image::fromFile('nette.jpg')->resize(100, 50)->send();
Vytvoření obrázku
// a) ze souboru
$image = Image::fromFile('nette.jpg');
Zjištění velikosti
Změna velikosti
Obrázek se proporcionálně zmenší tak, aby nepřesáhl rozměry 50×30 pixelů:
$image->resize(50, 30);
$image->resize(50, NULL);
$image->resize(NULL, 30);
$image->resize('75%', 30);
V uvedených příkladech se obrázek pouze zmenšuje. Případné zvětšení lze povolit příznakem Image::ENLARGE:
Doostření
Po zmenšení obrázku je možné vylepšit jeho vzhled jemným doostřením:
$image->sharpen();
Ořez
S nastavením průhlednosti na 30 %:
$watermark = Image::fromFile('watermark.png');
$image->place($watermark, '50%', '75%', 30);
Uložení obrázku
Obrázek můžeme uložit do souboru:
$image->save('resampled.jpg');
Volitelně lze stanovit stanovit i kvalitu a formát obrázku. (Pokud není uveden, detekuje se z přípony.):
Další funkce
Nette\Image zjednodušuje volání všech grafických funkcí PHP z rozšíření GD:
$size = 300;
$radius = 150;
$image->send(Image::GIF);
Viz také:
● Nette\Image API reference
Příklad použití:
// zápis do cache
$cache['data'] = $myData;
// čtení z cache
$cachedData = $cache['data'];
// mazání z cache
unset($cache['data']);
// nebo
$cache['data'] = NULL;
if (isset($cache['template'])) {
$template = $cache['template'];
} else {
$template = sloziteGenerovani();
$cache['template'] = $template;
}
echo $template;
Ale to není zdaleka vše. Při ukládání položek lze specifikovat několik dalších parametrů a podmínek pro invalidaci.
Protože v případě přiřazení $cache['key'] = $data není kde tyto podmínky specifikovat, použijeme pro
ukládání obsahu funkci $cache->save($key, $data, $options). Parametr $options je pole, které může
mít tyto klíče:
expire => (int) čas, kdy obsah vyexpiruje sliding => (bool) má se expirace prodlužovat? files => (array) seznam
souborů, na kterých cache závisí items => (array) seznam klíčů v keši, na kterých tato položka závisí tags =>
(array) seznam vlastních tagů priority => (int) priorita consts => (array) seznam konstant, na kterých cache závisí
Když nějaká položka vyexpiruje, pak vyexpiruje celá cache. Garbage collector ji poté fyzicky odstraní z disku.
Příklad použití:
Cache vyexpiruje za 10 min. Pokud v té době bude načtena, expirace se prodlouží na dalších deset minut.
Cache vyexpiruje v okamžiku, kdy se změní kterýkoliv z uvedených souborů (pro bezproblémovou funkčnost je
třeba používat absolutní cesty). V praxi to používám třeba pro kešování konfigurace nebo šablon – jakmile se
příslušný soubor změní, Nette automaticky invaliduje i keš.
Cache je závislá na jiných položkách v keši – vyexpiruje v okamžiku, kdy se změní položky s klíčem ‚key1‘ nebo
‚key2‘. To lze využít tehdy, když kešujeme třeba www stránku a pod jinými klíči její části. Jakmile se část změní,
invaliduje se celá stránka.
Položce můžeme přiřadit seznam vlastní tagů. To lze použít třeba tehdy, pokud kešujeme www stránku, kde je
článek + komentáře. Jakmile se článek změní, nebo jakmile se objeví nový komentář, necháme vyexpirovat
všechny položky s příslušným tagem:
V rámci jednoho sezení má Nette načetlou cache i v paměti. Zavolání funkce $cache->release() uvolní tyto
zdroje z paměti.
Invaliduji-li tag funkcí $cache->clean(…), pak se smaže z keše vše s tímto tagem. U velmi velkých aplikací,
kde mohou být na serveru tisíce kešovaných souborů by se musel prohledávat celý adresář, který kěš drží a to by
mohlo být systémově pomalé. Jako řešení se nabízí implementace na databázi, nebo také při kešování nastavovat
prioritu.
V Nette\Environment lze na práci s cache zaregistrovat službu, pokud si chcete objekt pro práci s keší přepsat,
jediný požadavek je, aby implementovalo rozhraní ICacheStorage. V případě, že se rozhodnete změnit způsob
zpracování keše v úložišti, můžete využít připraveného objektu DummyStorage, což je úložiště, které ve
skutečnosti nic nedělá. Také je zde i podpora pro MemCache kterou obstarává objekt MemcachedStorage.
Viz také:
Použití
Nejdříve si získáme/vytvoříme instanci třídy:
$httpRequest = Environment::getHttpRequest();
Metody
● getMethod() zjistí jakou metodou se na stránky přistoupilo (GET, POST, HEAD, PUT, …);
● getQuery() vrací podčást nebo celý query-string, který je naparsován do asociativního pole;
● getPost([string $key]) vrací podčást nebo celý obsah pole $_POST;
● getFiles() celý obsah pole uploadovaných souborů $_FILES, getFile(string $key) vrací jeho podčásti;
● getCookies() vrací celý obsah pole $_COOKIE, getCookie(string $key) vrací jeho podčásti;
● getHeaders() vrací všechny HTTP hlavičky poslány prohlížečem, getHeader(string $key) vrací jednotlivé
hlavičky;
● getReferer() – alias pro getHeader('referer') vracející adresu jako objekt Nette\Web\Uri
● getRemoteAddress(), getRemoteHost();
● isSecured() zjistí jedná-li se o zabezpečenou https komunikaci;
● isAjax() zjistí jedná-li se o AJAXový požadavek.
Metoda getRemoteAddress() slouží k získání IP adresy uživatele nebo jejího DNS překladu.
echo $httpRequest->getRemoteAddress();
// 127.0.0.1
echo $httpRequest->getRemoteHost();
// localhost
A nakonec velmi užitečná metoda detectLanguage(), která získá spanferovaný jazyk prohlížeče podle priority
kterou máte nastavenu, případně, pokud jí předáte pole jazyků, které podporuje vaše aplikace, vrátí z nich ten,
který by viděl návštěvníkův prohlížeč nejradši.
Nejsou to žádná kouzla, jen se využívá hlavičky accept-language.
Pomocí filtrů lze URI vyčistit od znaků, které se do nich mohou dostat kvůli špatně implementovaných komentařů
na webech.
Nette\Web\Uri
Třída Uri je obecným zapouždřením jakéhokoliv URL (tedy zatím jen URL) a poskytuje nám jednoduchou a
elegantní práci s URI adresami podle doporučení pro označování syntaxe adres RFC 3986. Základem třídy je
funkce `parse_url()`.
echo $uri->absoluteUri;
// altenativně: echo (string) $uri;
// output: http://nette.org/cs/dokumentace?action=history#footer
Jsou podporovány i jiná schémata než http, např. file, ftp nebo https.
Propojení třídy Uri s třídou HttpRequest
Nette\Web\HttpRequest obsahuje dva (vlastně tři) URI objekty:
$httpRequest->getAbsoluteUri()->baseUri;
// apod:
$httpRequest->getUri()->basePath;
$httpRequest->getUri()->relativeUri;
$httpRequest->getUri()->absoluteUri;
Viz také:
« Kešování response »
Nette\Web\HttpResponse
Třída zapouzdřuje a sjednocuje funkce pro obsluhu HTTP odpovědi serveru. Umožňuje
změnit stavový kód odpovědi, určit typ obsahu a jeho kódování, měnit HTTP hlavičky a
spravovat cookies.
$httpResponse = /*Nette\*/Environment::getHttpResponse();
$httpResponse->setContentType('text/plain', 'UTF-8');
Manipulace s hlavičkami
Většinu metod v této třídě je nutné volat před odesláním jakéhokoliv výstupu.
$httpResponse->setContentType('text/plain', 'UTF-8');
$httpResponse->addHeader('Pragma', 'no-cache');
$httpResponse->setHeader('Pragma', 'no-cache');
Další metody
Pokud potřebujete zjistit zda je ještě možné odeslat další hlavičku (např. byl již odeslán nějaký výstup), můžete
použít metodu isSent, ta vrací TRUE pokud byly hlavičky odeslány a nelze tedy už odeslat další.
Seznam hlaviček připravených k odeslání (nebo již odeslaných) získáte metodou getHeaders.
Cookies
Pro manipulaci s cookies slouží metody setCookie a deleteCookie.
// Odstranění cookie
$httpResponse->deleteCookie('author');
Tyto dvě metody přijimájí ještě další parametry: $path (podadresář kde bude cookie dostupná), $domain a
$secure.
Komspanse
Chcete-li ušetřit objem přenesených dat mezi klientem a serverem, můžete zapnout gzip komspansi.
$httpResponse->enableComspanssion();
Viz také:
Odbočka: pokud použijete relativní cestu k souboru, require_once jej dohledá možná trošku nečekaným
způsobem. Na include_path se také nerad spoléhám, proto se kloním k používání absolutních cest tímto způsobem:
require_once dirname(__FILE__).'/soubor.php';
Obzvláště ta dualita třída ↔ soubor mi hodně vadí. Hledal jsem tedy nějaké flexibilní řešení, které by jednak
odstranilo obě slabá místa, zároveň co nejvíce zjednodušilo programátorovi život a hlavně nekladlo nová omezení
či pravidla.
Nette\Loaders\RobotLoader
Základem je magická funkce __autoload. Díky ní se soubor s definicí třídy vloží až ve chvíli, kdy je skutečně
potřeba.
Nette má vlastní obsluhu __autoload(). Jejím jádrem je vcelku jednoduchá funkce, která v adresáři webové
aplikace proběhne všechny PHP skripty (tedy i podadresářích) a pomocí funkce token_get_all v nich vyhledá
definice tříd a rozhraní. Výsledkem je tabulka identifikátorů a relativních cest k souborům. Nette pak přesně ví,
který soubor při požadavku na konkrétní třidu vložit. Je to velice rychlé. Tabulka se samozřejmě uchovává na disku,
v podobě INI souboru.
Při nahrání nové verze aplikace na web lze jedním příkazem tabulku vygenerovat znovu, nebo ještě jednodušeji –
stačí smazat příslušný soubor a vygeneruje se sama.
Nette může běžet v tzv. ladícím režimu (DEBUG MODE). Pokud v tomto režimu není třída nalezena, provede se
automaticky regenerace cache. Nepomůže-li to, ohlásí se error.
Výhody řešení
● zbavíte se všech volání require_once
● vkládají se jen potřebné soubory
● bez striktních konvencí pojmenování souborů
● možno mít více tříd v jednom souboru
● není třeba ručně udržovat tabulku
● Nette již při generování odhalí konflikty názvů
● připadáte si jako v kompilovaném jazyce
Konfigurace RobotLoaderu
RoborLoader standardně provádí autolading z adresářů appDir a libsDir, což stačí pro 95% případů. Pokud
je potřebné autoloading rozšířit na více adresářů, je nutné o tom RobotLoaderu říct. Nejjednodušším způsobem to
jde v konfiguračním souboru config.ini. Například takto:
service.Nette-Loaders-RobotLoader.factory = Nette\Configurator::createRobotLoader
service.Nette-Loaders-RobotLoader.option.directory[] = %appDir%
service.Nette-Loaders-RobotLoader.option.directory[] = %libsDir%
service.Nette-Loaders-RobotLoader.option.directory[] = "add/my/directory"
service.Nette-Loaders-RobotLoader.run = TRUE
Viz také:
« response Anotace »
Anotace
Anotace jsou speciálním druhem komentářů, který rozšiřuje schopnosti PHP o další
funkcionalitu.
Anotace se píší do phpDoc/JavaDoc bloků a začínají vždy @. V Nette Frameworku se s nimi pracuje pomocí Reflexí.
Nette implementuje vlastní anotační parser, takže by neměl nastat problém s různými akcelerátory, které kvůli
zrychlení scriptu „mažou“ komentáře.
Ukázkový kód
Toto jsou dvě ukázkové třídy, se kterými budeme v následujícím textu pracovat.
/**
* @author John Doe
* @author Tomas Marny
* @renderable
* @title(value = 'Three (Four)', mode = 'false')
*/
class FooClass
{
/** @secured */
public $foo;
/** @AJAX */
public function foo() {}
}
/**
* @author Franta Vomacka
*/
class BarClass extends FooClass
{
/**
* @privilege(Bar, bar)
* @description(testing)
* @more info, test
*/
public function bar() {}
}
Typy anotací
Anotace dělíme podle dvou kritérií: podle toho, které části kódu se týkají (třída, funkce, vlastnost, metoda) a
podle stylu zápisu.
1. @secured
2. @author John Doe
3. @privilege(Bar, bar)
První anotace je typu boolean. To znamená, že pokud je přítomna, obsahuje hodnotu TRUE. Druhá je typu string,
nese tedy textovou hodnotu. A třetí, ta nejzvláštnější, by se mohla na první pohled zdát jako pole, ale není tomu
vždy tak.
Pokud chcete pokaždé získat data jako pole, stačí výsledek přetypovat: (array)
$reflection->getAnnotation('foo')
Toto se může hodit např. pro anotace, u kterých pokud chybí předpokládáte výchozí hodnotu TRUE, ale občas
jsou případy, kdy potřebujete tuto anotaci uvést s hodnotou false.
Práce s anotacemi
Získání anotace
Data anotace získáme zavoláním metody Nette\Reflection\ClassReflection::getAnnotation() kde jako parametr
opět uvedeme název anotace.
array(3) { "author" => array(2) { 0 => string(8) "John Doe" 1 => string(11) "Tomas
Marny" } "renderable" => array(1) { 0 => bool(TRUE) } "title" => array(1) { 0 =>
object(ArrayObject) (2) { "value" => string(12) "Three (Four)" "mode" => string(5)
"false" } } }
Výsledkem je pole anotací „author“ a „renderable“, které samy obsahují pole hodnot. Pro lepší pochopení si
ukážeme ještě získání anotací k metodě BarClass::bar().
array(3) { "privilege" => array(1) { 0 => object(ArrayObject) (2) { "0" => string(3)
"Bar" "1" => string(3) "bar" } } "description" => array(1) { 0 => string(7) "testing"
} "more" => array(1) { 0 => string(10) "info, test" } }
Dědičnost anotací
Anotace se nepřenáší na potomky třídy, ale můžete jich použít více stejných nad jednou třídou.
Reflexe
Nette\Reflection je soubor tříd rozšiřujících standardní reflexní třídy z PHP,
především o anotace a některé vlastnosti Nette\Object.
Reflexe je jistý programový přístup k reverznímu inženýrství. Umožňuje nám zjistit takové informace jako jaké
vlastnosti a metody daná třída má.
Typy reflexe
Rozlišujeme několik typů reflexe:
● Reflexe třídy (ClassReflection) – název, implementovaná rozhraní, anotace, definované konstanty a metody…
● Reflexe vlastnosti (PropertyReflection) – název, výchozí hodnota, anotace, modifikátor přístupu, statičnost, …
● Reflexe metody (MethodReflection) – název, parametry, anotace, modifikátor přístupu, statičnost, abstraktnost,
finálnost,…
● Reflexe funkce (FunctionReflection) – název, název, parametry, jmenný prostor okolo, název souboru…
● Reflexe parametru (MethodParameterReflection) – název, povinnost, výchozí hodnota, typ (pole / třída), pořadí
parametru…
● Reflexe PHP rozšíření (ExtensionReflection) – název, verze, závislosti, definované třídy, funkce, konstanty a
klíče v php.ini
Pro detailní přehled dostupných metod si prostuduje API dokumentaci a PHP manuál.
Získání reflexe
Reflexi lze získat buď přímo, použitím jejího konstruktoru…
while ($counter--) {
// write
file_put_contents('soubor', $original);
// read
$content = file_get_contents('soubor');
// compare
if ($original !== $content)
die('ERROR');
}
Dokola zapisujeme a následně čteme stále tentýž řetězec. Může se zdát, že volání die('ERROR') nemůže nikdy
nastat. Opak je pravdou. Schválně si zkuste tento skript spustit ve dvou oknech zároveň. Error se dostaví
prakticky okamžitě.
Proč tomu tak je, nedávno vysvětloval Jakub Vrána. Uvedený kód není bezpečný (safe), pokud se v jednu chvíli
provádí vícekrát (tedy ve více vláknech = threads). Což na internetu není nic neobvyklého, často se v tentýž
okamžik pokusí více lidí připojit k jednomu webu. Takže psaní thread-safe aplikací je velmi důležité. Obecně totiž
platí, že pokud váš PHP skript vytváří nebo píše do souborů, je nutné toto řešit! Už pouhý jeden zápis je kritický!
V opačném případě musíte počítat se ztrátou dat a vznikem těžko odhalitelných chyb.
Je třeba zajistit, aby se funkce file_get_contents & spol. vykonávaly atomicky. Pro Nette jsem napsal třídu
Nette\IO\SafeStream, která právě toto zajistí.
Volání SafeStream::register() registruje „bezpečný stream“, pomocí něhož můžeme atomicky manipulovat
se soubory prostřednictvím standardních funkcí. Stačí jen uvést protokol „safe://“. Příklad:
// zaregistrujeme protokol
SafeStream::register();
fclose($handle);
// a teprve teď se přejmenoval na test.txt
$ini = parse_ini_file('safe://autoload.ini');
Jak to funguje?
Využívá se funkce stream_wrapper_register, která zaregistruje protokol zaštiťující funkce pro manipulaci se
systémem souborů.
Čtení
Otevřu soubor v módu r a pokusím se získat zámek pro čtení (neboli shared lock, LOCK_SH). Poté je možné
soubor volně číst. Zámek se uvolní automaticky s uzavřením souboru – fclose() nebo při ukončení skriptu.
Zápis
Otevřu soubor v módu r+. Tím zjistím, zda existuje (budeme přepisovat) nebo je ho třeba vytvořit.
Mazání souboru
Soubor prostě smažeme funkcí unlink(). Že má soubor otevřený jiný thread, ať už pro čtení nebo zápis, nám
nemusí vadit. Ve Windows totiž otevřený soubor vůbec smazat nelze a unlink selže. Naopak v Unixu se smaže,
jakožto položka adresáře, ale s jeho obsahem je možné nadále bezpečně pracovat.
Viz také:
Třída Nette\Forms\Form je určena pro samostatné použití mimo aplikaci Nette. Pokud chcete
používat formuláře v spansenterech, využijte od ní odvozenou třídu Nette\Application\AppForm,
která přidává obsluhu handlerů v spansenterech.
Co všechno umějí?
Nette Framework klade velký důraz na bezpečnost aplikací a proto vynakládá značné úsilí i pro zabezpečení
formulářů. Dělá to zcela transparentně, nevyžaduje nic manuálně nastavovat a troufáme si říci, že v této oblasti
má velký náskok před ostatními frameworky. Ochrání vaše aplikace před útokem Cross-Site Request Forgery
(CSRF), odfiltruje ze vstupů kontrolní znaky, ujistí se, že všechny textové vstupy představují validní UTF-8 řetězce,
že položky označené v select boxech skutečně patří mezi nabízené, automaticky ořeže mezery na jednořádkovém
textovém políčku atd.
Začínáme
Nejprve si ukážeme, jak vytvořit jednoduchý formulář, nastavit mu validační pravidla a jak jej vykreslit.
Vytvoření formuláře
Začneme vytvořením formuláře:
Tímto se vytvořil formulář, který se metodou HTTP POST odešle na stejnou stránku, na jaké se nachází.
Samozřejmě metodu i cílové URL lze změnit:
$form->setAction('/submit.php');
$form->setMethod('get');
Jak nastavit HTML elementu <form> další atributy? Metoda getElementPrototype() vrací element v podobě
Nette\Web\Html objektu, se kterým se dá snadno pracovat:
$form->getElementPrototype()->id = 'login-form';
Prvky formuláře
Existují dva způsoby, jak přidávat nové ovládací prvky do formuláře. Jednak můžeme využít toho, že formulář je
potomkem třídy Nette\ComponentContainer, takže instance prvků lze přidávat metodou addComponent(), nebo lze
použí ještě snažší cestu v podobě předpřipravených továrníček addText(), addPassword() atd. Příklad:
Pokud z nějakého důvodu potřebujeme upravit html atributy prvků formuláře, můžeme si je vytáhnout metodami
getControlPrototype() a getLabelPrototype() a dále s nimi pracovat úplně stejně jako s objektem Html.
Také lze obdobně získat Html objekt samotného formuláře.
Validační pravidla
Metody addRule() a addCondition() … :
Metody addRule() a addCondition() jako název validační operace akceptují callback nebo jméno statické funkce,
díky čemuž je možné používat vlastní validační pravidla.
Veškerá JavaScriptová podpora byla vyseparována do samostatné třídy. Díky tomu je možné vytvořit vlastní
JavaScriptový validátor nebo obsluhu událostí, lze snadno propojit vygenerovaný formulář s nějakým
JavaScriptovým frameworkem a podobně. (viz fórum)
Každý HTML prvek formuláře lze před vykreslením libovolně upravit. Přístup k němu zajišťují metody
getControlPrototype() a getLabelPrototype(), které vrací objekt typu Nette\Web\Html.
Seskupování prvků
Seskupování elementů je snadné – stačí vytvořit skupinu a přidat do ní libovolné elementy:
$form->addGroup('Personal data')
->add($form['name'], $form['age'], $form['gender'], $form['email']);
Vlastně je to ještě jednodušší. Po vytvoření nové skupiny se tato stává aktivní a každý nově přidaný prvek je
zároveň přidán i do ní. Takže formulář lze stavět tímto způsobem:
$form->addGroup('Shipping address');
$form->addCheckbox('send', 'Ship to address');
$form->addText('street', 'Street:', 35);
$form->addText('city', 'City:', 35);
$form->addSelect('country', 'Country:', $countries);
Skupina, kterou respanzentuje třída FormGroup, představuje množinu prvků IFormControl bez specifického
sémantického významu. Význam jí tedy dodá až například renderovací rutina, která prvky vykreslí seskupené do
elementů fieldset a podobně.
$form->setCurrentGroup($form->getGroup('název skupiny'));
Při zadání s parametrem hodnoty NULL nebudou další prvky zadávány do žádné skupiny.
Vykreslení formuláře
Formulář definuje metodu render() a lze jej vykreslit i konstrukcí echo $form.
echo $form;
Zpracování formuláře
// definice
$form = new Form();
$form->addText('name', 'Your name:');
$form->addSubmit('ok', 'Send')
->onClick[] = 'OkClicked'; // nebo 'OkClickHandler'
$form->addSubmit('cancel', 'Cancel')
->setValidationScope(FALSE)
->onClick[] = 'CancelClicked'; // nebo 'CancelClickHandler'
// alternativa:
$form->onSubmit[] = 'FormSubmitted'; // nebo 'FormSubmitHandler'
if (!$form->isSubmitted()) {
// první zobrazení, nastavíme výchozí hodnoty
$form->setDefaults($defaults);
}
// obslužné handlery:
function OkClicked(SubmitButton $button)
{
// submitted and valid
save($form->getValues());
redirect(...);
}
Obslužný handler pro onSubmit lze použít v případě, kdy formulář nemá žádné nebo právě jedno tlačítko.
V odstatních situacích bývá vhodnější využít handler onClick přímo na tlačítku.
Handler onClick se volá před handlerem onSubmit. Handlery se volají pouze v případě, že je odeslání
validní. Pokud odeslání validní není, volají se handlery pro události onInvalidClick a onInvalidSubmit.
Uvnitř metody OkClicked tedy není nutné ověřovat validitu formuláře. Naopak metoda FormSubmitted může
být zavolána i v případě nevalidního formuláře, byl-li odeslán tlačítkem Cancel.
V případě, že formulář nebyl odeslán tlačítkem (například byl odeslán přes JavaScript), nebo se tak tváří kvůli
chybě v Internet Exploreru, bude Nette za odesílací tlačítko považovat první tlačítko formuláře. Tudíž obsluha přes
události onClick je spolehlivá.
Obslužné handlery se vyvolají při prvním volání metody fireEvents() (od verze 0.9.1, dříve při volání
isSubmitted()), při použití uvnitř Nette\Application vyvolání zajistí samotný spansenter. Úkolem metody
isSubmitted() je zjistit, zda-li byl formulář odeslán. Formulář se validuje až při zavolání metod validate()
nebo isValid().
Nastavení výchozích hodnot formuláře (v případě, že nebyl odeslán) metodou setDefaults() nepřemazává
ostatní výchozí hodnoty prvků formuláře. Má ale druhý volitelný parametr, pomocí kterého se toho dá docílit –
$erase = TRUE.
Data získaná metodou Form::getValues neobsahují hodnoty formulářových tlačítek, tak je lze
často rovnou použít pro další zpracování (například vložení do databáze).
Jako parametr je možné uvést text chybové hlášky, která se zobrazí uživateli, pokud je detekováno neoprávněné
odeslání.
Token chránící před CSRF útokem má platnost po dobu existence session. Díky tomu nebrání použití ve více
oknech najednou (v rámci jedné session). Platnost je však možné zkrátit na počet sekund, které se uvedou jako
druhý parametr.
Obrana by měla být aktivována pokaždé, kdy formulář mění nějaká citlivá data v aplikaci.
Viz také:
● Nette\Application\AppForm
● Nette\Forms API reference
● Nette\Forms\Form API reference
● Best practice: Formulářová tlačítka
● Vlastní vykreslování formulářů
$ftp->login($username, $password);
$ftp->close();
// or simply unset($ftp);
try {
$ftp = new Ftp;
$ftp->connect($host);
$ftp->login($username, $password);
$ftp->put($destination_file, $source_file, FTP_BINARY);
On the other hand, if you'd like the possible exception quietly catch, call methods with the spanfix „try“:
$ftp->tryDelete($destination_file).
When the connection is accidentally interrupted, you can re-establish it using method ftp->reconnect().
Viz také:
String::checkEncoding
Zjistí, je-li řetězec v požadovaném kódování.
Příklad:
String::fixEncoding
Vrací správně zakódovaný řetězec v určitém kódování. Výchozí je UTF-8, případně je změněno druhým
parametrem metody.
Příklad:
$correctString = String::fixEncoding($string);
String::startsWith
Vrací TRUE v případě, že řetězec $haystack začíná řetězcem $needle.
Příklad:
$haystack = "Začíná";
$needle = "Za";
String::startsWith($haystack, $needle); // true
String::endsWith
Vrací TRUE v případě, že řetězec $haystack končí řetězcem $needle.
Příklad:
$haystack = "Končí";
$needle = "čí";
String::endsWith($haystack, $needle); // true
String::normalize
Odstraní z textu pravostranné mezery a sjednotí oddělovače řádků.
Příklad:
$normalizedString = String::normalize($string);
String::webalize
Upraví řetězec do tvaru použitelného v URL adresách. Odstraní diakritiku a všechny znaky kromě
alfanumerických nahradí oddělovačem slov -.
Příklad:
Mají-li být zachovány i jiné znaky, lze je vyjmenovat v druhém parametru funkce.
echo String::webalize("19. 2. podtržítková_akce", "._");
// 19.-2.-podtrzitkova_akce
Vstupní řetězec musí být kódován v UTF-8. Pokud je v jiném kódování, převeďte jej funkcí iconv.
String::truncate
Ořízne řetězec na maximální délku a zachová celá slova, je-li to možné. Na konec oříznutého textu se přidá
trojtečka, což lze změnit třetím nepovinným parametrem.
public static string truncate (string $s, int $maxLen, [string $append = "…"])
Příklad:
Vstupní řetězec musí být kódován v UTF-8. Pokud je v jiném kódování, převeďte jej funkcí iconv.
$s = "Dobrý den";
// převést na malá
echo String::lower($s); // dobrý den
// převést na velká
echo String::upper($s); // DOBRÝ DEN
Regulární výrazy
Třída String zapouzdřuje několik užitečných funkcí pro práci s regulárními výrazy. Jejich společným důležitým
rysem je, že v případě jakékoliv chyby vyhodí výjimku Nette\RegexpException.
public static array split (string $subject, string $pattern, [int $flags = 0])
Rozdělí řetězec do pole dle regulárního výrazu. Jako příznak $flag je možné uvést PREG_SPLIT_NO_EMPTY
nebo PREG_SPLIT_OFFSET_CAPTURE, viz dokumentace PHP. Příklad:
Vyhledávání výrazů:
public static array match(string $subject, string $pattern, [int $flags = 0, [int $offset = 0]])
Hledá v řetězci dle regulárního výrazu a vrátí pole s jednotlivými subvýrazy (pokud subvýrazy nepoužijete, vrátí
se pole s jedním prvkem). Jako příznak $flag je možné uvést PREG_OFFSET_CAPTURE, viz dokumentace PHP.
Příklad:
public static array matchAll(string $subject, string $pattern, [int $flags = 0, [int $offset = 0]])
Hledá v řetězci všechny výskyty dle regulárního výrazu a vrátí je jako dvourozměrné pole. Jako příznak $flag je
možné uvést PREG_OFFSET_CAPTURE nebo PREG_PATTERN_ORDER, viz dokumentace PHP. (Narozdíl od funkce
spang_match_all je příznak PREG_SET_ORDER výchozí). Příklad:
Záměny v řetězci:
public static string replace(string $subject, mixed $pattern, [mixed $replacement = NULL, [int
$limit = -1]])
Provede v řetězci záměny dle regulárního výrazu. Druhý parametr $pattern může kromě řetězce s regulárním
výrazem nabývat také hodnoty asociativního pole ve tvaru pattern => replacement. Jako třetí parametr
$replacement lze uvést callback. Příklad:
Viz také:
$el = Html::el("img");
$el->src = "image.jpg";
echo $el;
Další možností je zavolání přetížené metody. V tomto případě je často výhodné použít zřetězených volání
(fluent interfaces).
Pokud jste zvyklí settery a gettery volat s příslušnými spanfixy set a get, můžete to tak činit i zde.
Hodnoty atributů
Hodnotou atributu nemusí být jen číslo. Pokud je to praktické, můžete použít i logickou hodnotu nebo dokonce
pole. Také existuje speciální setter pro usnadnění nastavování cíle odkazu s parametry.
$checkbox = Html::el("input")->type("checkbox");
$checkbox->checked = TRUE; // <input type="checkbox" checked="checked" />
$checkbox->checked = FALSE; // <input type="checkbox" />
// použití pole
$el->class[] = $active ? 'active' : NULL; // NULL se ignoruje
$el->class[] = 'top';
$el->style['color'] = 'green';
$el->style['display'] = 'block';
echo $el;
// <input class="active top" style="color: green; display: block" />
Zadáte-li nepovinný druhý parametr, tak v případě, že to bude pole, tak nastavíte parametry elementu. Pokud jím
bude text, tak nastavíte vnitřní text elementu.
Výpis elementu
● Nejjednodušším způsobem vypsání elementu je použít echo.
● Převedení na řetězec zařídíte přetypováním.
● Získání otevírací a uzavírací značky obstarají metody startTag a endTag.
● Alternativou k příkazu echo je použití metody render.
Hierarchie elementů
$el = Html::el();
// (všimněte si, $el je nyní "kontejner", tj. bez názvu elementu)
// nastavit HTML
$el->setHtml("<em>html</em>");
if ($el->count()) ...
Viz také:
« Pro řetězce
Nette\Templates\Template
Třída Nette\Templates\Template zapouzdřuje soubor se šablonou.
Základy
use Nette\Templates\Template;
// nastavíme parametry
$template->hello = 'Hello World';
Příklad šablony:
Použití šablon samostatně bez MVP návrhu Nette Frameworku se dále věnuje samostatná stránka.
Filtry
Šablonu je možné předzpracovat pomocí jednoho či více filtrů, což jsou funkce, které dostanou jako parametr
obsah šablony a vrátí ho v pozměněném stavu. Jako filtr lze zaregistrovat libovolný callback nebo anonymní funkci.
// Zaregistruje filtr, který nahradí v textu šablony všechny výskyty slova 'apple' slovem 'pizza'.
$template->registerFilter(function ($s) {
return str_replace('apple', 'pizza', $s);
});
Helpery
Do šablon je možné zaregistrovat pomocné funkce, tzv. helpery. Jako helper lze zaregistrovat libovolný callback
nebo anonymní funkci.
Registrace helperu:
Přečtěte si popis standardních helperů, které najdete přímo v distribuci, a o možnosti snazšího
zápisu pomocí Latte filteru.
HelperLoader
Manuální registraci velkého množství helperů lze nahradit registrací jednoho či více HelperLoaderů. Jako
HelperLoader lze zaregistrovat libovolný callback nebo anonymní funkci.
$template->registerHelperLoader('Helpers::loader');
HelperLoader dostane jako parametr název požadovaného helperu a vrací jeho callback nebo NULL v případě, že
helper není schopnen dodat.
class Helpers
{
public static function loader($helper)
{
$callback = callback(__CLASS__, $helper);
if ($callback->isCallable()) {
return $callback;
}
}
Viz také:
Latte filter »
Latte filter
Latte filter slouží nejen pro usnadnění zápisu šablon, ale také umožňuje pracovat s
bloky a podporuje kontextově sensitivní escapování.
Registrace
Ve vykreslitelných komponentách se filtr registruje automaticky. Jinde je potřeba jej zaregistrovat stejně jako
každý jiný filtr.
$template->registerFilter(new LatteFilter);
Základní makra
Zápis v Latte PHP ekvivalent nebo význam
{$variable} Vypíše kontextově escapovanou proměnnou.
{!$variable} Vypíše proměnnou bez escapování.
{*text komentáře*} Komentář, bude odstraněn
{plink ...} Vypíše kontextově escapovaný odkaz.
Odkaz nad komponentou (v šabloně spansenteru ekvivalentní s makrem
{link ...}
plink)
<?php if (?): ?> ... <?php elseif (?): ?> ... <?php
{if ?} ... {elseif ?} ... {/if}
endif ?>
{foreach ?} ... {/foreach} <?php foreach (?): ?> ... <?php endforeach ?>
{for ?} ... {/for} <?php for (?): ?> ... <?php endfor ?>
{while ?} ... {/while} <?php while (?): ?> ... <?php endwhile ?>
{include dir/file.phtml} Vloží podšablonu
{var foo => value} Deklaruje proměnnou v šabloně
{default foo => value} Výchozí hodnoty proměnných šablony
{control loginForm} Vykreslí komponentu s daným názvem
{dump $variable} Pošle dump proměnné do DebugBaru
{l} a {r} Vloží znaky { a }
Výpis proměnných
Všechny proměnné jsou vypisovány escapované.
Jméno: {$name}<br>
Příjmení: {$surname}<br>
Věk: {$age}
Pokud z nějakého důvodu potřebujeme escapování vypnout, tak před dolar vložíme vykřičník.
<h1>{$title}</h1> {* Escapovaný titulek *}
{!$content} {* Neescapovaný obsah stránky *}
Příklad:
<script type="text/javascript">
var pole = {$pole};
var name = {$name}; // ALE POZOR - NESMÍ se už používat extra uvozovky
var anotherName = "{$name}"; // CHYBA, vygeneruje anotherName = ""Jim Beam"";
</script>
Vložíme-li proměnnou v šabloně do html, css nebo javascriptu, vždy se správně escapuje ($id je nějaký
identifikátor obsahující libovolné znaky):
<style type="text/css">
#{$id} { background: blue; }
</style>
<script type="text/javascript">
document.getElementById({$id}).style.backgroundColor = 'red';
</script>
<p id="{$id}">Hello!</p>
{* Pokud je v metodě `renderDetail` definován parametr `$id`, tak lze jeho jméno při
tvorbě odkazu vynechat *}
{plink Products:detail, $productId}
Lze použít i speciální konstrukci {ifset $var} ... {elseifset $var2} ... {/if}, která nahrazuje zápis
{if isset($var)} ... {elseif isset($var2)} ... {/if}.
Foreach cyklus
Foreach cyklus se chová jako běžný foreach v php s několika rozšířeními.
Uvnitř cyklu je inicializovaná proměnná $iterator, díky které můžete zjistit některé jinak těžko zjistitelné údaje
o právě probíhajícím cyklu.
Příklad:
Vkládání souborů
Vkládání souborů se provádí makrem {include file.phtml}. Vložený soubor má k dispozici globální
proměnné aktuální šablony ($template->getParams()) a parametry, které mu při volání předáme.
Deklarace proměnných
Někdy může být vhodné deklarovat proměnnou až v šabloně. Pro tyto účely se používá makro {var} (existuje
alias {assign}) za použití následující syntaxe:
{* Vícenásobná deklarace *}
{var name => 'John Smith', age => 27}
V případě, že chceme deklarovat promměnou pouze v případě, že dosud deklarovaná není, tak použijeme makro
{default}.
{default page => 1} {* Lze zapsat také jako {if !isset($page)}{var page => 1}{/if} *}
Značka {control} slouží pro snadné vykreslování komponent. Pro lepší pochopení je dobré vědět, jak se tato
značka přeloží do PHP.
se přeloží jako:
$control->getWidget("cartControl")->render();
$control->getWidget("cartControl")->renderSmall();
Metoda getWidget() vrací komponentu cartControl a nad touto komponentou volá metodu render(), resp.
renderSmall() pokud je jiný způsob renderování uveden ve značce za dvojtečkou.
Lze použít i volání s parametry, které se předají render metodám, například:
se přeloží jako:
$control->getWidget("cartControl")->renderSmall($maxItems);
<ul>
<ul> {foreach $items as $item}
<li n:foreach="$items as $item">{$item}</li> <li>{$item}</li>
</ul> {/foreach}
</ul>
<p>{if $strong}<strong>{/if}...{if
<p><strong n:tag-if="$strong">...</strong></p>
$strong}</strong>{/if}</p>
<ul>
<ul n:inner-foreach="$items as $item"> {foreach $items as $item}
<li>A {$item}</li> <li>A {$item}</li>
<li>B {$item}</li> <li>B {$item}</li>
</ul> {/foreach}
</ul>
Podpora helperů
Latte filter podporuje snadné volání helperů za použití této syntaxe:
<h1>{$heading|upper}</h1>
<h1>{$heading|lower|capitalize}</h1>
Vykonají se v pořadí od levého k pravému.
<a href="...">{$linkText|truncate:20}</a>
Kam dále?
1. Pokročilá makra
2. Dědičnost
3. Snippety
4. Vlastní makra
{_exspanssion} a {_!exspanssion}
Tyto značky umožňují překládání v šablonách. Pro jejich správnou funkčnost musí být nastaven překladač:
$template->setTranslator(new MyTranslator);
{capture $var}
<ul>
<li>Hello World</li>
</ul>
{/capture}
<p>Captured: {$var}</p>
Značka {block|texy}
Tam kde není vhodné použít filtr texyElements, například při kombinaci s filtrem Latte, kde by docházelo ke
kolizím, můžete zkusit použít kombinace bloku a helperu.
$template->registerFilter(new Nette\Templates\LatteFilter);
$template->registerHelper('texy', array($texy, 'process'));
šablona:
{block|texy}
Vítejte!
--------
[* image.jpg *]
{/block}
[* image.jpg *]
<?php echo $template->texy(ob_get_clean()) ?>
Alternativní je předat do šablony proměnnou s texy obsahem a ten pak pomocí helperu převést.
šablona:
<div>{!foo->info |texy}</div>
Značka {snippet}
Snippety se používají při práci s Ajaxem pro označení logické oblasti na stránce, která má být jako celek
v případě Ajaxového požadavku překreslena. Značky a {snippet} používají ve své vnitřní implementaci
SnippetHelper, který zjednodušuje práci s Ajaxem na nutné minimum. Celá tato problematika včetně příkladů
použití je probrána na stránce Ajax & snippety.
Označené části se automaticky invalidují, když se změní šablona a to včetně i všech případných inkludovaných
souborů.
Dále je podporováno vnořování značek {cache}. Části šablony se pak invalidují, když se invaliduje kterákoliv
vnořená část.
{include 'spot.phtml'}
{include 'comments.phtml'}
{/cache}
Pokud šabloně předáme vazbu na model, který data vrací až „on demand“ nebo-li „lazy“ způsobem, tak je
kešování naopak velmi efektivní a tato funkčnost přesouvá kešování tam, kde je potřeba, tedy do šablon.
Značka {ifCurrent}
Značka {ifCurrent destination} pomáhá zjistit, zda-li je cíl odkazu shodný s aktuální stránkou. Pokud
odkaz směřuje na stránku, která je zrovna zobrazena, můžeme ho například jinak nastylovat a podobně. Trik je
v tom, že při generování odkazu se detekuje, jestli odkaz míří na aktuální stránku. Výsledek pak vrátí
$spansenter->getCreatedRequest()->hasFlag('current').
Viz také:
« helperů Dědičnost »
Dědičnost šablon
Dědičnost šablon představuje mocný nástroj, který umožňuje snadnou a efektivní
tvorbu i velmi komplikovaných layoutů bez nutnosti zbytečného opakování kódu.
Bloky
Bloky slouží pro označení části šablony. Mohou být buď anonymní nebo pojmenované (ty se používají
především).
Definice
K definici se obvykle používá makro {block}, které umožňuje definovat jak anonymní, tak pojmenované bloky.
{block}Anonymní blok{/block}
{block #foo}Pojmenovaný blok s názvem 'foo'. Hash (#) na začátku jména lze při
deklaraci bloku vynechat.{/block}
Kromě makra {block} lze pro definování pojmenovaného bloku ve specifických situacích využít také atributy
n:block a n:inner-block.
Atribut n:block se používá pro definici bloku okolo HTML tagu. Následující dva zápisy jsou ekvivalentní:
{block #foo}<p>...</p>{/block}
<p n:block="foo">...</p>
Atribut n:inner-block se používá pro definici bloku uvnitř HTML tagu. Následující dva zápisy jsou opět
ekvivalentní:
<p>{block #foo}...{/block}</p>
<p n:inner-block="foo">...</p>
Vkládání
Části šablony označené jako bloky jsou vyjmuty ze svých původních míst v šabloně a pro jejich vypsání je třeba je
znovu „vložit“. Pokud je blok definován v šabloně, která nepoužívá layout (o tom se dozvíte více později), je vložen
automaticky na místo své definice. V ostatních případech je třeba blok vložit ručně, k čemuž slouží makro
{include}. Název bloku zde musí být uveden s # (jinak by se makro pokusilo načíst soubor).
{include #foo}
Příklady
Dovnitř tagu <title> bude vložen obsah tagu <h1>.
...
<title>{include #title} | Example.com</title> <!-- Vypíše "Registrace | Example.com"
-->
...
<h1 n:inner-block="title">Registrace</h1> <!-- Definován blok #title. Vyjmut z šablony
a následně automaticky vložen. -->
...
Platnost proměnných
Při deklaraci bloku do něj automaticky přecházejí všechny lokální i globální proměnné. Změna hodnoty lokální
proměnné se projeví pouze v daném bloku (a tím pádem i v dceřiných blocích). Nejlépe to bude vidět asi na
příkladu:
{block #alpha}
{$text} <!-- Vypíše "A" --><br>
{assign text => "B"}
{$text} <!-- Vypíše "B" --><br>
{block #beta}
{$text} <!-- Vypíše "B" --><br>
{/block}
{/block}
Při vložení bloku do něj přecházejí automaticky pouze globální proměnné šablony. Lokální proměnné je možné
v případě potřeby předat ručně.
{block #menu}
<ul>
{foreach $menu as $item}
<li>{if is_array($item)} {include #menu, menu => $item} {else} {$item} {/if}</li>
{/foreach}
</ul>
{/block}
Místo {include #menu, ...} lze psát {include #this, ...}, čímž odstraníme duplicitu slova menu.
Základy
Začněme od začátku – máme jednoduchou stránku:
FirstPage.phtml
<!doctype html>
<html>
<head>
<title>První stránka | Můj web</title>
</head>
<body>
<div id="leftColumn"><ul>...</ul></div>
<div id="content"><p>Mauris consectetur lobortis purus eget...</p></div>
</body>
</html>
SecondPage.phtml
<!doctype html>
<html>
<head>
<title>Druhá stránka | Můj web</title>
</head>
<body>
<div id="leftColumn"><ul>...</ul></div>
<div id="content"><p>Proin eu sem purus. Donec bibendum vestibulum...</p></div>
</body>
</html>
Protože stránky se liší akorát částí <title> a obsahem <div#content>, tak je výhodné vytvořit layout a
stránky FirstPage.phtml a SecondPage.phtml budou upravovat jen ty části, které se od layoutu liší.
layout.phtml
<!doctype html>
<html>
<head>
<title>{include #title} | Můj web</title>
</head>
<body>
<div id="leftColumn"><ul>...</ul></div>
<div id="content">{include #content}</div>
</body>
</html>
Pomocí makra {layout} (nebo jeho aliasu – {extends}) označíme, která šablona bude sloužit jako layout, a
pomocí makra {block} označíme jednotlivé části stránky, které budou vloženy (makrem {include}) do layoutu.
FirstPage.phtml
{layout layout.phtml}
SecondPage.phtml
{layout layout.phtml}
Každá stránka musí definovat všechny bloky, které layout makrem {include} načítá.
layout.phtml
<!doctype html>
<html>
<head>
<title>{block #title}Výchozí titulek{/block} | Můj web</title>
</head>
<body>
<div id="leftColumn"><ul>...</ul></div>
<div id="content">{include #content}</div>
</body>
</html>
Blok (zde #title) umístěný v šabloně stránky přepíše stejně pojmenovaný blok umístěny v layoutu (pakliže
existuje).
layout.phtml
<!doctype html>
<html>
<head>
<title>{block #title}Můj web{/block}</title>
</head>
<body>
<div id="leftColumn"><ul>...</ul></div>
<div id="content">{include #content}</div>
</body>
</html>
FirstPage.phtml
{layout layout.phtml}
Víceúrovňový layout
Layout šablony může sám používat další layout. Toho lze využít pro tvorbu komplikovanějších layoutů.
Shrnutí
Syntaxe
Definice anonymního bloku {block}anonymní blok{/block}
Definice pojmenovaného bloku {block #foo}pojmenovaný blok{/block}
Definice pojmenovaného bloku okolo HTML tagu <p n:block="foo">...</p>
Definice pojmenovaného bloku uvnitř HTML tagu <p n:inner-block="foo">...</p>
Načtení bloku {include #foo}
Načtení bloku s parametry {include #foo, a => 45, b => 'xyz', c => $text}
Načtení sebe sama {include #this}
Načtení stejně pojmenovaného bloku v layoutu {include #parent}
Použití layoutu {layout layout.phtml} nebo {extends layout.phtml}
Pravidla
● Pokud je blok definován v souboru, který nepoužívá layout, je automaticky vypsán.
● Blok umístěný v šabloně stránky přepíše stejně pojmenovaný blok umístěny v layoutu (pakliže existuje).
● Každá stránka musí definovat všechny bloky, které layout makrem {include} načítá (a sám je nedefinuje).
● Při deklaraci bloku do něj automaticky přecházejí všechny lokální i globální proměnné šablony.
● Při vkládání bloku do něj automaticky přecházejí pouze globální proměnné šablony.
// v šabloně {podpis}
LatteMacros::$defaultMacros["podpis"] = "Já";
// v šabloně {aktualniDatum}
LatteMacros::$defaultMacros["aktualniDatum"] =
"<?php echo date('j. n. Y') ?>";
// v šabloně {icon delete}
LatteMacros::$defaultMacros["icon"] =
'<img src="%%.png" width="16" height="16" alt="%%">';
« Snippety
Přehled standardních filtrů
Pro obecné informace o filtrech si přečtěte stránku Nette\Templates\Template.
Latte
Latte filter (dříve znám jako CurlyBracketsFilter) je jediným filtrem, který je ve vykreslitelných
komponentách (Control a Presenter) registrován automaticky. Slouží nejen pro usnadnění zápisu šablon, ale
také umožňuje pracovat s bloky a podporuje kontextově sensitivní escapování.
<ul n:if="count($products)">
{foreach $products as $product}
<li>
<a href="{plink Products:view, $product->id}">{$product->name}</a>
<small n:if="$product->detail">{$product->detail}</small>
</li>
{/foreach}
</ul>
NetteLinks
Překládá adresy odkazů ve tvaru nette:Presenter:view?arg=value na URL.
<a href="nette:Products:view?id=17">TV</a>
Registrace:
$template->registerFilter('Nette\Templates\TemplateFilters::netteLinks');
RelativeLinks
Všechny cesty v atributech src, href a action doplní v případě potřeby o $baseUri.
Registrace:
$template->registerFilter('Nette\Templates\TemplateFilters::relativeLinks');
RemovePhp
Odstraní ze šablony veškerý PHP kód.
Registrace:
$template->registerFilter('Nette\Templates\TemplateFilters::removePhp');
TexyElements
Umožní použití speciálního tagu <texy>...</texy>.
Registrace
Aby tento filtr fungoval, musí být inicializovaná statická proměnná $texy třídy
Nette\Templates\TemplateFilters.
$template->registerFilter('Nette\Templates\TemplateFilters::texyElements');
Nette\Templates\TemplateFilters::$texy = new Texy();
use Nette\Application\Presenter;
use Nette\Templates\TemplateFilters;
// inicializace Texy
TemplateFilters::$texy = new Texy();
TemplateFilters::$texy->encoding = 'utf-8';
TemplateFilters::$texy->allowedTags = Texy::NONE;
TemplateFilters::$texy->allowedStyles = Texy::NONE;
TemplateFilters::$texy->setOutputMode(Texy::HTML5);
Viz také:
Truncate
Ořízne řetězec na maximální délku a zachová celá slova, je-li to možné. Na konec oříznutého textu se přidá
trojtečka, což lze změnit třetím nepovinným parametrem.
Vstupní řetězec musí být kódován v UTF-8. Pokud je v jiném kódování, převeďte jej funkcí iconv.
Bytes
Převádí velikosti souborů v bajtech do lidsky čitelné podoby.
Date
Vlastní formát datumu. Volá php funkci strftime, používají se zde tedy stejné zástupné znaky a pro helper platí
stejná omezení jako pro tuto funkci. Helper zpracovává proměnné typu int (timestamp), string nebo instanci
třídy DateTime.
{$today|date:'%d.%m.%Y'}
Viz také:
Stáhněte a rozbalte si Nette Framework a zkopírujte adresář s frameworkem Nette do své aplikace, např. do
složky libs/Nette. Dále si připravte adresář pro dočasné soubory (například temp) a ujistěte se (hlavně na
serveru), že do něj lze zapisovat.
use Nette\Debug;
use Nette\Environment;
use Nette\Templates\Template;
use Nette\Templates\LatteFilter;
// načteme framework
require_once dirname(__FILE__) . '/libs/Nette/loader.php';
// zaregistrujeme filtr Latte, který umožní používat syntax jako {if} ... {/if}, {foreach} ...
$template->registerFilter(new LatteFilter);
// předáme ji parametry
$template->name = 'Jack';
$template->people = array('John', 'Mary', 'Paul');
// a vyrenderujeme
$template->render();
<h1>Hello {$name}</h1>
<ul>
{foreach $people as $person}
<li>{$person}</li>
{/foreach}
</ul>
{if !$iterator->last}
<hr />
{/if}
{/foreach}
V samotné šabloně je lepší se vyvarovat používání PHP. Pokud bychom chtěli třeba volat funkci str_pad pro
zarovnávní řetězce na zadaný počet míst, bude lepší využít helper:
Přičemž můžeme předat navíc parameter (bude předán jako druhý argument funkci justifyHelper):
<li>{$person|justify:10}</li>
Routování
Routování má na starosti vytváření odkazů a hezkých URL, převod URL mezi moduly, spansentery, pohledy a
jejich stavy.
Routery:
● SimpleRouter : index.php?...
● Route : /tiskarny/canon/mx440/
Router Nette\Application\Route má statické pole $styles, které mimojiné určuje, že parametry module,
spansenter a view budou transformovány (filtrovány) pomocí určitých funkcí. To zajistí převody MyPresenter ->
my-spansenter atd. Už z routeru tedy vypadne název spansenteru ve tvaru PascalCase. Modifikovat chování lze
buď úpravou pole $styles, nebo přímo v definici routy použitím modifikátoru: <spansenter #mymod>. Filtry pak
popisuje struktura v $styles['#mymod'].
Všechny routery definované pro naši aplikaci jsou uchovávány v objektu MultiRouter.
Pokud Vám nevyhovuje v něčem chování routerů, které jsou již obsaženy v Nette, můžete si naimplementovat
vlastní router. Jediný požadavek je implementace rozhraní IRouter.
PresenterLoader
Výchozí loader ze jména spansenteru odvodí třídu a případně název souboru takto:
Načtení šablon
Presenter se pokusí podle svého názvu (nezaměňovat s názvem třídy) načíst šablonu layoutu (ta je nepovinná) a
šablonu view.
Kde ji hledá určují metody formatLayoutTemplateFiles() & formatTemplateFiles(). Všimněte si množného čísla
v názvu – metody vrací pole možných umístění seřazených podle priority.
● templates/AdminModule/CatalogModule/Default/@layout.phtml
● templates/AdminModule/CatalogModule/Default.@layout.phtml
● templates/AdminModule/CatalogModule/@layout.phtml
● templates/@layout.phtml
Soubory spanfixované znakem @ nelze podstrčit jako view, tedy mohou být ve stejné složce se šablonami views.
Lze to použít i pro „podšablony“, které se do jiných šablon inkludují.
● templates/AdminModule/CatalogModule/Default/edit.phtml
● templates/AdminModule/CatalogModule/Default.edit.phtml
● templates/@global.edit.phtml
Odkazování
Při odkazování na jiné spansentery je vhodné dodržovat konvenci PascalCase, ačkoliv PresenterLoader umí název
korigovat.
Nette\Application\Router má při vytváření URL k dispozici správně zapsaný název spansenteru. Aby
správně fungovalo vynechávání defaultních hodnot, je potřeba i defaultní hodnoty zapsat správně.
Události
Jde o vlastnost Nette\Object, kdy mohu do pole $this->onXyz přidávat „callbacky“ a poté je hromadně zavolat
přes $this->onXyz($arg, ...).
Příklad:
$application->onStartup[] = 'myfunc';
$application->onStartup[] = array($object, 'method');
$application->onStartup[] = function() { ... }; //anonymní funkce v PHP >= 5.3
$application->onStartup(TRUE, 123); // volá myfunc(TRUE, 123) a $object->method(TRUE, 123);
V třídě Aplication je možno použít již přednastavených událostí: onStartup, onShutdown, onRequest a
onError.
● onStartup je spuštěna na začátku životního cyklu ještě před prvním zpracováním pořadavku PresenterRequest
a routováním
● onShutdown je spuštěna, proběhne-li korektně celý životní cyklus aplikace
● onRequest je spuštěna při každém novém požadavku (např. při vytvoření požadavku routerem, forwardu na jiný
spansenter nebo při potřebě zpracovat chybu error spansenterem) a jako parametr je jí předán objekt
PresenterRequest, který je aplikací zpracováván
● onError je spuštěna při zachytávání výjimky v metodě run() pokud je nastaveno zachytávání výjimek
$application->catchExceptions == TRUE a je jí předána i zachycená výjimka $exception
I třída Presenter disponuje událostí a to onShutdown stejně jako Application, k jejímu zpracování ale dochází
před zpracováním metody shutdown a jako parametr je jí předána případná výjimka $exception.
Viz také:
● Model-View-Presenter
● Fully qualified action
● Generování odkazů a Neplatné odkazy
● Doporučená adresářová struktura
● Routování
● Nette\Application\MultiRouter
● Nette\Application\SimpleRouter
● Nette\Application\Presenter
● Nette\Application\Presenter API reference
« Šablony Presenter »
Nette\Application\Presenter
Reaguje na události pocházející od uživatele a zajišťuje změny v modelu nebo v pohledu.
Charakteristika fází:
1. výkonná (execution)
2. změny vnitřních stavů (interaction)
3. vykreslovací (rendering)
4. ukončení činnosti (shutdown)
● Klíčový moment pro redirect: je zde prostor pro inicializace perzistentních parametrů a manipulaci s modelem
s možností následného přesměrování, tzn. v tomto stavu se zohlední při redirectu i hodnoty perzistentních
parametrů.
Př.: pokud zde nastavím perzistentnímu parametru $lang hodnotu 'cs', pak se i tato hodnota zohlední v novém
požadavku po přesměrování. Po redirectu se skript ukončí, prohlížeč si vyžádá novou stránku a skript se spustí
znovu. Tudíž všechny „obyčejné“ proměnné se ztratí.
Presenter má během svého životního cyklu možnost kdykoliv ukončit svou činnost, pokud je s prací hotový (
$spansenter->terminate()). To může udělat i během „společných“ metod startup(), beforeRender().
U složitějších aplikací se nevyhnete stromovým strukturám a hierarchii spansenterů. To jak je správně navrhovat
je řečeno v článku Návrh struktury spansenters/views. V takovýchto strukturách nelze abstraktní spansenter kvůli
bezpečnosti vyvolat URL požadavkem.
V spansenteru, pokud někde dochází k zásadní chybě jako je nenalezení článku v databázi, tak je
vhodné vyhazovat výjimky a další zpracování přenechat exception handleru.
Vykreslování šablony již neprobíhá v životním cyklu spansenteru. Šablonu nyní renderuje
Nette\Application\RenderResponse. Více informací ohledně této změny naleznete na fóru .
Signál způsobí znovunačtení stránky úplně stejně jako při původním požadavku (kromě případu, kdy je volán
AJAXem) a vyvolá metodu signalReceived($signal), jejíž výchozí implementace ve třídě PresenterComponent
se pokusí zavolat metodu složenou ze slov handle{signal}.
Další zpracování je na daném objektu. Objekty, které dědí od PresenterComponent (tzn. Control a Presenter)
reagují tak, že se snaží zavolat metodu handle{signal} s příslušnými parametry.
Jinými slovy: vezme se definice funkce handle{signal} a všechny parametry, které přišly s požadavkem, a
k argumentům se podle jména dosadí parametry z URL a pokusí se danou metodu zavolat. Např. jako prametr $id
se předá hodnota z parametru id v URL, jako $something se předá something z URL, atd.
Pokud metoda neexistuje, metoda signalReceived vyvolá výjimku.
Signál může přijímat jakákoliv komponenta, spansenter nebo objekt, který implementuje rozhraní
ISignalReceiver.
Mezi hlavní příjemce signálů budou patřit Presentery a vizuální komponenty dědící od Control (a ty se při
přijetí signálu automaticky invalidují, což je důležité pro AJAX).
Signál má sloužit jako znamení pro objekt, že má něco udělat – anketa si má započítat hlas od uživatele, blok
s novinkami se má rozbalit a zobrazit dvakrát tolik novinek, formulář byl odeslán a má zpracovat data a podobně.
Signál se vždy volá na aktuálním spansenteru a view, tudíž není možné jej směřovat jinam.
URL pro signál vytváříme pomocí metody PresenterComponent::link(). Jako parametr $destination
předáme řetězec {signal}! a jako $args pole argumentů, které chceme signálu předat. Signál se vždy volá na
aktuální view s aktuálními parametry, parametry signálu se jen přidají. Navíc se přidává hned na začátku
parametr ?do, který určuje signál.
Metoda isSignalReceiver() ověří, zda je komponenta (první argument) příjemcem signálu (druhý argument).
Druhý argument můžeme vynechat – pak zjišťuje, jestli je komponenta příjemcem jakéhokoliv signálu.
Experimentálně lze jako druhý parametr uvést TRUE a tím ověřit, jestli je příjemcem nejen uvedená komponenta,
ale také kterýkoliv její potomek.
V kterékoliv fázi předcházející handle{signal} můžeme vykonat signál manuálně zavoláním metody
$this->processSignal(), která si bere na starosti vyřízení signálu – vezme komponentu, která se určila jako
příjemce signálu (pokud není určen příjemce signálu, je to spansenter samotný) a pošle jí signál.
Příklad:
Šablony (Templates)
Presenter se pokusí vykreslit implicitní šablonu, pokud nebylo řečeno metodami setLayout() & setView() jinak,
jméno šablony odvodí od view.
● /templates/Homepage/@layout.phtml
● /templates/Homepage.@layout.phtml.
● nebo se použije společný layout uložený v /templates/@layout.phtml.
Změnit layout jde metodou setLayout(), kde parameter FALSE layout zcela vypne, nebo lze předat název
layoutu. Např. setLayout('extra') bude místo souboru ...@layout.phtml hledat ...@extra.phtml.
Tohle chování má výhodu v tom, že pokud přidáváme nové view, stačí přidávat nové šablony do příslušné složky
a není potřeba psát žádné (prázdné) metody. A naopak, view jsou na šablonách nezávislé, můžeme je zpracovat
dřív, než na kreslení šablony dojde. Detailnější popis k šablonám lze nalézt v Nette\Templates.
Každá komponenta (tedy i spansenter) má přidělen jeden jmenný prostor a v něm sídlí všechny parametry. Balík
všech parametrů je uložen v poli $params každé komponenty, dá se k nim přistupovat také metodou
getParam(...).
...
}
Výše uvedená syntax pro označení perzistentních parametrů může selhat při zapnutém
eAccelatoru na hostingu. Pro tyto případy je možné perzistentní parametry deklarovat
alternativně.
Parametry nastavíme třeba metodou link(), kolem které se to vlastně všechno točí:
$this->link('expand!', array('param1' => 123, 'param2' => TRUE, 'param3' => $id));
Tzn. parametry signálů (a platí to i pro parametry metod render) se liší jen v tom, že nám zjednodušší zápis
odkazů. Pokud existuje metoda handleExpand($param1, $param2, $param3), stačí psát:
$this->link('expand!', array(123, TRUE, 'param3' => $id)); // první dva se párují s param1 a param2
Stejně tak i perzistentní parametry se liší jen v drobnosti – v tom, že je nemusíme v odkazech uvádět vůbec.
Pokud bude param3 perzistentní, stačí psát:
U perzistentních parametrů se ještě navíc pro snažší přístup vytváří reference mezi položkou ve zmíněném poli
$params a proměnnou objektu, tj. $this->param3 = & $this->params['param3'].
Jak již bylo řečeno, perzistentní parameter není potřeba uvádět při volání link(...), neboť se předává
automaticky. Ale uvést ho samozřejmě možné je a tak mu změnit hodnotu.
Podmínkou perzistence parametru je jeho deklarace jako public a uvedení řetězce @persistent v phpDoc
syntaxi komentáře proměnné:
/** @persistent int */
public $page = 0;
Perzistence zohledňuje hierarchii tříd, tzn. že každý poděděnec má tytéž perzistentní parametry jako rodič.
Je-li perzistentní parametr inicializován výchozí hodnotou, jako výše uvedený, pak nejsou tyto hodnoty předávány
v URL. Pokud aplikace přijme request, kde je tato výchozí hodnota zadána, provádí se redirect (
index.php?page=0 → index.php) z důvodu SEO optimalizace. Jinak by se nám stránka, která zobrazí stejné
informace pod dvěma tvary, zaindexovala dvakrát. Proto je vhodné parametry, které se přenášejí v URL (nejen
perzistentní, ale i ty, které přenášíme v metodách render apod.), inicializovat výchozí hodnotou, stejně jako
výchozí spansentery a pohledy v routách. Nejsou-li parametry inicializovány, mají hodnotu NULL.
● TRUE -> 1
● FALSE -> 0
Presenter a komponenty
Jelikož je třída Presenter potomkem ComponentContainer, může manipulovat s komponentama a uchovávat je.
K tomu slouží metody getComponent() (nebo její alternativa $this[...]), která vrátí požadovanou
komponentu podle názvu nebo cesty, a createComponent<Name>(), což je továrnička na komponenty. Výhoda
továrničky je v tom, že signal handler může komponentu (nebo subkomponentu) od spansenteru získat, aniž
by se musela dopředu inicializovat v metodách action. Odpadá tak potřeba komponenty ukládat do proměnných
objektů.
Ukládání vnitřních stavů komponenty má na starosti předek spansenteru PresenterComponent. Díky tomu
i jednotlivé komponenty (např. PresenterComponent) mohou používat továrničky. Vnitřní stavy komponent se
ukládají po vykreslovací fázi.
Také komponenty mohou být perzistentní. Perzistentní komponenty musí být správně anotované
(subkomponenty uvnitř komponent není třeba nijak značit, jsou perzistentní samy o sobě):
/**
* @persistent(game, abc, xyz)
*/
class DefaultPresenter extends Presenter
{
public function actionDefault()
{
$fifteen = new FifteenControl($this, 'game');
$fifteen->onGameOver[] = array($this, 'GameOver');
$this->template->fifteen = $fifteen;
}
...
}
Stav perzistentních komponent se přenáší při přechodu na jiný Presenter podobně, jako v případě perzistentních
parametrů.
Proč je to nutné? V podstatě z technického důvodu. Mezi spansentery se předávají jen data, která jsou jim
společná, tedy která jsou deklarována na úrovni společných předků. Ale jak zjistit, že komponentu game
deklarovala právě metoda třídy DefaultPresenter a ne nějaký její předek nebo potomek? To zjistit nelze. Lze
ale zjistit, která třída deklarovala proměnnou fifteen a toho se právě využívá.
Tedy při subrequestu, při zavolání signálu jsou komponenty perzistentní samy o sobě. Je ale nutné, aby na
vstupním a cílovém spansenteru byla tatáž komponenta zařazená ve stromu pod stejným jménem. Tudíž nemá
smysl, aby to fungovalo pro předem neznámé komponenty, ale jen pro komponenty s spansenterem nějak pevně
svázané.
Situace se komplikuje v případě, že jsou ve hře komponenty, které mají zpracovat signály. Příkladem této
komponenty může být třída AppForm. Je totiž nutné zajistit, aby komponeta přijímající signál existovala předtím,
než se zpracovávají signály komponent. V případě že příjemce signálu v tomto místě neexistuje, skončí aplikace
výjimkou o neexistujícím příjemci signálu. Příkladem této chyby je vytvoření komponenty až metodách render
jiným způsobem než továrničkou.
...
}
Pokud nic z toho nepotřebujeme (nebo nechceme), není potřeba komponentu s spansenterem vázat (respektive
není potřeba ani dědit z PresenterComponent nebo Control). Nicméně původní konstruktor by se měl vždy
rozhodně volat.
Příklad svázání:
SomeControl tedy:
a SomePresenter:
Továrničky na komponenty
Továrna na komponenty je elegantní způsob jak komponenty vytvářet způsobem, až je jich doopravdy potřeba.
Celé kouzlo spočívá v implementaci metody createComponent<Name>(), která umožňuje vytvoření komponenty
právě lazy loading / on-demand způsobem. V této metodě buď komponentu rovnou připojíte k spansenteru (nebo
například i controlu) nebo vrátíte a továrnička se o její připojení postará sama. Metodě createComponent<Name>
je předáván volitelný parametr s názvem komponenty.
...
Výhody použití továrničky nemusí být na první pohled patrné, projeví se hlavně při použití více komponent. Díky
tomu, že jsou všechny komponenty definovány na jednom místě dochází k lepší přehlednosti. Komponenty
z továrničky se také stávají lépe znovupoužitelnými, stačí si je jen kdekoliv přičarovat zápisem $this[...] a
případné jemnější nastavení komponenty je pak možno udělat až při jejím předání šabloně. Nic také nebrání použít
jednu komponentu na stránce vícekrát, rozliší se názvem, který také pomáhá určovat správnou komponentu jako
příjemce signálu.
V šabloně je možné získat a vykreslit komponentu pomocí makra widget nebo control. Není proto
potřeba manuálně komponenty předávat do šablony.
<h2>Editační formulář</h2>
{control editForm}
Viz také:
Asociativní pole není příliš sexy, proto Nette nabízí vychytávku: pokud existuje v cílovém spansenteru metoda
renderShow($id, $lang, ...) nebo metoda spanpareShow($id, $lang, ...), kde ono ‚show‘ v názvu
odpovídá názvu linkovaného view, je možné klíče ‚id‘ a ‚lang‘ vynechat – automaticky se vezmou z parametrů
těchto metod:
A naopak, když je dotyčná metoda po odkliknutí zavolána, tak se jí předají tyto argumenty v parametrech $id a
$lang. V poli je možné uvést další parametry, které metody renderShow a spanpareShow nedefinují, například
lze nastavit nějaký persistentní parametr (v tom případě ovšem už s asociativním klíčem). Pokud žádný takový
další parametr není, je možné pole úplně vynechat:
Tedy v Nette se odkazuje na spansenter & action, nebo ještě jednodušeji: odkazuje se na konkrétní metodu.
$this->link('Catalog:show', 10, 'en') odkazuje a po odkliknutí zavolá metodu
CatalogPresenter::renderShow(10, 'en'). Jaké se vytvoří URL v tu chvíli nehraje roli. To je úkol oddělené
vrstvy – routování.
Odkazování v šablonách
Rozlišujeme více možností vytvoření odkazu, proto není jedno na jakém objektu metodu link() voláme.
Jelikož action/view může měnit pouze spansenter, komponenty pracují vždy pod tímto prahem. Navíc se
prohledávají odkazy v hierarchii směrem dolů k potomkům. Tudíž $control->link() vede na signál,
$spansenter->link() obvykle na view (nebo signál, je-li označen vykřičníkem). V šablonách se za asistence
filtru curlyBrackets může odkaz nad komponentou zkrátit pomocí značky {link} a odkaz nad spansenterem
pomocí {plink}.
Rozdíl se projeví v šablonách komponent, kde zápis {link Home:default} nevygeneruje odkaz, ale signál na
komponentu, zatímco {plink Home:default} vygeneruje platný odkaz na spansenter, se kterým je spárována.
{* zkrácený zápis *}
<a href="{plink Article:view}">Zobrazit články</a>
<a href="{link Article:view}">Zobrazit články</a>
<a href="{link Article:view}" onclick="{ajaxlink Article:view}">Zobrazit články</a>
Poznámka: proměnné $spansenter a $control jsou frameworkem předávány šabloně automaticky. Pro více
informací shlédněte dokumentaci třídy Control či její API.
Zkrácený zápis plink a link je vhodnější použít, pokud dopředu známe počet parametrů. Pokud počet
parametrů neznáme, předáme parametry polem. Při předávání parametrů polem ale musíme využít nezkráceného
zápisu, jinak by došlo k obalení pole $args ještě prázdným polem.
Viz také:
Jak naložit s neplatnými odkazy určuje statická proměnná Presenter::$invalidLinkMode. Ta může nabývat
těchto hodnot (konstant):
a[href^="error:"] {
background: red;
color: white;
text-decoration: blink;
}
Konkrétní příklad
Za neplatný odkaz se považuje i volání metody spansenteru link() pokud je zavolána s parametrem, který není
definovaný, odkaz se nevygeneruje správně a spansenter se zachová tak, jak má tedy nastavena proměnná
Presenter::$invalidLinkMode.
...
}
AppForm využívá ke svému zpracování handlery. Ty se zpracovávají ve fázi zpracování signálů životního cyklu
spansenteru.
Metoda onSubmit se volá jen v případě, že je formulář skutečně odeslán, není potřeba znovu kontrolovat přes
isSubmitted. Maximálně pokud můžeme zjistit, jakým prvkem byl formulář odeslán. onSubmit se provede pouze
pokud byl formulář úspěšně a validně odeslán.
Obslužné handlery musíme nějak výstižně pojmenovávat, nesmí se nám zaměňovat název s metodou
handle{Signal}. Takovou klasikou bývá {componentName}{event-occured}, tedy třeba
loginFormSubmitted. To ale záleží na uvážení programátora. Tato metoda také pobírá jeden argument, kterým
je instance formuláře.
Nikdy neregistrujte AppForm ve fázi render! Na registraci v této fázi je již příliš pozdě (AppForm
nemůže přijímat signály), proto používejte k registraci komponent továrničky (viz příklad).
Komponenty se vám tak vytvoří automaticky, vždy když budou potřeba (už nikdy ne pozdě).
Obsluha událostí
Pro obsluhu událostí se doporučuje používat následující vzor (viz Best practice: Formulářová tlačítka):
Obslužný handler pro onSubmit lze použít v případě, kdy formulář nemá žádné nebo právě jedno tlačítko.
V odstatních situacích bývá vhodnější využít handler onClick přímo na tlačítku.
Handler onClick se volá před handlerem onSubmit. Handlery se volají pouze v případě, že je odeslání validní.
Uvnitř metody OkClicked tedy není nutné ověřovat validitu formuláře. Naopak metoda FormSubmitted může být
zavolána i v případě nevalidního formuláře, byl-li odeslán tlačítkem Cancel.
V případě, že formulář nebyl odeslán tlačítkem (například byl odeslán přes JavaScript), nebo se tak tváří kvůli
chybě v Internet Exploreru, bude Nette za odesílací tlačítko považovat první tlačítko formuláře. Tudíž obsluha přes
události onClick je spolehlivá.
if ($form->isSubmitted()) { ... }
Přídklad by nám vrátil objekt implementující rozhraní ISubmitterControl (nejčastěji objekt SubmitButton)
v případě, že je formulář odeslán nebo FALSE jestliže není. Nevadí, že vrácená hodnota je objekt, podmínka se
vyhodnotí korektně.
if ($form['spanview']->isSubmittedBy()) { ... }
if ($form['subform']['spanview']->isSubmittedBy()) { ... }
Validace jednotlivých prvků formuláře
U každého tlačítka je možné nastavit, jestli vyžaduje validaci:
$form->addSubmit('cancel', 'Cancel')
->setValidationScope(FALSE); // nebude se nic validovat
Funkce se jmenuje tak proto, že se plánuje její rozšíření o možnost předat pole nebo skupinu (to zůstává k diskusi)
prvků, na které se validace při stisku tlačítka omezí.
$form['save']->setRendered(TRUE);
Tímto zavoláním vynecháme prvek z vykreslování. Formulář ho pak považuje za vykreslený, tudíž ho nevykreslí, a
my si jej pak můžeme vykreslit v šabloně ručně. To se hodí v případech, kdy chceme například vykreslit
samostatně více odesílacích tlačítek vedle sebe.
Defaultně action formuláře ukazuje na stejný view/action se stejnými parametry, jen se do URL přidá signál (
?do=formA-submit), který Nette předá příslušnému formuláři a ten se podle toho zařídí (zavolá si metody
definované v onSubmit). Pokud se má formulář odesílat do jiného view, tak je potřeba ten signál přidat do
argumentů.
což je samozřejmě možné, ale zvažme, že ne každá komponenta $form je prvek FormControl. Můžou tam být
jakékoliv jiné komponenty, nebo kontainery, které teprve obsahují prvky formuláře, případně další kontainery atd.
Pro iterování je lepší použít šikovnou metodu $form->getComponents($deep = FALSE, $type = NULL), kde
první parametr říká, zda se má iterovat do hloubky (tj. projít i prvky konteinerů) a druhý nastavuje volitelný filtr:
Viz také:
Komponenty spansenteru jsou persistentní objekty, které si spansenter uchovává počas svého životního cyklu.
Mají schopnost vzájemně ovlivňovat ostatní poděděné komponenty, ukládat své stavy jako persistentní
(IStatePersistent) a odpovídat na uživatelské příkazy (ISignalReceiver), ale nejsou vykreslitelné.
Viz také:
● Nette\Application\PresenterComponent
● PresenterComponent API reference
● ISignalReceiver API reference
● IStatePersistent API reference
● Nette\Application\Presenter
« AppForm PresenterRequest »
Nette\Application\PresenterRequest
Objekt zapouzdřující požadavky určené a následně předané spansenteru, a metody pro
manipulaci s nimi.
Objekt PresenterRequest je důsledně zapouzdřenou respanzentací všech dotazů, které byly přijaty prohlížečem
od uživatele jako požadavek pro načtení stránky, a také vlastností a parametrů aktuální Routy, která byla označena
jako vyhovující masce těchto požadavků.
Konkrétně jde o první fázi životního cyklu aplikace, kdy Router z URL vytváří objekt PresenterRequest, který
nese i informace jaký spansenter bude požadavek obsluhovat. Tento objekt si poté uchovává aplikace i výsledný
spansenter.
Objekt PresenterRequest v sobě drží, kromě výše zmíněných informací, také informace o datech odeslaných
metodou POST, o případných uploadovaných souborech a o tom, jakou metodou byl požadavek zpracován (GET,
POST, …).
Viz také:
● PresenterRequest API reference
● Presenter
● Routování
Existují však situace, kdy se pod jednou akcí mohou vykreslit diametrálně odlišné stránky. Příkladem je třeba
akce zobrazit zboží v e-shopu. Presenter vyhledá položku v databázi a zjistí, že:
Klíčové je si uvědomit, že tyto tři možnosti jsou tři různé výsledky jediné akce – zobrazení zboží. Presenter nemá
akce jako „zobrazit zboží jako nedostupné“ nebo „zobrazit zboží jako neexistující“. Má pouze akci „zobrazit zboží“,
to aplikační logika rozhodne, zda-li zboží existuje, je smazáno, nebo neexistuje. Výsledkem této logiky je pohled na
zboží (v našem příkladu jeden z tří).
Akce je tedy jen jedna (např. Product:show) a tato se rozpadá na tři pohledy. Kód by vypadal asi takto:
class ProductPresenter
{
// action 'show' calls method 'actionShow'
function actionShow($id)
{
$row = dibi::query('SELECT * FROM products WHERE id=%s', $id)->fetch();
if (!$row) {
$this->view = 'notfound';
} elseif ($row->deleted) {
$this->view = 'deleted';
} else {
// není potřeba, je to předvolené nastavení
// $this->view = 'show';
}
}
function renderShow()
{ ... }
function renderNotfound()
{ ... }
function renderDeleted()
{ ... }
}
Volání $this->view = ... tedy ovlivní, které metody spanpareXYZ() a renderXYZ() budou volány a která
šablona se načte. Jinak prostě platí, že se pro pohled použije stejný název jako pro akci.
« PresenterRequest Routování URL »
Routování
Moderní dynamické webové aplikace vyžadují sbírat požadavky zvenčí a na jejich základě přistupovat ke zdrojům
dat, které poté patřičně odspanzentují. Aby aplikace věděla jaké akce má vykonávat při různých požadavcích, musí
se nejprve určit nějaká pravidla. Této technice se říká routování a typickým požadavkem zvenčí je pro webové
aplikace URL adresa.
U webových aplikací založených na architektuře (nebo chcete-li) vzoru MVC a MVP je routování (nebo-li
směrování) spojníkem právě mezi požadavkem zvenčí a správným řadičem, který požadavek zpracuje a
naservíruje očekávaný výstup.
Právě tvar routy určuje, kterému spansenteru a pohledu bude požadavek přidělen a jak bude zpracován.
V současné době se setkáváme s „lidštějšími URL“ (cool, spantty nebo user-friendly URL), které jsou použitelnější a
zapamatovatelnější než jejich „syroví“ předchůdci, ale daleko větší důležitost sehrávají v současně aktuálních
optimalizací webů pro vyhledávače (SEO). Nette Framework na současné trendy myslí a vychází v tomto
vývojářům plně vstříc. Routování navíc odstiňuje vývojáře od direktiv pro mod_rewrite, tudíž nemusíme definovat
tvar rout na více místech – tím předcházíte i určitému procentu chybovosti aplikace.
Obecně lze 90% všech tvarů požadavků v běžné webové aplikaci charakterizovat takto:
Z pohledu OOP programování, nám tento způsob umožňuje přistupovat rovnou k metodám objektů, předávat jim
ony parametry a pokochat se výsledkem.
● example.com/article/show/5
● example.com/article/edit/5
● example.com/article/delete/5
nebo
● example.com/category/show
● example.com/category/show/5
První entita, na kterou v URL narazíme, je zároveň první dělící čárou aplikace – který řadič převezme požadavek?
V případě příkladů to bude nejspíše, jak už je z názvu patrné, řadič pro manipulaci s články, v druhém případě pro
manipulaci s logickým prvkem webu – kategorií.
Druhá entita je další dělící čárou – která metoda (akce) tohoto řadiče převezme požadavek? Zde postupně metoda
pro výpis, editaci a smazání článku, v druhém případě pro zobrazení výpisu v kategorii.
A konečně, této metodě je třeba předat nějaké parametry – např. ID stránky nebo konkrétní pozici ve stránkování.
V jiných systémech má routování obrovských význam, naopak v Nette se jím netřeba zabývat. To je věc, která se
může řešit až když je aplikace hotová. Navíc není vůbec žádný problém kdykoliv úplně změnit veškeré cesty
pouze přepsáním rout – je tomu tak, protože routování je obousměrné, slouží jak k parsování, tak
k generování cest.
Není tudíž problém kdykoliv docílit toho, aby požadavek example.com/article/my-new-article vedl na
stejnou stránku jako „kdysi“ example.com/article/show/5 nebo si zjednodušit stránkování výpisu kategorií do
následujícího tvaru: example.com/category/5.
Můžeme také označit některé, například starší, tvary cest za nepoužívané a zastaralé a říct tak aplikaci aby
takovéto odkazy negenerovala, ale jen přijala a přesměrovala na správný (aktuální) tvar URL požadavku.
Výhoda Nette oproti některým jiným MVC frameworkům je v tom, že nemusí pojmenovávat routy. Routy mají
minimální závislosti mezi vrstavama (např. není vztah mezi pořadím parametrů v routě a argumenty metody
spansenteru).
Viz také:
● Model-View-Presenter
● Nette\Application\Route
● Nette\Application\MultiRouter
● Nette\Application\SimpleRouter
● Nette\Application\Presenter
SimpleRouter je jednoduchou implementací rozhraní IRouter. Má stejné možnosti jako Route, tj. generovat
i přijímat URL adresy, nastavit jim příznak pro zabezpečené schéma (https) a jednosměrky (požadavky jsou jen
přijímány). Co však s Route nemá společné, je možnost vytvářet „cool URL“, protože SimpleRouter nemá jako
jeden z parametrů masku tvaru vstupní/výstupní URL. Ty pak mají tvar klasického query stringu.
SimpleRouter stejně jako Route na začátku svého životního cyklu naparsuje vstupní požadavek (tedy query string)
a vytvoří z něj objekt PresenterRequest a stejně jako Route také generuje URL adresy z tohoto objektu
PresenterRequest, pokud je dvousměrný.
Pokud není aplikaci určena žádná uživatelská definice routy, jako výchozí se použije právě
SimpleRouter, který předá požadavek ke zpracování spansenteru Default a pohledu default.
Pěkná ukázka použití SimpleRouteru ve spolupráci s objektem PresenterRequest je v adresáři tests v distribuci.
Viz také:
Třída Route implementující rozhraní IRouter je nástrojem pro tvorbu user-friendly URL adres. Že nejde o nic
složitého se můžete přesvědčit sami níže.
Definice cest
Prvním parametrem při tvorbě routy je maska cesty. Ta může být doplněna o validační podmínky ve formě
klasických regulárních výrazů. Druhým parametrem je pole výchozích a fixních hodnot.
Příklad ukazuje masku sestávající ze tří parametrů, přičemž všechny jsou volitelné, neboť mají definovánu
výchozí hodnotu. Parametr id má navíc specifikovánu validační podmínku [0-9]+, tj. akceptuje jen číslo
(u ostatních parametrů je použita výchozí podmínka [^/]+).
$application = Environment::getApplication();
$router = $application->getRouter();
// vytvoříme odkaz
$spansenter->link('Article:'); // ekvivalentní s Article:show
Pokud jsou routy inicializovány jako ty v příkladu výše (uvedení výchozích hodnot spansenteru a action), nemusí
se v metodách pro tvorbu odkazů uvádět celá maska routy. Action není potřeba uvádět vůbec – výchozí hodnota je
default zcela automaticky.
Routa s maskou index.php určuje, že při požadavku na stránku index.php se otevře spansenter Article a
action show. Příznak Route::ONE_WAY (jednosměrka) zajistí, že routa může požadavek přijmout (stránka
index.php existuje), ale aplikace takové URL nevytvoří. Tedy při generování URL pro spansenter Article a
action show se použije vhodná následující routa.
Jednosměrné routy se používají třeba pro zachování zpětné kompatibility – pokud na web již existují odkazy ve
tvaru http://example.com/index.php, budou tyto nadále funkční. Navíc dojde k automatickému
přesměrování na nový tvar URL (tzv. kanonizace).
Routy nemusí být striktně SEO friendly, nic nám nebrání napsat tvar klasického query stringu. Můžeme při tom
využívat plné síly regulárních výrazů.
Má-li být parametrem routy cesta filesystému, pak maskou .*? povolíme všechny znaky včetně
lomítek. Například: new Route('/storage/<path .*?>', ...)
Uvnitř aplikace se odkazuje tak, jako když jsou volány metody v OOP: Presenter::action($arg1, $arg2).
Konkrétně třeba Product:detail($id). Voláme metodu detail třídy Product a předáme jí parametr $id.
Podrobně je filosofie routování popsána v jiném článku.
Kanonizace
Kanonizace je proces přesměrování URL na výchozí (kanonickou) formu. Jednoduše řeřeno, kanonická URL je ta,
kterou vygeneruje router. Je úkolem třídy Presenter toto zajistit a ve výchozím nastavení je automatická
kanonizace zapnuta. Lze ji vypnout přes $spansenter->autoCanonicalize = FALSE.
Pokud k cíli vede několik možných URL, tak jedno z nich je kanonické a ostatní se na ně přesměrují.
Funguje to tak, že router převede HTTP požadavek na objekt PresenterRequest. Aplikace poté z tohoto objektu
zpětně vygeneruje URL a pokud není ekvivalentní s aktuálním, dojde k přesměrování.
Příklad kanonizace:
$router[] = new Route('index.php', array(
'spansenter' => 'Blog',
'action' => 'default',
), Route::ONE_WAY);
Při tomto tvaru rout budou všechny níže uvedené adresy přesměrovány na adresu
http://example.com/blog/nejaky-clanek, což se hodí při zachování zpětné kompatibility odkazů, které již
vedou na váš web z internetu.
● http://example.com/index.php?spansenter=blog&id=nejaky-clanek
● http://example.com/?spansenter=blog&id=nejaky-clanek
● http://example.com/blog/index.php?id=nejaky-clanek
● http://example.com/blog/?id=nejaky-clanek
● http://example.com/blog/nejaky-clanek/
K přesměrování nedojde při AJAXovém nebo POST požadavku (protože by došlo ke ztrátě dat, které jsou
posílány), takže se nemusíte bát, že za cenu SEO optimalizovaných adres budete muset něco obětovat.
Foo parametry
Foo parametry rozšiřují možnosti definice rout. Narozdíl od klasických parametrů nemají název (místo něj se
použije otazník), nepředávají se spansenteru a slouží k tomu, aby bylo možné do masky přidat regulární výraz.
Pokud by uvedená routa byla obousměrná, generovala by cestu index, kterou však sama neumí akceptovat.
Výraz by se proto musel rozšířit i o prázdnou hodnotu na 'index<? \.html?|\.php|>'.
Nebo lze explicitně definovat řetězec, který bude při generování cesty použit (obdoba výchozí hodnoty
u skutečných parametrů). Řetězec se vloží ihned za otazník:
Volitelné sekvence
V routovací masce třídy Route lze označovat tzv. volitelné sekvence. Ty se uzavírají do hranatých závorek:
//Akceptuje cesty:
// /cs/download => lang=cs, name=download
// /download => lang=NULL, name=download
Výhodou je, že volitelný parameter se může nacházet i uprostřed masky, ale především lze definovat jeho okolí,
v tomto případě znak lomítka, které musí parametr, pokud je uveden, obklopovat. To lze využít například
u volitelných subdomén:
// Akceptuje cesty:
// /cs/stranka
// /en-us/stranka
// /stranka
// /stranka/page-12
// Akceptuje cesty:
// /index.html
// /index
//
// Generuje:
// /index
Generování URL
Při generování cest se používá pravidlo nejkratšího URL, takže všechno, co lze vynechat, se vynechá. Právě
proto routa index[.html] generuje, jak jsem naznačil výše, cestu index.
Pokud byste naopak chtěli generování volitelné sekvence vynutit, napište za levou závorku vykřičník:
// Akceptuje cesty:
// /index.html
// /index
// Generuje:
// /index.html
Zanořování a parametry
Interní poznámka: volitelné parametry (tj. parametry mající výchozí hodnotu) mimo hranaté závorky se chovají
v podstatě tak, jako by byly uzávorkovány následujícím způsobem:
// tento zápis
$route = new Route('<spansenter>/<action>/<id>', array(
'spansenter' => 'Dashboard',
'action' => 'default',
'id' => NULL,
));
Pokud byste chtěli ovlivnit chování zpětných lomítek, tj. aby se místo např. dashboard/view/ generovalo
dashboard/view, lze toho docílit takto:
A to z důvodu, že spansentery Product, Basket, Customer chcete mít v URL respanzentované jako
produkt, kosik, zakaznik. Věc je možno řešit buď vytvořením více rout (což však bude zpomalovat aplikaci),
nebo definicí vlastních filtračních funkcí:
function myFunctionIn($s) {
return ...;
}
function myFunctionOut($s) {
return ...;
}
Což je zase poměrně komplikované. Třída Route proto nabízí možnost nastavit překladovou tabulku:
Route::setStyleProperty('spansenter', Route::FILTER_TABLE, array(
'produkt' => 'Product',
'kosik' => 'Basket',
'zakaznik' => 'Customer',
));
Zajímavost: jeden spansenter může být uveden pod více ruznými kliči. Pak k němu povedou všechny varianty
(tedy vytvoří se aliasy), s tím, že za kanonickou se považuje ta poslední.
Filtry rout
Pokud používáte jako parametr v routách řetězec, který obsahuje encodovatelné znaky (například lomítko či
mezeru), jsou tyto znaky nahrazeny implicitně funkcí rawurlencode při generování, tak jak je nastaveno ve třídě
Route v proměnné styles. Klasickým případem je Routa pro soubory, kde je parametr path může nabývá tvarů
filesystemu, tzn. řetězec s lomítky.
Takováto routa by generovala v parametru cesty s lomítkem přeloženým za %2F, jelikož výchozí styl má
nastaveno self::FILTER_OUT => 'rawurlencode'.
Jelikož jde o statickou proměnnou, můžeme chování jednoduše upravit, nebo definovat styl vlastní přímo pro
parametr path:
Route::$styles['path'] = array(
Route::PATTERN => '.*?',
);
Všimněte si, že již není dále třeba u parametru path určovat filtr (což je regulární výraz .*?) explicitně v definici
routy. Téhož výsledku lze dosáhnout i takto:
Route::addStyle('path', NULL);
Route::setStyleProperty('path', Route::PATTERN, '.*?');
Route::setStyleProperty dělá to samé jako <url .*>, ale klíčový rozdíl je právě v tom, že druhý parametr
Route::addStyle říká, že nechceme zdědit standartní filtrování pomocí rawurlencode.
Dejme tomu, že do website programované v Nette chceme přidat fórum. Stačí, aby fórum disponovalo instalační
funkcí createRoutes():
class Forum
{
function createRoutes($router, $spanfix)
{
$router[] = new Route($spanfix . 'index.php', array(
'spansenter' => 'Forum:Homepage',
'action' => 'default',
));
$router[] = new Route($spanfix . 'admin.php', array(
'spansenter' => 'Forum:Admin',
'action' => 'default',
));
...
}
}
$router = $application->getRouter();
// přidáme své routy
...
// přidáme modul forum
Forum::createRoutes($router, '//forum.example.com/');
Multijazyčnost
Nette Framework nikomu nevnucuje konkrétní řešení. Otázkou mnohých složitějších systémů je multijazyčnost.
Základem k jejímu vyřešení je dobrý návrh rout. Možností jak vyřešit tento požadavek je spostu a vše záleží jen na
Vaší představivosti. Můžete se inspirovat na pár příkladech:
Viz také:
● Routování
● Fórum: Příklady routeru
● Fórum: Routovací tipy a triky
« SimpleRouter MultiRouter »
Nette\Application\MultiRouter
Hromadné úložiště pro routy aplikace.
Jelikož Nette narozdíl od jiných frameworků nepojmenovává routy, je zde důmyslný mechanismus, který
z jednoho globálního úložiště na routy vybere odpovídající routu (objekty Route a SimpleRouter), pro kterou
požadavek nejvíce vyhovuje. To, zda-li se vybere úspěšně ta routa, kterou jsme zamýšleli, závisí na našich
konkrétních definicích masek rout. Obecně platí pravidlo, že routy deklarujeme postupně od těch nejvíce
specifických po ty obecné. Tím se vyhneme případným kolizím.
MultiRouter je v základu potomek objektů z jmenného prostoru Nette\Collections, konkrétně objektu ArrayList,
který je přímým potomkem Collection. Sám implementuje i rozhraní IRouter stejně jako Route a SimpleRouter.
Tato hierarchie poskytuje vlastnosti, díky kterým je routování v Nette čistě a rychle napsáno (rychlost parsování
URL je ekvivalentní jako RewriteRules z .htaccess souborů), proto nemá cenu například nastavení MultiRouteru
kešovat. Naopak – funguje rychleji, než když se načítá z keše! Pokud tedy negenerujete routy z databáze, nemá
cenu se keší zabývat.
Na závěr se sluší dodat, že jako vše v Nette je možno napsat vlastní router a implementovat si routovací techniky
dle libosti, stačí implementovat daná rozhraní.
// získáme instanci objektu MultiRouter, který slouží jako úložiště pro routy
$router = Environment::getApplication()->getRouter();
Další příklady rout lze nalézt v dokumentacích tříd Route a SimpleRouter a na fóru.
Viz také:
Přihlašování uživatelů je oblast velmi úzce související s ochranou osobních údajů a zabezpečením aplikace. Jelikož
PHP nenabízí žádnou standardní implementatici, jde také bohužel o oblast bezbřehé programátorské „kreativity“.
Lze se setkat s odstrašujícími případy, kdy programátoři například ukládají hesla do cookies a nebo vytvářejí jiné
sofistikované bezpečnostní díry.
Nette Framework se snaží tuto díru zacelit. A zároveň přihlašování zjednodušit až na naprosté minimum. Tím jsou
dvě metody login() (přihlásit) a logout() (odhlásit), plus dotazovací metoda isLoggedIn() sdělující, zda je
uživatel nyní přihlášen.
O realizační stránku se stará třída Nette\Web\User. Ta je, stejně jako v případě Nette\Web\Session, singleton,
proto nevytváříme její instanci přímo, ale vrátí ji metoda Environment::getUser(). Používá se zhruba tímto
způsobem:
require 'Nette/loader.php';
$user = Environment::getUser();
// přihlášení uživatele
$username = ...
$password = ...
$user->login($username, $password);
// je přihlášen?
echo $user->isLoggedIn() ? 'ano' : 'ne';
// odhlášení
$user->logout();
Aby příklad fungoval, je potřeba napsat rutinu, která provede ověření uživatelského jména a hesla. Této rutině se
říká autentizační handler a jde o objekt implementující rozhraní Nette\Security\IAuthenticator. To má jedinou
metodu login(). Implementace, která ověřuje přihlašovací údaje oproti databázové tabulce, může vypadat
třeba takto:
require 'Nette/loader.php';
Autentizační handler si zaslouží hlubší rozbor. Z pohledu návrhu aplikace podle vzoru MVP jde o součást modelu,
přičemž samotnou autentizaci zpravidla iniciuje spansenter. Nette Framework vás tak vede k oddělení ověření
údajů od spanzentační vrstvy.
Úkolem handleru je buď vrátit tzv. identitu v případě úspěchu, nebo vyhodit výjimku. Nette definuje výjimku
Nette\Security\AuthenticationException a několik chybových kódu, které můžete využít k formálnímu popisu
vzniklé chyby. (Nicméně na to, jakou výjimku vyhodíte, se žádná omezení nekladou, nakonec bude je zachytávat a
ošetřovat opět váš kód.)
V případě úspěšné autentizace vrácí handler identitu, což je objekt implementující rozhraní Nette\Security\I-
Identity a popisující aktuálního uživatele. Popis může obsahovat libovolné údaje, povinné je uživatelské jméno (což
nemusí být nutně totéž, jako přihlašovací jméno) a role (o těch si povíme více v příštím dílu). K identitě se
dostaneme přes getter getIdentity():
$user = Environment::getUser();
if ($user->isLoggedIn()) {
echo 'Prihlášen uživatel: ', $user->getIdentity()->getName();
} else {
echo 'Uživatel není přihlášen';
}
Ve verzi 1.0 byla metoda getName() nahrazena metodou getId().
Odhlášení
Jak už jsem zmínil, uživatele odhlásí metoda logout(). Při odhlášení se však nesmaže uživatelská identita,
kterou máme i nadále k dispozici. Pokud bychom chtěli identitu explicitně smazat, odhlásíme uživatele voláním
logout(TRUE).
Kromě manuálního odhlášení nabízí Nette Framework i automatické odhlášení po uplynutí časového intervalu
nebo zavření okna prohlížeče. K tomu slouží metoda setExpiration(), kterou volejte vždy před samotnou
autentizací. Metoda setExpiration() jako parametr akceptuje relativní čas v sekundách nebo UNIX timestamp,
v aktuální verzi frameworku je možné použít i velmi srozumitelný textový zápis. Druhý parametr stanoví, zda se má
uživatel odhlásit při zavření okna prohlížeče:
Dokonce je možné zjistit, z jakého důvodu k poslednímu odhlášení došlo (viz. metoda getLogoutReason).
Shrnutí
Kompletní postup přihlašování uživatele pak vypadá asi takto:
require 'Nette/loader.php';
require 'MyAuthenticator.php';
// přihlašovací údaje
$username = ...
$password = ...
$user = Environment::getUser();
// nastavíme expiraci
$user->setExpiration('+ 30 minutes');
try {
// pokusíme se přihlásit uživatele...
$user->login($username, $password);
// ...a v případě úspěchu spansměrujeme na další stránku
Environment::getHttpResponse()->redirect('index.php');
« MultiRouter Nette\Web\User »
Nette\Web\User
Zajišťuje přihlašování a odhlašování uživatelů, popisuje jejich identitu a ověřuje práva
na základě rolí.
Autentizace (přihlášení)
Přihlášení a odhlášení uživatele:
// přihlášení
$user->login($userName, $password); // předáme přihlašovací jméno a heslo
// jednoduché odhlášení
$user->logout();
Přihlašování vyžaduje u uživatele povolené cookies; jiná metoda přihlašování není bezpečná!
Oveření uživatelského jména a hesla provádí autentizační handler, což je objekt implementující rozhraní
Nette\Security\IAuthenticator. Jeho triviální implementací je třída
Nette\Security\SimpleAuthenticator, která dostane v konstruktoru seznam uživatelů a hesel jakožto
asociativní pole. Úkolem handleru je v metodě login(array $credentials) ověřit, zda uživatelské jméno a
heslo odpovídá a v případě úspěchu vrátit tzv. identitu. Neúspěch indikuje vyhozením výjimky
Nette\Security\AuthenticationException s popisem důvodu. Lze využít i připravené konstanty
IAuthenticator::IDENTITY_NOT_FOUND nebo IAuthenticator::INVALID_CREDENTIAL.
$user->setAuthenticationHandler($authenticator);
Automatické odhlášení
Ve výchozím nastavení je uživatel odhlášen v okamžiku, kdy zavře okno prohlížeče. Chování lze změnit metodou
setExpiration(). První parametr určuje čas v sekundách, za který bude uživatel v případě neaktivity odhlášen.
Druhý parametr říká, zda uživatele odhlásit v okamžiku zavření okna prohlížeče. Třetí parametr stanoví, zda při
odhlášení smazat identitu.
Pokud při ověřování autentizace uživatele zjistíme, že není přihlášen, můžeme jít ještě dál a zjistit příčinu
odhlášení. Nette umožňuje zjistit metodou getLogoutReason() základní příčiny odhlášení jako zavření okna
prohlížeče, neaktivita nebo manuální odhlášení uživatele. Můžeme tak například pomocí flash zpráviček dát
uživateli vědět o příčině jeho odhlášení.
Z jakého důvodu byl uživatel odhlášen prozradí metoda $user->getLogoutReason(). Pokud vypršel časový
limit vrací User::INACTIVITY, pokud uživatel zavřel okno prohlížeče vrací User::BROWSER_CLOSED a pokud byl
uživatel odhlášen voláním metody logout() vrací User::MANUAL.
// user authentication
$user = Environment::getUser();
if (!$user->isLoggedIn()) {
if ($user->getLogoutReason() === User::INACTIVITY) {
$this->flashMessage('You have been logged out due to inactivity. Please login again.');
}
Dále je možno využít událostí onLoggedIn a onLoggedOut například pro jednoduchého přidání callbacku pro
logování autorizačních aktivit na webu. Událost onLoggedIn je volána po úspěšném přihlášení. Událost
onLoggedOut je zpracována po odhlášení uživatele, ať už se odhlásil sám nebo z důvodu neaktivity či zavření
prohlížeče.
Expirace Nette\Web\Session musí být nastavena na stejnou nebo vyšší hodnotu, jakou má
expirace přihlášení
Identita
Identita je objekt implementující rozhraní Nette\Security\IIdentity. Nette\Web\User jej udržuje v session.
Jeho výchozí implementace Nette\Security\Identity udržuje informaci o jméně, rolích a dalších uživatelských
datech (na uživatelská data si můžeme šáhnout přes Environment::getUser()->getIdentity()->promena).
Ačkoliv má uživatel identitu, nemusí být přihlášený! Identita se sice obvykle získá při přihlášení, ale i když vás
systém po nějaké době odhlásí ($user->logout()), stále si ji pamatuje (jméno, obsah košíku na eshopu, …).
Autorizace
Autorizace = zjištění, zda má uživatel právo to či ono udělat. Rozhoduje se na základě rolí a toho, zda je uživatel
přihlášen. V nejjednodušších případech si vystačíme právě s indikátorem přihlášení:
if ($user->isLoggedIn()) ..
if ($user->isInRole('editor')) ..
if ($user->isAllowed($resource, $privilege)) ..
Protože uživatel může mít více rolí, povolení dostane, pokud alespoň jedna role má povolení. Oba parametry jsou
volitelné, výchozí hodnota nese význam všechny. Takže pokud např. parametr $privilege nevyužijeme, můžeme ho
vynechat.
Ještě upozornění: pokud uživateli po odhlášení zůstane identita, tak i včetně všech rolí – získatelných přes
$user->getIdentity()->getRoles(). Nicméně metoda $user->getRoles() stav přihlášení zohledňuje.
Stejně tak i dotazy isInRole() a isAllowed() – proto je není nutné kombinovat s dotazem isLoggedIn().
Environment::getServiceLocator()->addService($authenHandler, 'Nette\Security\IAuthenticator');
Environment::getServiceLocator()->addService($authorHandler, 'Nette\Security\IAuthorizator');
// kde handler je buď hotový objekt (pak to ale není lazy), jméno třídy nebo callback na továrnu
service.Nette-Security-IAuthenticator = MyAuthenticator
$user->setNamespace('forum');
Viz také:
● Nette\Web\User
● Nette\Security
● API reference
Třída disponuje pouze dvěma veřejnými metodami getName(), která vrací jméno identity, a getRoles(), která
vrátí seznam všech rolí, kterých identita nabyla. Také lze přistupovat k uživatelským datům jako k property.
Nette\Web\User udržuje tuto identitu v session a slouží ke všem autorizačním a autentizačním procesům. Identita
se sice obvykle získá při přihlášení, ale i když vás systém po nějaké době odhlásí, stále si ji pamatuje (toto chování
lze upravit a můžeme při odhlášení identitu vymazat).
Viz také:
« Nette\Web\User SimpleAuthenticator »
Nette\Security\SimpleAuthenticator
Triviální implementace autentizačního handleru.
Autentizace
Autentizace je proces ověření proklamované identity subjektu.
Patří k bezpečnostním opatřením a zajišťuje ochranu před falšováním identity, kdy se subjekt vydává za někoho,
kým není. V Nette rozlišujeme autentizaci entity (osoby).
Ověření uživatelského jména a hesla provádí autentizační handler, což je objekt implementující rozhraní
Nette\Security\IAuthenticator.
Úkolem handleru je ověřit, zda uživatelské jméno a heslo odpovídá a v případě úspěchu vrátit tzv. identitu.
Příklad použití:
// přihlášení
$user->login('kathy', '12345'); // předáme přihlašovací jméno a heslo
// jednoduché odhlášení
$user->logout();
Viz také:
● Nette\Security\Identity
● Nette\Security\Permission
● Nette\Security\SimpleAuthenticator API reference
● Nette\Security\IAuthenticator API reference
● Dynamická správa rolí a zdrojů
Autorizace se může v Nette Framework vyhodnocovat na základě členství uživatele v určitých skupinách či
přidělených rolích. Začněme ale od úplného začátku.
Triviální autorizace
Zopakuji, že autorizace předpokládá předchozí úspěšnou autentizaci, tj. že uživatel je spolehlivě přihlášen.
U jednoduchých webů s administrací, do které se přihlašuje jen jeden uživatel, je možné jako autorizační kritérium
použít již známou metodu isAuthenticated(). Řečeno srozumitelnějším jazykem: jakmile je uživatel přihlášen,
má veškerá oprávnění a naopak.
// use Nette\Environment;
$user = Environment::getUser();
Druhým parametrem konstruktoru Identity je buď řetězec s názvem role, nebo pole řetězců – rolí.
Jako autorizační kritérium nyní použijeme metodu isInRole(), která prozradí, zda-li uživatel vystupuje
v dané roli:
$user = Environment::getUser();
Výhodou a smyslem rolí je nabídnout přesnější řízení oprávnění, ale zůstat nezávislý na uživatelském jméně.
Ještě musím zmínit jednu důležitou věc. Jak už totiž víte, po odhlášení uživatele nemusí dojít ke smazání jeho
identity. Tedy i nadále metoda getIdentity() vrací objekt Identity, včetně všech udělených rolí. Nette
Framework vyznávající princip „less code, more security“, kdy méně psaní vede k více zabezpečenému kódu,
nechce nutit programátora všude psát if ($user->isAuthenticated() && $user->isInRole('admin'))
a proto metoda isInRole() pracuje s efektivními rolemi. Pokud uživatel je přihlášen, vrací se role udělené
v autentizačním handleru, pokud přihlášen není, má virtuální roli guest.
Autorizační handler
Představuje nejjemnější možné řízení oprávnění. Autorizační handler je objekt implementující rozhraní
Nette\Security\IAuthorizator. To má jedinou metodu isAllowed() s úkolem rozhodnout, zda má daná role
povolení provést určitou operaci s určitým zdrojem. Rámcová podoba implementace vypadá takto:
require 'Nette/loader.php';
$user = Environment::getUser();
V tuto chvíli záleží čistě na vás, jak implementujete autorizační handler. Nicméně Nette Framework disponuje
i jednou poměrně silnou předpřipravenou implementací.
// definujeme role
$acl->addRole('guest');
$acl->addRole('member');
$acl->addRole('administrator', 'member'); // administrator je potomkem member
// definujeme zdroje
$acl->addResource('file');
$acl->addResource('article');
Na opravnění se poté opět dotazujeme metodou isAllowed(), jak bylo ukázáno výše.
« SimpleAuthenticator Permission »
Nette\Security\Permission
Ověřuje práva a přístupy k objektům na základě rolí.
Tato vrstva velmi úzce souvisí s objektem Nette\Web\User a jejím základem je definování pravidel (rules).
Autorizace (zjištění, zda má uživatel právo to či ono udělat) rozhoduje se na základě rolí (roles), zdrojů/objektů
(resources) a práv/akcí (privileges), která v celé aplikaci určí kdo může přistupovat k chráněnému objektu a jaké
akce s ním může vykonávat.
● Nepřihlášený (neautentizovaný) návštěvník webu (což je výchozí role Nette\Web\User) může číst a procházet
veřejnou část webu, tzn. číst články, komentáře, novinky a volit v anketách.
● Oproti tomu přihlášený uživatel, který je registrován může dostávat novinky mailem a komentovat.
● Další uživatelé mají přístup k administrační části a mohou provádět úkony jako psát a spravovat své příspěvky
a provádět různé změny v aplikaci (třeba změnit vzhed, záhlaví, zápatí) a mají samozřejmě i práva procházet
veřejnou část a dostávat novinky mailem.
Nadefinovali jsme si tedy určité role (guest, registered a administrator) a objekty (news, article,
poll, comments, backend), ke kterým mohou uživatelé s nějakou rolí přistupovat nebo provádět určité akce (
view, vote, comment, feed, edit).
Jednoduše řečeno
● role (role) je vlastnost uživatele, který může přistupovat k objektům/zdrojům
● zdroj (resource) je objekt který je kontrolován
● práva (privilege) jsou akce, které může s objektem provádět uživatel s rolí
Vzájemné vazby kdo co může s čím dělat ale určují až pravidla (rules), která jsou uchovávána právě objektem
Permission. Ten toho umí samozřejmě víc, než jen uchovávat pravidla.
Zároveň je tu ještě jedna vazba, možná ne na první pohled zcela zřejmá, a to dědičnost rolí, která zajistí, že
uživatel s rolí administrátor může dělat i to co obyčejný návštěvník webu.
Poznámka: práva administrátora lze nadefinovat i jako ‚bez omezení‘ tzn. bez rodičů od kterých by dědil nějaká
omezení (viz níže).
Začínáme
Nejprve si ukážeme, jak nadefinovat objektu Permission uživatelské role.
Ještě před tím je ale třeba vytvořit instanci třídy, se kterou budeme pracovat.
Role (Roles)
Jak již bylo řečeno, budeme postupně vytvářet stromovou strukturu rolí, která dědí oprávnění od svých rodičů.
Použijeme výše zmíněný příklad webové aplikace o třech rolích. Mějmě tedy role guest, registered a
administrator.
$acl->addRole('guest');
$acl->addRole('registered', 'guest');
$acl->addRole('administrator', 'registered');
Docela triviální že? Tímto zajístíme, že se nám vlastnosti přenášejí z rodičovské role na potomky.
Rodiče lze zadat i jako pole s více rolemi.
Za zmínku stojí metoda getRoleParents(), která vrací pole se všemi rodičovskými rolemi a také metoda
roleInheritsFrom(), která zjistí, zda-li od sebe dědí dvě role. Jejich použití:
$acl->addResource('news');
$acl->addResource('article');
$acl->addResource('poll');
$acl->addResource('comments');
$acl->addResource('backend');
// registrovaný dědí právo view od hosta, ale má i právo komentovat a dostávat novinky mailem
$acl->allow('registered', 'comments', 'comment');
$acl->allow('registered', 'news', 'feed');
Autorizace
Nyní když máme vytvořený seznam pravidel, můžeme jednoduše provádět autorizaci.
Práva administrátora lze nadefinovat i jako ‚bez omezení‘ tzn. bez rodičů od kterých by dědil nějaká omezení.
Vypadalo by to asi takto:
Můžete si všimnout že kdykoliv za běhu aplikace můžeme i odebrat roli. A nejenom ji, odebírat ze seznamu
pravidel lze objekty: removeResource(), removeAllResources(); ale i samotná pravidla: removeAllow(),
removeDeny(). Máme zde i metody na kontrolu přítomnosti nějaké role nebo objektu: needRole(),
needResource(); které vyhodí výjimku InvalidStateException pokud není zjištěna jejich přítomnost.
Jak již bylo řečeno, role může dědit od jiné role či od více rolí. Co se ale stane pokud má jeden předek akci
zakázanou a druhý povolenou? Jaké budou práva potomka? Určuje se to podle váhy role – poslední uvedená role
v seznamu předků má největší váhu, první uvedená role tu nejmenší. Více názorné je to z příkladu:
$acl->allow('admin', 'backend');
$acl->deny('guest', 'backend');
Napojení na Nette\Web\User
Na začátku jsem zmiňoval, že celá třída Nette\Security\Permission velmi úzce navazuje na
Nette\Web\User. Dá se používat zcela bez něj, ale spolu dosáhnou maximální efektivity.
nebo v config.ini
service.Nette-Security-IAuthenticator = MyAuthenticator
service.Nette-Security-IAuthorizator = Permission
return new Identity('John Doe', $roles); // zde je důležité právě předání rolí
}
// bez Environment
$acl = new Permission;
$user = new User;
// samotná autorizace
$user->isAllowed('file', 'delete_file'); // TRUE
$user->isAllowed('jany', 'sleep_with_jany'); // FALSE
$user->isAllowed('jany', 'say_hello'); // TRUE
// obecně
$user->isAllowed($resource, $privilege);
Metoda isAllowed() má nyní jen dva parametry, role se sama doplní pomocí metody getRoles() a v cyklu
se projdou všechny role a při prvním kladném vyhodnocení se kladně vyhodnotí i podmínka výše. To vše je již
implementováno v Nette\Web\User.
U proměnné $user můžeme dále používat veškeré metody uvedené v Nette\Web\User.
Viz také:
V praxi u složitějších aplikací (v aplikacích kde chceme umožňovat právě dynamickou úpravu rolí, zdrojů) si
nejspíše nevystačíme se základním objektem Nette\Security\Permission. V malých aplikacích můžeme bez
problémů „natvrdo“ nadefinovat seznam rolí, zdrojů a pravidel někde v aplikaci, ale dojde-li na nějaké úpravy nebo
rozšíření rolí, budeme muset ručně do aplikace zasahovat.
U jednoduchých případů lze zaregistrovat jako autorizační handler přímo třídu Nette\Security\Permission. Pro naši
potřebu si ale vytvoříme jejího potomka, třídu Acl, kterou obohatíme o konstruktor, který sestaví všechna pravidla,
zdroje a role.
Uvažujeme se strukturou databáze stejnou jako na obrázku. Umožňuje zadat v tabulce acl resource_id
i privilege_id NULL, taková situace znamená, že se pravidlo aplikuje na všechny dostupné resources či privileges.
foreach($model->getRoles() as $role)
$this->addRole($role->name, $role->parent_name);
foreach($model->getResources() as $resource)
$this->addResource($resource->name);
foreach($model->getRules() as $rule)
$this->{$rule->allowed == 'Y' ? 'allow' : 'deny'}($rule->role, $rule->resource, $rule
->privilege);
}
…nebo v config.ini:
service.Nette-Security-IAuthorizator = Acl
Pokud chceme objekt třídy Acl cachovat (nesmíme zapomenout invalidovat cache při každé změně ACL
v databázi):
$cache = Environment::getCache();
if (!isset($cache['acl'])) $cache['acl'] = new Acl();
Environment::getServiceLocator()->addService('Nette\Security\IAuthorizator', $cache['acl']);
Tím jsme si z tabulky vygenerovali objekt, který je potomkem Nette\Security\Permission a umožňuje nám
v aplikaci používat dynamickou správu rolí, a zaregistrovali potřebnou službu.
Viz také:
● Nette\Security\Permission
● Nette\Security\Permission API reference
« Permission AJAX »
Ajax & snippety
AJAXový požadavek lze detekovat metodou třídy (resp. služby) zapouzdřující HTTP požadavek:
Environment::getHttpRequest()->isAjax() (detekuje podle HTTP hlavičky X-Requested-With). Uvnitř
spansenteru je k dispozici „zkratka“ v podobě metody $spansenter->isAjax().
AJAXový požadavek se nijak neliší od klasického požadavku – je zavolán spansenter s určitým view a parametry.
Je také věcí spansenteru, jak bude na něj reagovat: může použít vlastní rutinu, která vrátí nějaký fragment HTML
kódu (HTML snippet), XML dokument, JSON objekt nebo kód v JavaScriptu. K tomu lze využít i předpřipravený
objekt šablony – například tak, že při detekci AJAXu zvolí speciální šablonu:
Nicméně daleko silnější nástroj představuje vestavěná podpora AJAXu. Díky ní lze udělat z obyčejné aplikace
AJAXovou prakticky několika řádky kódu.
Myšlenka vychází z toho, že při prvotním (tedy neAJAXovém) požadavku se přenese celá stránka a poté se při
každém již AJAXovém subrequestu (= požadavku na stejný spansenter a view) přenáší pouze kód změněných částí.
K tomu slouží dva mechanismy: invalidace a renderování snippetů.
Invalidace
Každý objekt třídy Control (což je i samotný Presenter) si umí zapamatovat, jestli při subrequestu došlo ke
změnám, které si vyžadují jej překreslit. K tomu slouží triptych metod invalidateControl(),
validateControl() a isControlInvalid(). Příklad:
Nette však nabízí ještě jemnější rozlišení, než na úrovni komponent. Uvedené metody mohou totiž jako parametr
nabývat název tzv. „snippetu“, nebo-li ústřižku. Lze tedy invalidovat/validovat na úrovni těchto snippetů (každý
objekt může mít libovolné množství snippetů). Pokud se invaliduje celá komponenta, tak je i každý snippet
považován za invalidní. Komponenta je invalidní i tehdy, pokud je invalidní některá její subkomponenta.
Díky invalidaci snippetů přesně víme, které části kterých prvků bude potřeba překreslit.
Renderování snippetů
Nette je založeno na myšlence logických, nikoliv grafických prvků, tj. objekt třídy Control nepředstavuje
pravoúhlou oblast ve stránce, ale logickou komponentu, která se může renderovat i do více podob (např. DataGrid
může mít jednu metodu pro vykreslení mřížky a druhou pro vykreslení „stránkovadla“ apod). Každý prvek může být
navíc na stránce vykreslen vícekrát, nebo podmíněně, nebo pokaždé s jinou šablonou atd.
Není tedy možné jednoduše zavolat metodu render na každém invalidním objektu, vlastně ani žádný interface
metodu render nedefinuje. K vykreslování je nutné přistupovat tak, jako když se kreslí celá stránka. (Přesněji
řečeno, je možné jít touto cestou a vykreslit pouze invalidní snippety invalidních objektů, ale vaše aplikace musí
být k tomu účelu vhodně navržena, kdežto vestavěná podpora AJAXu musí fungovat obecně).
Vykreslování stránky probíhá velmi podobně, jako u neAJAXového požadavku, načtou se tytéž šablony atd.
Klíčovým úkolem však je vypustit ty části, které se na výstup vůbec dostat nemají, a ty, které se vykreslit mají,
přidružit s identifikátorem a poslat klientovi ve formátu, kterému bude obslužný JavaScript rozumět.
Pokud se uvnitř šablony nachází control nebo snippet, musíme jej obalit párovou značkou {snippet} ...
{/snippet} – ty totiž zajistí, že se vykreslený snippet vystřihne a předá AJAXovému ovladači. Také jej obalí
pomocnou značkou <div> (lze použít i jinou značku). V uvedeném příkladě je snippet pojmenován jako header a
může představovat i například šablonu controlu:
{snippet header}
<h1>Hello .... </h1>
{/snippet}
Při využití této techniky je nutné si dát pozor na vkládané šablony pomocí {include $template}, které
obsahují nějaké snippety, aby nebyly přeskočeny. Tomu lze zabránit přidáním zavináče před složené závorky, tedy
použitím @{include $template}, který nechá na snippetu rozhodnutí, zda-li se má překreslit (rozhodne se
podle proměnné SnippetHelper::$outputAllowed) a vkládanou šablonu správně obalí podmínkami. Šablona,
do které vkládáme jinou šablonu obsahující snippet by vypadala například takto:
<html>
<body>
@{include $template}
</body>
</html>
Více informací k zavináčové magii.
Komunikace s prohlížečem
Jednotlivé snippets společně s dalšími (i uživatelskými daty) se předají do úložište, které je dostupné přes
$spansenter->payload. Presenter předává data ve formátu JSON.
Jak to celé funguje pohromadě demostruje příklad Fifteen, jehož kód najdete v distribuci.
Propojení s Nette\Config
Třída Config má na starosti načtení konfigurace prostředí složené z kombinací nastaveních, služeb, proměnných a
konstant. Doporučuje se používat konfiguraci uloženou ve formátech ini a xml, jelikož parsery pro tyto soubory
jsou nativně podporovány v PHP a jsou velmi rychlé. Dokonce tak rychlé, že naparsovanou konfiguraci se nevyplatí
ani kešovat.
Dědění podsekcí.
V rámci sekce pak lze vypnout dělení klíčů pomocí tečky – za název sekce se přidá vykřičník – tečky jsou poté
brány jako součást názvu direktivy.
Poznámka k set.include_path: V linuxových systémech se používá jiný oddělovač cest než na Windows
systémech. Nette\Config řeší tento problém použitím univerzálního oddělovače cest v konfiguračním souboru
(středník), pak při běhu nahradí tento oddělovač za oddělovač cest konkrétní platformy, tudíž nedochází k žádným
nekompatibilitám.
Název prostředí
Prostředí je zjednodušeně název počítače, na kterém aplikace právě běží. Může to být jeden z počítačů, kde
probíhá vývoj, může to být produkční server. Každé prostředí má jiné parametry (cesty k adresářům, připojení
k databázi, …), mohu si je pojmenovat a podle názvu prostředí načíst kupříkladu konfiguraci:
Environment::setName('mujpocitac');
...
Environment::loadConfig(); // načte z config.ini sekci [mujpocitac]
V případě většího množství prostředí není nutné přepisovat všechna společná nastavení do každého prostředí
zvlášť. Následující kód (zkopírovaný z ukázkového souboru na začátku této stránky) zajistí, že i v prostředí
production se použijí nastavení z prostředí common
Režimy prostředí
Režim neboli mód je indikátor, určující nějaký parametr daného prostředí. Módy lze nastavovat buď přes config.ini,
nebo přímo voláním Environment::setMode('mujmod', $bool), zjišťovat stav lze přes
Environment::getMode('mujmod').
Autodetekce
Třída Nette\Environment disponuje vestavěnou autodetekcí pro režimy production, debug, console a pro
název prostředí (a jako téměř vše v Nette ji lze rozšířit nebo přepsat). Asi nejdůležitější mód production určuje,
jestli aplikace běží na ostrém (produkčním) serveru nebo ne. Proto také existuje zkratka, místo
Environment::getMode('production') lze volat výstižnější Environment::isProduction(). Pro režim
console existuje obdobná zkratky Environment::isConsole().
mode.production = TRUE
Autodetekce názvu prostředí úzce souvisí s detekcí režimů production & console, protože právě na základě
nich se název zvolí z variant Environment::DEVELOPMENT, Environment::PRODUCTION nebo
Environment::CONSOLE.
Příklad
…a přímo ze života: mám jednu aplikaci, která běží v pěti různých prostředích:
1. 2× na lokálním serveru, kde probíhá vývoj (můj počítač + virtuální testovací stroj)
2. 1× na serveru tojeono.cz (jako texy.info)
3. 2× na serveru hostmonster.com (jako nette.org a dibiphp.com)
Každé prostředí může mít jiný název. Prostředí 2) a 3) budou mít nejspíš vždy aktivní režim production.
V prostředí 1) budu vyvíjet nejčastěji v „neživém“ režimu, ale před nahráním na server si mohu mód production
ručně aktivovat a ověřit, jestli všechno funguje v pořádku. Mezi názvy prostředí a režimy tedy není žádná přímá
souvislost, krom autodetekce.
Viz také:
« AJAX Komponenty »