Naší návštěvní knihu nebudeme psát úplně od základů, pomůžeme si kostrou aplikace,
která je dostupná v distribučním balíku s Nette. Nachází se ve složce tools/Skeleton
a obsahuje předpřipravenou adresářovou strukturu, několik základních tříd a dalších souborů,
které nám usnadní práci.
Příprava skeletonu je zde popsána jen stručně a pro úplnost, větší popis
je obsahem jiných tutoriálů.
Začneme vytvořením složky v adresáři přístupném z testovacího webového serveru
a rozbalíme do ní obsah skeletonu. Do složky libs nakopírujeme Nette a dibi. Dále do složky document_root/js
nakopírujeme jQuery.
A abychom si tu obsluhu AJAXu v jQuery nemuseli psát sami, využijeme
již připravených skriptů: jquery.nette.js
a jquery.ajaxform.js - s nimi do stejné složky, jako s jQuery.
Také bude dobré si ihned JavaScriptové knihovny do stránky nalinkovat, ať na to
později nezapomeneme. Do hlavičky v souboru app/templates/@layout.phtml přidáme:
Aby naše návštěvní kniha vypadala alespoň trošku k světu, stáhneme si mírně upravený
soubor screen.css a
umístíme jej do složky document_root/css.
Protože budeme psát návštěvní knihu, bude také moudré si připravit nějakou tu
databázi. Použijeme SQLite, které je dostupné téměř vždy a všude. Databáze to bude
opravdu jednoduchá - vystačíme si s jedinou tabulkou entries:
CREATE TABLE [entries] (
[id] INTEGER NOT NULL PRIMARY KEY,
[author] VARCHAR(50) NOT NULL,
[posted] TIMESTAMP NOT NULL,
[ip] VARCHAR(15) NOT NULL,
[text] TEXT NULL
);
CREATE INDEX [IDX_ENTRIES_POSTED] ON [entries] (
[posted] ASC
);
Celou databázi si můžete stáhnout: database.sdb. Umístěte do složky app/models.
Poskytnutá databáze je ve formátu SQLite 2. Můžete si stáhnout i databázi ve formátu SQLite 3. Poté ale nesmíte zapomenout použít v dibi
driver sqlite3. Pokud preferujete MySQL, je k dispozici i export pro MySQL.
Nezapomeňte, že pokud chcete, aby kniha návštěv fungovala, musí mít webserver oprávnění zapisovat nejen do souboru s databází, ale i do složky, ve které je tato databáze umístěna - v našem případě složka app/models. Pokud pracujete na systému, který vychází z unixu, zvažte použití příkazu chmod -R a+rwX app/models.
Začínáme
Nyní se již od kopírování a rozbalování knihoven můžeme pustit do samotné tvorby.
Abychom demonstrovali jednoduchost a sílu AJAXu v Nette, vytvoříme nejdříve aplikaci
bez jeho použití a až poté přidáme AJAX - se zachováním stejné funkčnosti.
Začneme tedy vytvořením jednoduchého modelu a připojením k databázi.
Modely a databáze
Ačkoliv v tomto tutoriálu budeme pracovat jen s jedinou tabulkou a tím pádem
si vystačíme s jediným modelem (a tedy jedinou třídou), vytvoříme si modely dva:
abstraktní BaseModel, který poslouží jako šablona pro další modely (co když bude
potřeba zítra do aplikace přidat další funkce?), a EntriesModel, který bude
reprezentovat samotnou tabulku entries v databázi.
Modely
Jak již bylo řečeno, BaseModel poslouží jako kostra pro další modely.
Bude obsahovat nejen několik základních funkcí pro práci s danou tabulkou,
ale také se nám postará o připojování k databáze a odpojování od ní. Vytvoříme
si následující soubor app/models/BaseModel.php:
<?php
abstract class BaseModel extends Object
{
/********************* Connection handling *********************/
/** @var DibiConnection */
public static $defaultConnection;
/**
* Establishes the database connection.
*/
public static function connect()
{
// use configuration from config.ini
self::$defaultConnection = dibi::connect(Environment::getConfig('database'));
}
/**
* Disconnects from the database.
*/
public static function disconnect()
{
self::$defaultConnection->disconnect();
}
/********************* Model behaviour *********************/
/** @var DibiConnection */
protected $connection;
/** @var string object name */
protected $name;
/** @var string primary key name */
protected $primary;
/** @var bool autoincrement? */
protected $autoIncrement = TRUE;
public function __construct(DibiConnection $connection = NULL)
{
$this->connection = ($connection !== NULL ? $connection : self::$defaultConnection);
}
/**
* Selects rows from the table in specified order
* @param array $order
* @return DibiResult
*/
public function fetchAll(array $order = array())
{
return $this->connection->query(
'SELECT * FROM %n', $this->name,
'%ex', (!empty($order) ? array('ORDER BY %by', $order) : NULL)
);
}
/**
* Inserts a new row
* @param array $values to insert
* @return
*/
public function insert(array $values)
{
return $this->connection->insert($this->name, $values)
->execute($this->autoIncrement ? dibi::IDENTIFIER : NULL);
}
}
Celá třída obsahuje jen ty funkce, které budeme pro náš příklad potřebovat.
Jistě by se našlo místo na další funkce (aktualizace záznamů, složitější vybírání
z databáze, ...), ne však v tomto tutoriálu.
Třída se může na první pohled zdát složitá, po bližším prozkoumání však o nic
složitého nejde. Navíc oč složitější je tato třída, o to jednodušší budou další
modely. app/models/EntriesModel.php bude vypadat takto:
Obsahuje jen definici názvu tabulky. Nic dalšího potřeba skutečně není.
Připojení k databázi
Tento tutoriál je psán pro Nette 0.9. V současné verzi Nette 2.0 se konfigurace provádí trochu jinak, viz Konfigurace Nette Frameworku.
Metoda BaseModel::connect() nám sice umožňuje připojit se k databázi,
musíme jí ale někde zavolat a také musíme do config.ini zapsat údaje
pro připojení.
Začneme tedy těmi údaji. Do souboru app/config.ini přidáme před začátek sekce
[production < common] následující řádky:
Pokud chcete použít databázi ve formátu SQLite 3, použijte driver sqlite3.
Formáty nejsou zaměnitelné, takže nelze načíst SQLite 2 databázi pomocí
driveru sqlite3 a naopak. Pokud zatím s dibi moc nekamarádíte, můžete
se také podívat na příklad konfigurace pro MySQL.
To nám vytvoří podsekci konfigurace s názvem database platnou pro všechna prostředí.
Nyní už zbývá se k databázi připojit. Toho dosáhneme pomocí událostí aplikace.
Do souboru app/bootstrap.php přidáme před volání $application->run(); následující
řádky kódu:
Tím aplikaci nadefinujeme, že se má během svého spouštění připojit k databázi
voláním metody BaseModel::connect() a při ukončování se zase slušně odpojit
voláním BaseModel::disconnect().
A to je k databázi vše. Pokud nyní při otevření aplikace v prohlížeči nespatříte
chybové hlášení, aplikace je připravena pracovat s databází.
Výpis příspěvků a jejich přidávání
Návštěvní kniha většinou obsahuje jen jednu jedinou stránku - pro výpis a současně
i přidávání příspěvků. To nám situaci zjednodušuje a můžeme pracovat na poli
jediného presenteru - HomepagePresenteru. V našem případě by měl obsahovat
několik základních částí:
získání seznamu příspěvků z databáze
vykreslení seznamu v šabloně
definice formuláře a zpracování jeho dat
vykreslení formuláře v šabloně
Začneme seznamem...
Seznam příspěvků
Abychom se dostali k seznamu příspěvků, musíme použít model. Máme několik možností,
jak model vytvářet:
vytvořit si jednu instanci modelu v metodě startup presetneru a tu používat
vytvořit si novou instanci modelu při každém použití v presenteru
vytvořit si chytře jedinou instanci modelu při prvním použití a tu pak používat i později
Poslední způsob je asi nejelegantnější, použijeme proto ten. Jmenuje se lazy loading.
Vytvoříme si jednoduchou funkci (getter), která bude kontrolovat, zda je daná členská
proměnná NULL. Pokud ano, tak vytvoří novou instanci modelu. Na konci tuto
proměnnou vrátí. Třída HomepagePresenter v souboru app/presenters/HomepagePresenter.php
bude vypadat takto:
<?php
class HomepagePresenter extends BasePresenter
{
/** @var EntriesModel */
protected $entriesModel;
/**
* Lazy getter for EntriesModel
* @return EntriesModel
*/
public function getEntries()
{
if ($this->entriesModel === NULL)
$this->entriesModel = new EntriesModel();
return $this->entriesModel;
}
}
V presenteru pak budeme používat členskou proměnnou $entries - díky
taťkovi všech objektů se bude volat náš getter getEntries().
Zde to může působit trochu jako kanón na mouchy, ale je dobré si na podobné
konstrukce zvyknout - líné vytváření objektů je velmi výhodné u větších aplikací.
Pokud budeme mít modelů více a budeme s nimi muset pracovat z více tříd,
pak budeme muset najít nějaké lepší a pohodlnější řešení. Pro jeden model však
zůstaneme u této relativně jednoduché metody.
Budeme pokračovat metodou renderDefault(), která data z databáze načte
a připraví je šabloně:
Pokud si nejste jistí, proč zrovna renderDefault(), konzultujte dokumentaci či
jiný tutoriál.
class HomepagePresenter extends BasePresenter
{
/* .... */
/********************* Default view *********************/
public function renderDefault()
{
$this->template->entries = $this->entries->fetchAll(array('posted' => dibi::DESC));
}
}
A nakonec samotná šablona. Soubor app/templates/Homepage/default.phtml
upravíme takto:
Pokud jsme nikde neudělali chybu, bude výstup nyní následující:
Seznam je prázdný, ale aby ho mohl někdo naplnit, musí mít jak.
Formulář pro přidávání příspěvků
Abychom mohli nějaký formulář v šabloně vykreslit, musíme jej nejdříve nadefinovat.
To uděláme opět ve třídě HomepagePresenter, vytvoříme si na formulář
továrničku. Ta
nám formulář vytvoří až v momentě, kdy je ho skutečně potřeba. U presenterů
s mnoha komponentami a mnoha pohledy by bylo nákladné vytvářet
vždy všechny komponenty a pracné je ručně vytvářet před prvním použitím,
proto nám práci usnadní zmíněná továrnička:
protected function createComponentPostForm()
{
$form = new AppForm();
$form->addText('author', 'Jméno:', 30, 50)
->addRule(Form::FILLED, 'Jméno je povinné.');
$form->addTextArea('text', 'Text:', 50, 8)
->addRule(Form::FILLED, 'Text příspěvku je povinný.');
$form->addSubmit('save', 'Přidat příspěvek');
$form->onSubmit[] = array($this, 'postForm_onSubmit');
return $form;
}
Tato továrnička bude vytvářet formulář s názvem postForm. Tento název pro nás bude
důležitý hlavně při vykreslování formuláře v šabloně.
Formulář má 2 políčka, jedno na jméno a druhé na text. Obě jsou povinná, nechceme
přeci anonymní příspěvky bez textu. Pod políčky je tlačítko na odeslání,
o odeslání se bude starat metoda postForm_onSubmit() v aktuálním třídě (tedy
ve třídě HomepagePresenter). Aby formulář po odeslání příspěvek skutečně přidal,
musíme si onu metodu nadefinovat:
public function postForm_onSubmit(Form $form)
{
$entry = $form->getValues();
$entry['posted'] = new DateTime();
$entry['ip'] = Environment::getHttpRequest()->remoteAddress;
$this->entries->insert($entry);
$this->flashMessage('Váš příspěvek byl uložen. Děkujeme za Váš čas.');
$this->redirect('this');
}
Vykreslení samotného formuláře na stránce provedeme v šabloně pomocí makra {control}, kterému jako parametr udáme název komponenty, v našem případě postForm. Toto makro si od presenteru vyžádá danou komponentu a ten, pokud již komponenta neexistuje, zavolá naší továrničku a komponentu vytvoří.
V názvu komponenty je rozlišována velikost písmen a při použití továrniček začíná název
komponenty vždy malým písmenem. Pokud dostáváte od aplikace chybu o neexistující komponentě,
zkontrolujte právě velikost písmen.
V některých příkladech můžete narazit i na použití makra {widget}. Pokud Vás zajímá, jaký je mezi
těmito dvěma makry rozdíl, tak vězte, že žádný. {control} je jen z historického hlediska aliasem
pro {widget}.
Nyní už by měla kniha fungovat a hosté mohou psát:
Teď už si jistě říkáte: kde je ten slibovaný AJAX? Nebude teď dost práce tam
přidat AJAXová volání, AJAXové zpracování... ? Nebude.
AJAX
Ještě než se pustíme do přidělání AJAXu do samotné aplikace, musíme se postarat
o správné přepsání událostí v JavaScriptu, aby vůbec došlo k jeho volání.
Za tímto účelem si vytvoříme malý script ve složce document_root/js. Nazveme
jej třeba ajax.js. Jeho obsah bude zhruba následující:
/* Volání AJAXu u všech odkazů s třídou ajax */
$("a.ajax").live("click", function (event) {
event.preventDefault();
$.get(this.href);
});
/* AJAXové odeslání formulářů */
$("form.ajax").live("submit", function () {
$(this).ajaxSubmit();
return false;
});
$("form.ajax :submit").live("click", function () {
$(this).ajaxSubmit();
return false;
});
První část scriptu přidá všem odkazům s třídou ajax událost,
která po kliknutí na ně vykoná AJAXový požadavek a zruší přechod
na další stránku. Druhá část, která se týká formulářů, má obdobný efekt:
po odeslání formuláře se data odešlou pomocí AJAXu a odeslání
normální cestou se přeruší. Použití funkce live zajišťuje,
že se událost přidá jak všem současným prvkům, tak i těm, které
do stránky budou přidány - například AJAXem.
Volání funkce live pro událost submit je možné až od jQuery verze
1.4. Pro nižší verze použijte plugin Live Query.
Opět nalinkujeme do stránky v @layout.phtml. A nyní již hurá na přidání AJAXu!
Snippety
Tento tutoriál je psán pro Nette 0.9. V současné verzi Nette 2.0 už se zavináče nepíší, viz Historie ajaxu v Nette, jinak se ale snippety používají velmi podobně.
Nejjednodušším způsobem, jak překreslit část stránky v Nette, je uzavřít ji do
snippetu a ten překreslovat. V našem případě budeme mít snippety tři - formulář,
který budeme chtít po úspěšném odeslání vyprázdnit, seznam příspěvků a flash
zprávičky v @layout.phtml. Současná stabilní verze také vyžaduje použití
zavináčové magie,
takže musíme přidat zavináč před úvodní makro {block content}.
Šablonu default.phtml tedy upravíme takto:
Poslední snippet přijde do šablony @layout.phtml a bude obalovat vykreslování
flash zpráviček - i uživatelům s AJAXem je jistě budeme chtít zobrazit. Také nesmíme
zapomenout na zavináč před makro {include #content}, jinak by při AJAXových
požadavcích nedocházelo k vkládání (a tím pádem ani k vykonání) bloku a snippety by nefungovaly.
Změn v samotném presenteru nebude mnoho. Formuláři jen přiřadíme třídu AJAX a
mírně poupravíme zpracování formuláře:
protected function createComponentPostForm()
{
$form = new AppForm();
$form->getElementPrototype()->class('ajax');
// ...
}
public function postForm_onSubmit(Form $form)
{
$entry = $form->getValues();
$entry['posted'] = new DateTime();
$entry['ip'] = Environment::getHttpRequest()->remoteAddress;
$this->entries->insert($entry);
$this->flashMessage('Váš příspěvek byl uložen. Děkujeme za Váš čas.');
if (!$this->isAjax())
$this->redirect('this');
else {
$this->invalidateControl('list');
$this->invalidateControl('form');
$form->setValues(array(), TRUE);
}
}
Na konec jsme jen přidali podmínku - v případě AJAXového požadavku neprovádíme
přesměrování, ale zneplatníme dva snippety a voláním $form->setValues(array(), TRUE);
vyprázdníme formulář.
Jediný snippet, který jsme nezneplatnili, byl ten kolem flash zpráviček. Drobnou
funkcí umístěnou do třídy BasePresenter se však o jejich zneplatnění nemusíme
vůbec starat a vše může probíhat automaticky. Do třídy BasePresenter v souboru
app/presenters/BasePresenter.php tedy můžeme umístit následující funkci:
public function afterRender()
{
if ($this->isAjax() && $this->hasFlashSession())
$this->invalidateControl('flashes');
}
Ta zajistí, že v případě nastavených flash zpráviček se u AJAXového požadavku
snippet automaticky invaliduje a my se o to vůbec nemusíme starat.
A to je vše. Nyní už by se měl formulář odeslat AJAXem a seznam příspěvků
by se měl automaticky aktualizovat.
Stránkování
Máme již sice před sebou plně funkční knihu návštěv, která navíc používá
AJAX, něco tomu ale stále chybí - stránkování. Po čase by se naše kniha
návštěv značně zaplnila a znepřehlednila samými pozitivními komentáři,
takže je moudré je rozdělit do stránek.
K tomu si vypůjčíme již hotovou komponentu VisualPaginator.
Nette již sice obsahuje třídu Paginator, ta ale obsahuje jen základní
logiku potřebnou ke stránkování a neumí vykreslit žádný pro uživatele přívětivý
výstup. Komponenta VisualPaginator je jen jakousi obálkou, která se stará o
vykreslování zmíněné třídy.
Při používání komponent třetích stran věnujte prosím pozornost její licenci.
Některé licence Vám neumožňují použít danou komponentu, pokud nesplňujete
určité podmínky. Například komponenty s licencí GNU GPL
můžete s projektem distribuovat jen tehdy, kdy i samotný projekt bude distribuován
pod licencí GNU GPL. Toto omezení se však týká jen distribuce projektu - pokud
projekt nebudete nijak šířit, můžete komponentu použít bez problémů.
Toto je vhodné si uvědomit zejména u komerčních projektů, kdy se i dodání webu
zákazníkovi považuje za distribuci.
VisualPaginator je šířen pod licencí New BSD, která povoluje prakticky jakékoliv
použití za předpokladu, že budou v komponentě ponechány copyrighty a prohlášení
o zodpovědnosti za škodu.
Vytvoříme si složku app/components a do ní rozbalíme složku VisualPaginator
z distribučního archivu s komponentou. Stylopis example.css můžeme přesunout
do složky document_root/css a nalinkovat do stránky. Nyní máme vše připravené
a můžeme se pustit do samotné implementace stránkování.
Začneme od modelu. Náš současný model umožňuje jen získání celého
seznamu v databázi. Pokud budeme stránkovat, bude praktičtější,
když už samotný dotaz bude obsahovat klauzule LIMIT a OFFSET,
které nám rozsah výsledků patřičně omezí. Můžeme si tedy upravit
metodu fetchAll() třídy EntriesModel tak, aby toto omezení
zohledňovala. O něco praktičtější však bude použít třídu DibiDataSource,
která je pro tento účel přímo stvořená.
Do třídy BaseModel tedy přidáme novou metodu: getDataSource,
která vrátí novou instanci DibiDataSource:
/**
* Creates a new DataSource
* @return DibiDataSource
*/
public function getDataSource()
{
return new DibiDataSource($this->name, $this->connection);
}
Třídě DibiDataSource se jako první argument konstruktoru zadává zdroj,
ze kterého se mají data vybírat. To může být buď název tabulky, jako
v našem případě, nebo SQL dotaz. V případě použití SQL dotazu se
použije jako poddotaz.
Použití SQL dotazu se nedoporučuje v případě MySQL databáze.
Ta totiž neumí použít indexy v tabulkách z poddotazu, takže
je poté DibiDataSource silně neefektivní.
Nyní se přesuneme do třídy HomepagePresenter, která bude
hlavním dějištěm našeho stránkování. Nejdříve upravíme metodu
renderDefault() tak, aby prozatím používala novou metodu modelu,
ale zatím nestránkovala:
public function renderDefault()
{
$dataSource = $this->entries->getDataSource();
$dataSource->orderBy('posted', dibi::DESC);
$this->template->entries = $dataSource;
}
Voláním metody orderBy() nad objektem $dataSource nastavujeme totéž,
co jsme dřív předávali jako parametr metodě fetchAll - sestupné řazení
podle sloupce posted.
Použití DibiDataSource v šabloně bude stejné, jako by šlo již o hotový
výsledek. Při pokusu procházet přes jeho prvky se totiž automaticky vykoná
výsledný dotaz a pro procházení se použije jeho výsledek. To nám umožňuje
libovolně upravovat parametry DibiDataSource až do jeho použití při vykreslování.
Nyní se pustíme do samotného stránkování. Vytvoříme si továrničku na komponentu
VisualPaginator:
protected function createComponentPaginator()
{
$visualPaginator = new VisualPaginator();
$visualPaginator->paginator->itemsPerPage = 10;
return $visualPaginator;
}
Povšimněte si řádku $visualPaginator->paginator->itemsPerPage = 10;. Jak již
bylo dříve zmíněno, slouží třída VisualPaginator jako obálka nad třídou
Paginator. Právě ta řídí veškerou stránkovací logiku a parametry
stránkování musíme přiřazovat právě jí. Tu třída VisualPaginator
obsahuje v propertypaginator.
Zmíněný řádek tedy třídě Paginator říká, že si přejeme na stránce zobrazit
10 záznamů.
Dále musíme zohlednit stránkování při sestavování DibiDataSource.
Je potřeba předat třídě Paginator informace o celkovém počtu objektů
v databázi a objektu DibiDataSource naopak nastavit pomocí metody
applyLimit() limit a offset. Oba parametry získáme ze třídy Paginator.
Celá metoda renderDefault() bude nyní vypadat takto:
Nyní se už stránkování nejen zobrazí, ale také je plně funkční.
Ale pozor! Při změně stránky se nepoužívá AJAX. Po kliknutí na odkaz vůbec
nedojde k AJAXovému volání, navíc by zatím ani nedošlo k překreslení žádného
snippetu. Pojďme to tedy napravit...
Pro přidání AJAXového volání po kliknutí na odkaz v máme 2 možnosti:
přidáme odkazům v šabloně paginatoru třídu ajax
upravíme ajax.js tak, aby AJAXová volání přiřadil automaticky i odkazům stránkovače
Zvolíme druhé řešení, protože je méně pracné - stačí jen přidat selektor .paginator a
do ajax.js:
$("a.ajax, .paginator a").live("click", function (event) {
event.preventDefault();
$.get(this.href);
});
Překreslení snippetu je jednoduché, nicméně mírně neelegantní - komponenta
VisualPaginator neobsahuje žádný mechanismus, kterým by bylo možné zajistit
vyvolání vlastního kódu v případě změny stránky. V podstatě ani není možné takový
mechanismus elegantně zajistit - komponenta neví, jakou stránku měl uživatel,
od kterého AJAXový požadavek přišel, právě zobrazenou. Vše by se muselo řešit
přes dodatečný parametr.
Dovolíme si tedy použít jednodušší řešení - snippet se seznamem příspěvků nebudeme
zneplatňovat pouze v případě, že uživatel odeslal formulář, ale při každém požadavku.
Volání invalidate() se nám tedy přesune do metody renderDefault():
public function renderDefault()
{
// ...
$this->template->entries = $dataSource;
if ($this->isAjax())
$this->invalidateControl('list');
}
Také by bylo vhodné uživateli po odeslání formuláře zobrazit první stránku s jeho příspěvkem.
Přidáme tedy do metody postForm_onSubmit následující řádek:
Řádek obsahuje dvě přiřazení - bohužel je nutné současnou stránku zvlášť nastavit
komponentě VisualPaginator a zvlášť třídě Paginator, kterou komponenta obsahuje.
V tento okamžik by i stránkování mělo fungovat AJAXově a naše návštěvní
kniha je zase o kousek lepší.
Závěr
Sestavili jsme jednoduchou knihu návštěv v Nette, která používá AJAX.
Trochu paradoxně jsme naprostou většinu času strávili psaním základního
kódu, který s AJAXem nesouvisel, a změny při přidávání AJAXu nad celou
aplikaci byly minimální.
Nabízí se další rozšíření návštěvní knihy - ochrana proti spamu,
moderování příspěvků... Některá rozšíření mohou postupně přibýt
do tohoto tutoriálu, záleží na Vašem zájmu.
Následující tutoriál Vás provede tvorbou jednoduché a nenáročné návštěvní knihy a zasvětí Vás při tom do světa AJAXu v Nette s pomocí jQuery.
Požadavky
Příprava
Naší návštěvní knihu nebudeme psát úplně od základů, pomůžeme si kostrou aplikace, která je dostupná v distribučním balíku s Nette. Nachází se ve složce
tools/Skeleton
a obsahuje předpřipravenou adresářovou strukturu, několik základních tříd a dalších souborů, které nám usnadní práci.Příprava skeletonu je zde popsána jen stručně a pro úplnost, větší popis je obsahem jiných tutoriálů.
Začneme vytvořením složky v adresáři přístupném z testovacího webového serveru a rozbalíme do ní obsah skeletonu. Do složky
libs
nakopírujeme Nette a dibi. Dále do složkydocument_root/js
nakopírujeme jQuery. A abychom si tu obsluhu AJAXu v jQuery nemuseli psát sami, využijeme již připravených skriptů: jquery.nette.js a jquery.ajaxform.js - s nimi do stejné složky, jako s jQuery.Také bude dobré si ihned JavaScriptové knihovny do stránky nalinkovat, ať na to později nezapomeneme. Do hlavičky v souboru
app/templates/@layout.phtml
přidáme:Aby naše návštěvní kniha vypadala alespoň trošku k světu, stáhneme si mírně upravený soubor screen.css a umístíme jej do složky
document_root/css
.Protože budeme psát návštěvní knihu, bude také moudré si připravit nějakou tu databázi. Použijeme SQLite, které je dostupné téměř vždy a všude. Databáze to bude opravdu jednoduchá - vystačíme si s jedinou tabulkou
entries
:Celou databázi si můžete stáhnout: database.sdb. Umístěte do složky
app/models
.Poskytnutá databáze je ve formátu SQLite 2. Můžete si stáhnout i databázi ve formátu SQLite 3. Poté ale nesmíte zapomenout použít v dibi driver
sqlite3
. Pokud preferujete MySQL, je k dispozici i export pro MySQL.Nezapomeňte, že pokud chcete, aby kniha návštěv fungovala, musí mít webserver oprávnění zapisovat nejen do souboru s databází, ale i do složky, ve které je tato databáze umístěna - v našem případě složka
app/models
. Pokud pracujete na systému, který vychází z unixu, zvažte použití příkazuchmod -R a+rwX app/models
.Začínáme
Nyní se již od kopírování a rozbalování knihoven můžeme pustit do samotné tvorby. Abychom demonstrovali jednoduchost a sílu AJAXu v Nette, vytvoříme nejdříve aplikaci bez jeho použití a až poté přidáme AJAX - se zachováním stejné funkčnosti.
Začneme tedy vytvořením jednoduchého modelu a připojením k databázi.
Modely a databáze
Ačkoliv v tomto tutoriálu budeme pracovat jen s jedinou tabulkou a tím pádem si vystačíme s jediným modelem (a tedy jedinou třídou), vytvoříme si modely dva: abstraktní
BaseModel
, který poslouží jako šablona pro další modely (co když bude potřeba zítra do aplikace přidat další funkce?), aEntriesModel
, který bude reprezentovat samotnou tabulkuentries
v databázi.Modely
Jak již bylo řečeno,
BaseModel
poslouží jako kostra pro další modely. Bude obsahovat nejen několik základních funkcí pro práci s danou tabulkou, ale také se nám postará o připojování k databáze a odpojování od ní. Vytvoříme si následující souborapp/models/BaseModel.php
:Celá třída obsahuje jen ty funkce, které budeme pro náš příklad potřebovat. Jistě by se našlo místo na další funkce (aktualizace záznamů, složitější vybírání z databáze, ...), ne však v tomto tutoriálu.
Třída se může na první pohled zdát složitá, po bližším prozkoumání však o nic složitého nejde. Navíc oč složitější je tato třída, o to jednodušší budou další modely.
app/models/EntriesModel.php
bude vypadat takto:Obsahuje jen definici názvu tabulky. Nic dalšího potřeba skutečně není.
Připojení k databázi
Tento tutoriál je psán pro Nette 0.9. V současné verzi Nette 2.0 se konfigurace provádí trochu jinak, viz Konfigurace Nette Frameworku.
Metoda
BaseModel::connect()
nám sice umožňuje připojit se k databázi, musíme jí ale někde zavolat a také musíme doconfig.ini
zapsat údaje pro připojení.Začneme tedy těmi údaji. Do souboru
app/config.ini
přidáme před začátek sekce[production < common]
následující řádky:Pokud chcete použít databázi ve formátu SQLite 3, použijte driver
sqlite3
. Formáty nejsou zaměnitelné, takže nelze načíst SQLite 2 databázi pomocí driverusqlite3
a naopak. Pokud zatím s dibi moc nekamarádíte, můžete se také podívat na příklad konfigurace pro MySQL.To nám vytvoří podsekci konfigurace s názvem
database
platnou pro všechna prostředí.Když už máte otevřený soubor
config.ini
, povšimněte si bezpečnostního varování.Nyní už zbývá se k databázi připojit. Toho dosáhneme pomocí událostí aplikace. Do souboru
app/bootstrap.php
přidáme před volání$application->run();
následující řádky kódu:Tím aplikaci nadefinujeme, že se má během svého spouštění připojit k databázi voláním metody
BaseModel::connect()
a při ukončování se zase slušně odpojit volánímBaseModel::disconnect()
.A to je k databázi vše. Pokud nyní při otevření aplikace v prohlížeči nespatříte chybové hlášení, aplikace je připravena pracovat s databází.
Výpis příspěvků a jejich přidávání
Návštěvní kniha většinou obsahuje jen jednu jedinou stránku - pro výpis a současně i přidávání příspěvků. To nám situaci zjednodušuje a můžeme pracovat na poli jediného presenteru -
HomepagePresenter
u. V našem případě by měl obsahovat několik základních částí:Začneme seznamem...
Seznam příspěvků
Abychom se dostali k seznamu příspěvků, musíme použít model. Máme několik možností, jak model vytvářet:
startup
presetneru a tu používatPoslední způsob je asi nejelegantnější, použijeme proto ten. Jmenuje se lazy loading. Vytvoříme si jednoduchou funkci (getter), která bude kontrolovat, zda je daná členská proměnná
NULL
. Pokud ano, tak vytvoří novou instanci modelu. Na konci tuto proměnnou vrátí. TřídaHomepagePresenter
v souboruapp/presenters/HomepagePresenter.php
bude vypadat takto:V presenteru pak budeme používat členskou proměnnou
$entries
- díky taťkovi všech objektů se bude volat náš gettergetEntries()
.Zde to může působit trochu jako kanón na mouchy, ale je dobré si na podobné konstrukce zvyknout - líné vytváření objektů je velmi výhodné u větších aplikací. Pokud budeme mít modelů více a budeme s nimi muset pracovat z více tříd, pak budeme muset najít nějaké lepší a pohodlnější řešení. Pro jeden model však zůstaneme u této relativně jednoduché metody.
Budeme pokračovat metodou
renderDefault()
, která data z databáze načte a připraví je šabloně:Pokud si nejste jistí, proč zrovna
renderDefault()
, konzultujte dokumentaci či jiný tutoriál.A nakonec samotná šablona. Soubor
app/templates/Homepage/default.phtml
upravíme takto:Pokud jsme nikde neudělali chybu, bude výstup nyní následující:
Seznam je prázdný, ale aby ho mohl někdo naplnit, musí mít jak.
Formulář pro přidávání příspěvků
Abychom mohli nějaký formulář v šabloně vykreslit, musíme jej nejdříve nadefinovat. To uděláme opět ve třídě
HomepagePresenter
, vytvoříme si na formulář továrničku. Ta nám formulář vytvoří až v momentě, kdy je ho skutečně potřeba. U presenterů s mnoha komponentami a mnoha pohledy by bylo nákladné vytvářet vždy všechny komponenty a pracné je ručně vytvářet před prvním použitím, proto nám práci usnadní zmíněná továrnička:Tato továrnička bude vytvářet formulář s názvem
postForm
. Tento název pro nás bude důležitý hlavně při vykreslování formuláře v šabloně. Formulář má 2 políčka, jedno na jméno a druhé na text. Obě jsou povinná, nechceme přeci anonymní příspěvky bez textu. Pod políčky je tlačítko na odeslání, o odeslání se bude starat metodapostForm_onSubmit()
v aktuálním třídě (tedy ve tříděHomepagePresenter
). Aby formulář po odeslání příspěvek skutečně přidal, musíme si onu metodu nadefinovat:Vykreslení samotného formuláře na stránce provedeme v šabloně pomocí makra
{control}
, kterému jako parametr udáme název komponenty, v našem případěpostForm
. Toto makro si od presenteru vyžádá danou komponentu a ten, pokud již komponenta neexistuje, zavolá naší továrničku a komponentu vytvoří.V názvu komponenty je rozlišována velikost písmen a při použití továrniček začíná název komponenty vždy malým písmenem. Pokud dostáváte od aplikace chybu o neexistující komponentě, zkontrolujte právě velikost písmen.
V některých příkladech můžete narazit i na použití makra
{widget}
. Pokud Vás zajímá, jaký je mezi těmito dvěma makry rozdíl, tak vězte, že žádný.{control}
je jen z historického hlediska aliasem pro{widget}
.Nyní už by měla kniha fungovat a hosté mohou psát:
Teď už si jistě říkáte: kde je ten slibovaný AJAX? Nebude teď dost práce tam přidat AJAXová volání, AJAXové zpracování... ? Nebude.
AJAX
Ještě než se pustíme do přidělání AJAXu do samotné aplikace, musíme se postarat o správné přepsání událostí v JavaScriptu, aby vůbec došlo k jeho volání. Za tímto účelem si vytvoříme malý script ve složce
document_root/js
. Nazveme jej třebaajax.js
. Jeho obsah bude zhruba následující:První část scriptu přidá všem odkazům s třídou
ajax
událost, která po kliknutí na ně vykoná AJAXový požadavek a zruší přechod na další stránku. Druhá část, která se týká formulářů, má obdobný efekt: po odeslání formuláře se data odešlou pomocí AJAXu a odeslání normální cestou se přeruší. Použití funkcelive
zajišťuje, že se událost přidá jak všem současným prvkům, tak i těm, které do stránky budou přidány - například AJAXem.Volání funkce
live
pro událostsubmit
je možné až od jQuery verze 1.4. Pro nižší verze použijte plugin Live Query.Opět nalinkujeme do stránky v
@layout.phtml
. A nyní již hurá na přidání AJAXu!Snippety
Tento tutoriál je psán pro Nette 0.9. V současné verzi Nette 2.0 už se zavináče nepíší, viz Historie ajaxu v Nette, jinak se ale snippety používají velmi podobně.
Nejjednodušším způsobem, jak překreslit část stránky v Nette, je uzavřít ji do snippetu a ten překreslovat. V našem případě budeme mít snippety tři - formulář, který budeme chtít po úspěšném odeslání vyprázdnit, seznam příspěvků a flash zprávičky v
@layout.phtml
. Současná stabilní verze také vyžaduje použití zavináčové magie, takže musíme přidat zavináč před úvodní makro{block content}
. Šablonudefault.phtml
tedy upravíme takto:Poslední snippet přijde do šablony
@layout.phtml
a bude obalovat vykreslování flash zpráviček - i uživatelům s AJAXem je jistě budeme chtít zobrazit. Také nesmíme zapomenout na zavináč před makro{include #content}
, jinak by při AJAXových požadavcích nedocházelo k vkládání (a tím pádem ani k vykonání) bloku a snippety by nefungovaly.Změny v presenteru
Změn v samotném presenteru nebude mnoho. Formuláři jen přiřadíme třídu AJAX a mírně poupravíme zpracování formuláře:
Na konec jsme jen přidali podmínku - v případě AJAXového požadavku neprovádíme přesměrování, ale zneplatníme dva snippety a voláním
$form->setValues(array(), TRUE);
vyprázdníme formulář.Jediný snippet, který jsme nezneplatnili, byl ten kolem flash zpráviček. Drobnou funkcí umístěnou do třídy
BasePresenter
se však o jejich zneplatnění nemusíme vůbec starat a vše může probíhat automaticky. Do třídyBasePresenter
v souboruapp/presenters/BasePresenter.php
tedy můžeme umístit následující funkci:Ta zajistí, že v případě nastavených flash zpráviček se u AJAXového požadavku snippet automaticky invaliduje a my se o to vůbec nemusíme starat.
A to je vše. Nyní už by se měl formulář odeslat AJAXem a seznam příspěvků by se měl automaticky aktualizovat.
Stránkování
Máme již sice před sebou plně funkční knihu návštěv, která navíc používá AJAX, něco tomu ale stále chybí - stránkování. Po čase by se naše kniha návštěv značně zaplnila a znepřehlednila samými pozitivními komentáři, takže je moudré je rozdělit do stránek.
K tomu si vypůjčíme již hotovou komponentu VisualPaginator. Nette již sice obsahuje třídu Paginator, ta ale obsahuje jen základní logiku potřebnou ke stránkování a neumí vykreslit žádný pro uživatele přívětivý výstup. Komponenta VisualPaginator je jen jakousi obálkou, která se stará o vykreslování zmíněné třídy.
Při používání komponent třetích stran věnujte prosím pozornost její licenci. Některé licence Vám neumožňují použít danou komponentu, pokud nesplňujete určité podmínky. Například komponenty s licencí GNU GPL můžete s projektem distribuovat jen tehdy, kdy i samotný projekt bude distribuován pod licencí GNU GPL. Toto omezení se však týká jen distribuce projektu - pokud projekt nebudete nijak šířit, můžete komponentu použít bez problémů. Toto je vhodné si uvědomit zejména u komerčních projektů, kdy se i dodání webu zákazníkovi považuje za distribuci. VisualPaginator je šířen pod licencí New BSD, která povoluje prakticky jakékoliv použití za předpokladu, že budou v komponentě ponechány copyrighty a prohlášení o zodpovědnosti za škodu.
Vytvoříme si složku
app/components
a do ní rozbalíme složkuVisualPaginator
z distribučního archivu s komponentou. Stylopisexample.css
můžeme přesunout do složkydocument_root/css
a nalinkovat do stránky. Nyní máme vše připravené a můžeme se pustit do samotné implementace stránkování.Začneme od modelu. Náš současný model umožňuje jen získání celého seznamu v databázi. Pokud budeme stránkovat, bude praktičtější, když už samotný dotaz bude obsahovat klauzule
LIMIT
aOFFSET
, které nám rozsah výsledků patřičně omezí. Můžeme si tedy upravit metodufetchAll()
třídyEntriesModel
tak, aby toto omezení zohledňovala. O něco praktičtější však bude použít tříduDibiDataSource
, která je pro tento účel přímo stvořená.Do třídy
BaseModel
tedy přidáme novou metodu:getDataSource
, která vrátí novou instanciDibiDataSource
:Třídě
DibiDataSource
se jako první argument konstruktoru zadává zdroj, ze kterého se mají data vybírat. To může být buď název tabulky, jako v našem případě, nebo SQL dotaz. V případě použití SQL dotazu se použije jako poddotaz.Použití SQL dotazu se nedoporučuje v případě MySQL databáze. Ta totiž neumí použít indexy v tabulkách z poddotazu, takže je poté
DibiDataSource
silně neefektivní.Nyní se přesuneme do třídy
HomepagePresenter
, která bude hlavním dějištěm našeho stránkování. Nejdříve upravíme metodurenderDefault()
tak, aby prozatím používala novou metodu modelu, ale zatím nestránkovala:Voláním metody
orderBy()
nad objektem$dataSource
nastavujeme totéž, co jsme dřív předávali jako parametr metoděfetchAll
- sestupné řazení podle sloupceposted
.Použití
DibiDataSource
v šabloně bude stejné, jako by šlo již o hotový výsledek. Při pokusu procházet přes jeho prvky se totiž automaticky vykoná výsledný dotaz a pro procházení se použije jeho výsledek. To nám umožňuje libovolně upravovat parametryDibiDataSource
až do jeho použití při vykreslování.Nyní se pustíme do samotného stránkování. Vytvoříme si továrničku na komponentu VisualPaginator:
Povšimněte si řádku
$visualPaginator->paginator->itemsPerPage = 10;
. Jak již bylo dříve zmíněno, slouží třídaVisualPaginator
jako obálka nad třídouPaginator
. Právě ta řídí veškerou stránkovací logiku a parametry stránkování musíme přiřazovat právě jí. Tu třídaVisualPaginator
obsahuje v propertypaginator
. Zmíněný řádek tedy tříděPaginator
říká, že si přejeme na stránce zobrazit 10 záznamů.Dále musíme zohlednit stránkování při sestavování
DibiDataSource
. Je potřeba předat tříděPaginator
informace o celkovém počtu objektů v databázi a objektuDibiDataSource
naopak nastavit pomocí metodyapplyLimit()
limit a offset. Oba parametry získáme ze třídyPaginator
. Celá metodarenderDefault()
bude nyní vypadat takto:Nyní už nám zbývá jen naší novou komponentu ve stránce vykreslit. Opět použijeme makro
{control}
a umístíme jí do snippetulist
:Nyní se už stránkování nejen zobrazí, ale také je plně funkční.
Ale pozor! Při změně stránky se nepoužívá AJAX. Po kliknutí na odkaz vůbec nedojde k AJAXovému volání, navíc by zatím ani nedošlo k překreslení žádného snippetu. Pojďme to tedy napravit...
Pro přidání AJAXového volání po kliknutí na odkaz v máme 2 možnosti:
ajax
ajax.js
tak, aby AJAXová volání přiřadil automaticky i odkazům stránkovačeZvolíme druhé řešení, protože je méně pracné - stačí jen přidat selektor
.paginator a
doajax.js
:Překreslení snippetu je jednoduché, nicméně mírně neelegantní - komponenta VisualPaginator neobsahuje žádný mechanismus, kterým by bylo možné zajistit vyvolání vlastního kódu v případě změny stránky. V podstatě ani není možné takový mechanismus elegantně zajistit - komponenta neví, jakou stránku měl uživatel, od kterého AJAXový požadavek přišel, právě zobrazenou. Vše by se muselo řešit přes dodatečný parametr.
Dovolíme si tedy použít jednodušší řešení - snippet se seznamem příspěvků nebudeme zneplatňovat pouze v případě, že uživatel odeslal formulář, ale při každém požadavku.
Volání
invalidate()
se nám tedy přesune do metodyrenderDefault()
:Také by bylo vhodné uživateli po odeslání formuláře zobrazit první stránku s jeho příspěvkem. Přidáme tedy do metody
postForm_onSubmit
následující řádek:Řádek obsahuje dvě přiřazení - bohužel je nutné současnou stránku zvlášť nastavit komponentě VisualPaginator a zvlášť třídě
Paginator
, kterou komponenta obsahuje.V tento okamžik by i stránkování mělo fungovat AJAXově a naše návštěvní kniha je zase o kousek lepší.
Závěr
Sestavili jsme jednoduchou knihu návštěv v Nette, která používá AJAX. Trochu paradoxně jsme naprostou většinu času strávili psaním základního kódu, který s AJAXem nesouvisel, a změny při přidávání AJAXu nad celou aplikaci byly minimální.
Nabízí se další rozšíření návštěvní knihy - ochrana proti spamu, moderování příspěvků... Některá rozšíření mohou postupně přibýt do tohoto tutoriálu, záleží na Vašem zájmu.
Celou aplikaci si můžete vyzkoušet i stáhnout.