Návod jak vytvořit blog
Blog, to je dnes už webová klasika, „hello world“ všech webových řešení. Pojďme se v rychlosti podívat na to, jak takový jednoduchý blog vytvořit v Nette.
Požadavky
- Pochopení základních principů Nette (vztahy v MVP vzoru, základní představa o funkci presenteru a jeho vztahu k šablonám)
- Povrchní znalost dibi
- Prostředí pro běh Nette
Úvod
Tvorba jednoduchého blogu je evergreenem mezi tématy tutoriálů různých
webových frameworků. Bohužel, většina tutoriálů je okleštěna na
minimální kostru úkolu a jako hlavní cíl si kladou ukázat uživateli, jak
jednoduše to s daným frameworkem jde. Tím se náročnost úlohy snižuje na
úroveň, na které téměř nestojí za to nějaký framework používat.
Neberte tedy tento tutoriál jako lákaldo na Nette, ale jako výukový
materiál.
V tutoriálu je použita namespacová verze Nette. Přestože aplikace je tak
malá, že namespaces jsou tu spíš na obtíž, je to něco, co by každý
potencionální uživatel Nette měl znát a umět používat. Pokud
z technických důvodů nemůžete použít verzi pro PHP =>5.3, použijte
bezprefixovou verzi Nette pro PHP 5.2 a vypusťte z uvedených zdrojových
kódu definice namespaces.
Blog za 19 minut!
Pomocí RoR nebo CodeIgniteru lze vytvořit „kompletní“ „blog“ za „20“ minut. Vážení, to nic není! Předvedu Vám, že s Nette to za 19 minut dokážete i Vy!
Instalace
Použijeme skeleton z ditribuce. Nakopírujte ho do požadované složky na
serveru. Do složky /libs nakopírujeme dibi (v distribuci Nette se nachází ve složce
3rdParty). Budeme také potřebovat databázi, já se budu držet MySQL, ale
pokud chcete použít SQLite nebo PostgreSQL, není to problém, stačí
číst dál.
Nezapomeňte, že ve skeletonu chybí Nette ve složce
/libs!
Databáze a model
Začněmě s tvorbou příslušných tabulek. Jejich struktura je jasná ze zadání. Spustíme tedy na naši databázi tyto příkazy:
CREATE TABLE `posts` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(128) COLLATE utf8_bin NOT NULL,
`body` text COLLATE utf8_bin NOT NULL,
`date` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
Tím vytvoříme tabulku s články.
CREATE TABLE `comments` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`post_id` INT NOT NULL ,
`author` VARCHAR( 128 ) NOT NULL ,
`body` TEXT NOT NULL ,
`date` DATETIME NOT NULL,
INDEX (post_id),
FOREIGN KEY (post_id) REFERENCES posts(id)
) ENGINE = INNODB CHARACTER SET utf8 COLLATE utf8_bin;
A tímto (překvapivě) tabulku komentářů.
Pokud používáte jinou databázi, vytvořte stejnou strukturu tabulek, dibi se postará o kompatibilitu na straně aplikace samo.
Když máme databázi, je potřeba se k ní skrz dibi připojit.
Konfigurační soubory v Nette nabízejí elegantní způsob správy dat jako
jsou údaje o databázi. Otevřete si config.ini ve složce
/app a do části [common] přidejte příslušnou
modifikaci těchto řádků:
db.server = localhost
db.database = blogtut
db.username = blogtut
db.password = blogtut
db.driver = mysqli
db.charset = utf8
db.lazy = TRUE
A nyní připojení samotné. Do souboru bootstrap.php
(zaváděcí soubor celé aplikace, jak už byste měli vědět) přidejte
před řádek
$application->run(); // Tento řádek spustí naši aplikaci...
toto:
dibi::connect(Environment::getConfig('db')); // ...a připojit se potřebujeme před spuštěním aplikace
Zde se na chvíli zastavíme a dáme si minutku teorie, ve které prohloubíme znalosti a odpovíme na možné otázky. Několik zásadních bodů:
- Nette načítá soubory s kódem (ať už knihovny nebo části aplikace samotné) samo a dle potřeby. Proto není žádný include či require ani tady, ani nikde jinde v našem kódu.
- Voláním
Environment::getConfig('db')jsme z konfiguračního souboru získali pole určující nastavení databáze. Náš zápis je jiná reprezentace asociativního pole
$db = array(
'server' => 'localhost',
'database' => 'blogtut',
'username' => 'blogtut',
'password' => 'blogtut',
'driver' => 'mysql',
'charset' => 'utf8',
'lazy' => TRUE
);
- I když spojení s databází nastavujeme pokaždé, když se aplikace
spustí, skutečné spojení probíhá jen tehdy, kdy je to skutečně potřeba,
a to díky nastavení
db.lazy = TRUEvconfig.ini.
Musíme ještě vytvořit modely pro obě tabulky.
<?php
class PostsModel
{
public static function fetchAll()
{
return dibi::fetchAll('
SELECT *
FROM [posts]
ORDER BY [date]', dibi::DESC
);
}
}
<?php
class CommentsModel
{
public static function fetchAll($post_id)
{
return dibi::fetchAll('
SELECT *
FROM [comments]
WHERE [post_id] = %i', $post_id
);
}
}
Pro srozumitelnost budou prozatím jejich metody bez zbytečných ohledů
tahat všechna dostupná data. Oba soubory s definicí tříd uložte do
složky /app/models a pojmenujte je podle třídy, kterou obsahují
(PostsModel.php, CommentsModel.php).
Pokud jste zvyklí ukončovat skripty značkou
?>, tak si rychle odvykněte! Ukončovací tagy jsou
nepovinné a způsobují pouze problémy s netisknutelnými znaky, což poté
způsobí nemožnost odeslání HTTP hlaviček.
Presenter
V /app/presenters/ je HomepagePresenter. Ten poslouží jako
dobrý základ našeho snažení. Přidáme do něj metodu, která vezme data
z modelu a předá je do template k vykreslení.
Metodu renderDefault upravíme na
public function renderDefault()
{
$this->template->posts = PostsModel::fetchAll();
}
Pokud si nejste jistí, proč pracuji s metodou default v presenteru Homepage, podívejte se na routy v souboru bootstrap.php.
View
Výborně, teď máme v view default dostupnou proměnnou
$posts, která obsahuje všechny příspěvky. Pojďme je
vypsat.
Ve složce /app/templates je soubor @layout.phtml. Ten
obsahuje základní rámec všech stránek, které budeme tvořit. Proto
doporučuji si ho prohlédnout.
V /app/templates/Homepage je soubor default.phtml,
který obsahuje definici bloku content, jehož obsah nahradí
{include #content} v layoutu. Výpis všech článků může
vypadat třeba takhle:
{block content}
<h1>Můj blogísek</h1>
<div id="posts">
{if count($posts)}
{foreach $posts as $post}
<div class="post">
<h3>{$post['title']}</h3>
<small>Přidáno {$post['date']|date}</small>
<p>{$post['body']}</p>
</div>
{/foreach}
{else}
Zatím nebyl napsán žádný článek.
{/if}
</div>
Stáhněte si testovací data, nahrajte je do databáze a zkuste otevřít root webu ve vašem prohlížeči. Výsledek by měl vypadat takto:

Dovolím si zkazit radost povinnou trochou teorie:
- Použité příkazy ve složených závorkách se nazývají makra Latte filtru a víc se o nich dozvíte v dokumentaci.
- Všiměte si části
{$post['date']|date}. Ono date za vertical barem (svislítkem, chcete-li) je helper. Helper je jednoduchá funkce, která provádí s dannou proměnnou nějakou operaci podstatnou pouze pro zobrazení.
Komentáře
To ani nebolelo a zabralo to jen pár minut, ale blog je o komunikaci s lidmi. Proto potřebujeme přidat možnost komentovat příspěvky. Klasiciký přístup je takový, že na titulní straně se zobrazuje jen začátek textu s odkazem na celý text, kde je i možnost komentovat. Pojďme tedy na to. Nová metoda presenteru Protože zobrazení samostatného příspěvku nijak nesouvisí s titulní stranou, přidáme do našeho preseneru novou metodu:
public function renderSingle($id = 0)
{
$this->template->post = PostsModel::fetchSingle($id);
}
A vytvoříme příslušnou metodu v PostsModel:
public static function fetchSingle($id)
{
return dibi::fetch('
SELECT *
FROM [posts]
WHERE [id] = %i', $id
);
}
Měli byste znát rozdíl mezi dibi::fetchAll a
dibi::fetch. První vrací kolekci DibiRow, která implementuje
ArrayAccess, druhá
vrací přímo DibiRow.
Samozřejmě není optimální pro každý typ požadavku psát samostatnou funkci v modelu, máme na to různé fígly, ale prozatím KISS.
Také musíme vytvořit template pro tento požadavek, takže do
/app/templates/Homepage/single.phtml vložíme:
{block content}
<div class="post">
<h1>{$post['title']}</h1>
<small>Přidáno {$post['date']|date}</small>
<p>{$post['body']}</p>
</div>
Nyní můžete v prohlížeči zkusit otevřit třeba
/Homepage/single/2.

Opět se vracíme k routám. Podívejte se ještě jednou do
bootstrap.php.
Odkazy
Aby se sem dostal i běžný uživatel, potřebujeme nějaké odkazy z hlavní stránky. K tomu slouží makro {llink …}. Předělejme tedy view titulní stránky:
{block content}
<h1>Můj blogísek</h1>
<div id="posts">
{if count($posts)}
{foreach $posts as $post}
<div class="post">
<h3>{$post['title']}</h3>
<small>Přidáno {$post['date']|date}</small>
<p>{$post['body']|truncate:300}</p>
<a href="{link single $post['id']}">Více…</a>
</div>
{/foreach}
{else}
Zatím nebyl napsán žádný článek.
{/if}
</div>

V naší pravidelné minutovce teorie bych nyní rád vyzdvihl dvě věci:
- Všimněte si helperu
truncate:300a jeho efektu. - Zápis
{plink single $post['id']}znamená: vytvoř odkaz na akci single aktuálního presenteru a přidej parametr$post['id']. Je ekvivalentní se zápisem{plink Homepage:single $post['id']}. Je důležité, že nezapisujeme žádné URL, ale odkaz na akci presenteru. - URL je zpětně vytvořeno tak, aby odpovídalo routám v
bootstrap.phpa naše aplikace je tím pádem na jeho tvaru naprosto nezávislá.
Formulář
Konečně se dostáváme k něčemu „záživnějšímu“ – pojďme si
vytvořit formulář na odesílání komentářů! Nette má několik způsobů
jak řešit formuláře, od tvrdého nakódování do templatu a odděleného
zpracování vstupů po sofistikované metody jako AppForm. Třída
AppForm nabízí výhody, o kterým se mnohým ani nesnilo. Náš
formulář bude samostatnou komponentou. Pokud jde o tvorbu komponent,
používá „továrničky“,
které vyrobí komponentu až v momentě, kdy je to skutečně potřeba. Do
HomepagePresenter přidáme klauzuli use a dvě
funkce:
use Nette\Application\AppForm;
public function createComponentCommentForm($name)
{
$form = new AppForm($this, $name);
$form->addText('author', 'Jméno')
->addRule(AppForm::FILLED, 'To se neumíš ani podepsat?!');
$form->addTextArea('body', 'Komentář')
->addRule(AppForm::FILLED, 'Komentář je povinný!');
$form->addSubmit('send', 'Odeslat');
$form->onSubmit[] = callback($this, 'commentFormSubmitted');
return $form;
}
public function commentFormSubmitted(AppForm $form)
{
$data = $form->getValues();
$data['date'] = new DateTime();
$data['post_id'] = (int) $this->getParam('id');
$id = CommentsModel::insert($data);
$this->flashMessage('Komentář uložen!');
$this->redirect("this#comment-$id");
}
První z nich zpracovává odeslaný formulář (všimněte si přesměrování, které zajistí, aby uživatel neodeslal formulář vícekrát kliknutím na tlačítko Obnovit), druhá je zmíněná továrnička.
Za pozornost stojí volání ‚$this->flashMessage('Komentář uložen!‘)‚. Nette obsahuje tzv. flash zprávičky, což jsou krátké zprávy které uživatele informují o aktuálním stavu aplikace. Defaultně jsou vypisovány v '@layout.phtml‘.
Do CommentsModel musíme přidat použitou metodu:
public static function insert($data)
{
dibi::query('
INSERT INTO [comments]', $data
);
return dibi::getInsertId();
}
A také nesmíme zapomenout předat všechny komentáře k příslušnému
příspěvku do šablony, takže metodu renderSingle upravíme:
public function renderSingle($id = 0)
{
if (!($post = PostsModel::fetchSingle($id))) {
$this->redirect('default'); //pokud clanek neexistuje, presemerujeme uzivatele
}
$this->template->post = $post;
$this->template->comments = CommentsModel::fetchAll($id);
}
Poslední věc, která zbývá, je úprava naší šablony:
{block content}
<a href="{link default}"><< home </a>
<div class="post">
<h1>{$post['title']}</h1>
<small>Přidáno {$post['date']|date}</small>
<p>{$post['body']}</p>
</div>
<h3>Komentáře:</h3>
<div id="comments">
{if count($comments)}
<div id="comment-{$comment->id}" class="commment" n:foreach="$comments as $comment">
<p>{$comment['body']}</p>
<small>{$comment['author']}, {$comment['date']|date}</small>
<hr>
</div>
{else}
Ke článku zatím nebyly napsány žádné komentáře. Buďte první!
{/if}
</div>
{control commentForm}
Ale pozor! Tady jaksi chybí {foreach ...}, že? Místo něho
jsou použity tzv. „n-atributy“,
které v některých případech nabízejí alternativní syntaxy zápisu
Latté maker.
Všimněte si, že formulář si sám najde cestu do šablony a vykreslí se. Zkuste odeslat formulář nevyplněný. Jak vidíte, Nette vygenerovalo validační Javascript k našemu formuláři. Ale validace probíhá i na straně serveru, takže vypnutý Javascript její funkčnost neovlivní.
Ve starších příkladech se místo makra control
můžete setkat s jeho starším aliasem widget.

Na výslednou aplikaci se můžete podívat zde, zdrojový kód je ke stažení tady.
Doufám, že jste se něco přiučili. Pokud už se v Nette trochu vyznáte a chcete další náměty, zkuste:
- Vypsat počet příspěvků/komentářů na hlavní stránku
- Přidat pár statických stránek a odkazy na ně
- Vytvořit jednoduchou administraci
- Zprovoznit stránkování příspěvků pomocí VisualPaginator
- K administraci přidejte uživatelské účty
- Přidat různé typy účtů, kontrolu oprávnění pomocí ACL
- Upravit routování a generovat „cool URL“
Sám se těmto úlohám budu věnovat v budoucích tutoriálech.
Stáhněte si zdrojový kód: http://github.com/…pball/master
Attached files
- posts.sql 2 kB
Comments 
Jonnyb | 9. 9. 2010, 16:42 | comment
Skvělý tutoriálek, díky
Ja | 13. 9. 2010, 19:23 | comment
Jak psal Lei, ocenuju jednoduchost, kdy se zacinajici dostane k jadru problemu a nemusi se prokousavat zbytecnostmi. Prosim o dalsi podobny tutorial, protoze presne neco takoveho tu dosud chybelo!
Diky!
Royce | 16. 9. 2010, 9:19 | bug
u vkládání komentáře by měl být namísto CommentsModel::insert($data); tohle: $id = CommentsModel::insert($data);
jelikož na to $id se pak redirectuje
OndraS | 16. 9. 2010, 12:18 | comment
Postupoval jsem přesně podle návodu a když byla první možnost vyzkoušet zobrazení dat z databáze, tak mi to píše tuhle chybu: Class ‚Presenter‘ not found
Source file ▼
File: C:\xampp\htdocs\blog_nette\app\presenters\BasePresenter.php Line: 19
Line 12: /** Line 13: * Base class for all application presenters. Line 14: * Line 15: * @author John Doe Line 16: * @package MyApplication Line 17: */ Line 18: abstract class BasePresenter extends Presenter Line 19: { Line 20: public $oldLayoutMode = FALSE; Line 21: Line 22: } Line 23:
Nevíte prosím někdo co s tím? Jsem úplný začátečník. Používám NetteFramework-0.9.5-PHP5.2.
OndraS | 16. 9. 2010, 13:07 | comment
předchozí je vyřešeno..
Podbor | 29. 9. 2010, 11:54 | comment
Pěkný tutoriál, který se mi bohužel nepodařilo dotáhnout do konce. Resp. mám asi někde problém s povolením směrování či s Routy. Při pokusu o přejití na celý příspěvek (tj. /Homepage/single/2.) ať už ručně, nebo odkazem se mi vždycky vrátí chyba: „stránka nenalezena“. Postupoval jsem přesně dle Tutoriálu a používám verzi pro PHP 5.3. Dovedl by někdo poradit ? Díky
PJK | 29. 9. 2010, 22:36 | comment
Podbor: Ujisti se, že v bootstrapu máš
$router = $application->getRouter();
$router[] = new Route('index.php', array(
'presenter' => 'Homepage',
'action' => 'default',
), Route::ONE_WAY);
$router[] = new Route('<presenter>/<action>/<id>', array(
'presenter' => 'Homepage',
'action' => 'default',
'id' => NULL,
));
a zkus nové Nette. Celý tutoriál jsem si (znovu) procházel a vše mi funguje. Zkus to prosím ještě jednou a kdyžtak se zeptej na fóru.
Podbor | 30. 9. 2010, 16:13 | comment
Díky za pomoc. Nakonec mi pomohlo fórum, přesněji diskuse na této adrese: http://forum.nette.org/…ty-error-404 (Píšu to zde proto, kdyby měl někdy někdo podobný problém). Chyba byla ve špatně nastaveném Apachi. Jinak musím říct, že je to velmi povedený a srozumitelně vytvořený tutoriál. Díky za něj!
Berry | 3. 1. 2011, 16:15 | comment
Zdravím, jsem úplnej začátečník s nette f. Už u quick start mi localhost hlásil chybu tak jsem zkusil jinej tutoriál a opět někde chyba i když jedu přesně podle návodu. Mám wampserver a když jsem se chtěl podívatna document_root tak to nahlásilo „fatal error Class ‚Environment‘ not found“
File: C:\wamp\www\blog\app\bootstrap.php Line: 53 Line 53: dibi::connect(Environment::getConfig(‚db‘));
Vše mám podle návodu, ale jak říkám, teprv se seznamuju s nette. Potřeboval bych nějakou podrobnou aktuální příčuku. Je někde?
Berry | 22. 1. 2011, 12:43 | comment
Tak uz to vsechno jede jak ma
lipka | 1. 3. 2011, 20:16 | comment
mam problem s pripojenim k databaze Line 109:throw new DibiDriverException(mysql_error(), mysql_errno()); na tomto riadku mi vyhodi takuto chybu Access denied for user ''@‚localhost‘ (using password: NO)
Tor | 8. 3. 2011, 22:24 | question
Chtel bych se zeptat jak do hlavni stranky nastavit routovani z document_root ? S hierarchii:
index.php (klasicke php) – document_root (nette) – app – lips
Diky
joeyGTR | 30. 7. 2011, 10:34 | comment
Zdravím, chcel by som vedieť pre akú verziu nette je tento tutoriál. Niektoré veci sa totiž v mojej verzii líšia. Najčastejšie koncovky súborov napríklad: config.ini mám ako config.neon a podobne…Alebo aj toto: „…dibi (v distribuci Nette se nachází ve složce 3rdParty)“ no ja takú zložku vo svojej distribúcii nemám. Ešte by ma zaujímalo aký lokálny server by som mal použiť…teraz mám WampServer. Ďakujem za info, som začiatočník.
kuki | 5. 10. 2011, 22:06 | bug
Ahoj, už nevím kde bych mohl dělat chybu. Vše mi funguje, ale když
v soboru HomepagePresenter upravim funkci public function
renderDefault() na $this->template->posts =
PostsModel::fetchAll();
tak se mi po zobrazeni webu zobrazuje jen Server error HTTP Error 500
nevíte kde může být chyba??
díky
Jan Tvrdík | 6. 10. 2011, 23:14 | comment
Ne, ale zkus se podívat do error logu serveru. (Případně si zapni logování, pokud ho máš vypnuté.)
Lei | 5. 9. 2010, 9:09 | comment
Diky za tutorial, hlavne za jeho jednoduchost. V OOP jeste trochu pokulhavam a treba v Quick Startu jsem moc nechapal spojeni s modelem. Tady je jen nezbytne minimum a diky tomu je to snadno pochopitelne.