Návod jak vytvořit blog v Nette 2.2.1

Napsal dne 22.6.2013 v Nette

Edit: Článek byl aktualizován na verzi 2.2.1

Poslední dobou se hromadí žádosti o pomoc s tvorbou blogu, protože tento starší návod vychází z mého pohledu již zastaralé verze Nette a navíc využívá databázovou vrstvu Dibi, kterou začátečníci nemusí hned pochopit. Protože jsem měl chvilku, rád bych vám nabídl návod jak vytvořit stejný blog, ovšem v nové verzi.

Nebudu vymýšlet nové kolo a tak se budu držet staršího návodu co se týče funkcionality i vzhledu. Proto některé texty budou (s dovolením) zkopírovány od autora PJK a některé přizpůsobeny „novému kabátu“. Dost už kecání, začneme tvořit náš první blog.

Úvod

Myslím, že představovat co je to blog a k čemu slouží nemá cenu. Každý ho zná nebo už někdy o něm slyšel. Abychom mohli vytvořit blog pomocí frameworku Nette, je prvně potřeba ho stáhnout. Že tomu předchází instalace Apache (či jiného webového serveru), MySQL (či jiné databáze) a PHP snad již zmiňovat nemusím, o jejich instalaci snad někdy jindy. Než začneme knihovnu Nette nahrávat na web, je vhodné zjistit si pomocí nástroje checker (nachází se ve staženém balíčku Nette ve složce tools/Requirements-Checker/) jestli náš server podporuje Nette.

Vycházet budeme ze sandboxu, který opět najdeme ve staženém balíčku Nette. Původní obsah v šabloně default.latte (adresář app/templates/Homepage/) tak můžeme klidně smazat.

Databáze a model

Začněme 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ářů. Jen připomínka, jak už z kódu vyplývá, je potřeba, aby byla na vašem serveru zapnutá možnost vytvářet strukturu pomocí storage enginu InnoDB (jinak vám tento blog fungovat nebude!).

Nyní když máme vytvořenou databázovou strukturu, je potřeba propojit ji s Nette. Jako první začneme u souboru config.neon (adresář app/config/). Abychom se mohli úspěšně připojit, je potřeba vyplnit přihlašovací údaje k databázi. Ty vyplníme v sekci database:

database:
 dsn: 'mysql:host=localhost;dbname=nazevDatabaze'
 user: uzivatelskyUcet
 password: heslo

Dále je zde potřeba naše repozitáře, resp. modely, zaregistrovat jako služby. V sekci services již máme předvyplněné tři služby (dvě anonymní s pomlčkou na začátku a router) a my k nim přidáme další dvě:

services:
 - App\Model\UserManager
 - App\RouterFactory
 posts: PostsRepository
 comments: CommentsRepository
 router: @App\RouterFactory::createRouter

Tím naše úloha v configu skončila a můžeme se přesunout na tvorbu repozitářů. Jako první si můžeme vytvořit předka všech další repozitářů, kteří budou z něj dědit a nebude tak potřeba psát stále dokola připojení k databázi. Vytvořme si teda repozitář s názvem Repository:

abstract class Repository extends Nette\Object {

/** @var Nette\Database\Context */
 protected $connection;

public function __construct(Nette\Database\Context $db) {
    $this->connection = $db;
 }
}

Dále si vytvoříme repozitáře pro obě tabulky z databáze:

class PostsRepository extends Repository {

public function fetchAll() {
 return $this->connection->table('posts')
 ->order('date DESC');
 }
class CommentsRepository extends Repository {

public function fetchArticleComments($post_id) {
 return $this->connection->table('comments')
 ->where('post_id = ?', $post_id);
 }

Pro srozumitelnost budou prozatím jejich metody bez zbytečných ohledů tahat všechna dostupná data. Všechny tři soubory s definicí tříd uložte do složky /app/model a pojmenujte je podle třídy, kterou obsahují (Repository.php, PostsRepository.php, CommentsRepository.php).

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í.

Homepage pak bude vypadat takto:

namespace App\Presenters;

use Nette,
 App\Model;

class HomepagePresenter extends BasePresenter {

/** @var \PostsRepository */
 private $postsRepository;

/** @var \CommentsRepository */
 private $commentsRepository;

public function __construct(\PostsRepository $postsRepository, \CommentsRepository $commentsRepository) {
 $this->postsRepository = $postsRepository;
 $this->commentsRepository = $commentsRepository;
 }

public function renderDefault() {
 $this->template->posts = $this->postsRepository->fetchAll();
 }
}

Asi by bylo vhodné, abych to trochu vysvětlil. Prvně je potřeba vytvořit privátní proměnné, do který budeme vkládat naše odkazy na repozitáře. Dále pomocí Dependency Injection vstříkneme pomocí kontruktoru naše již zaregistrované služby do proměnných (vášnivou debatu pomocí čeho využívat DI můžeme najít třeba zde). Tím jsme získali možnost využívat všech veřejných metod z našich repozitářů, jako v případě metody fetchAll z repozitáře PostsRepository.

View

Poslední částí vzoru MVC je view neboli pohled. Ten je v Nette reprezetován jako šablona, resp. Latte šablona. Nyní 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.latte. 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.latte, 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']|truncate:300}</p>
        </div>
        {/foreach}
    {else}
        Zatím nebyl napsán žádný článek.
    {/if}
</div>
{/block}

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. Klasický 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. Protože zobrazení samostatného příspěvku nijak nesouvisí s titulní stranou, přidáme do našeho preseneru novou metodu render*(), která má na starost renderování, v tohle případě bude vykreslovat stránku Single a bude ji předán ID dané stránky. Poté vše co jsme získali z databáze uložíme do proměnné post. To samé uděláme pro komentáře:

    public function renderSingle($id) {
        $this->template->post = $this->postsRepository->fetchSingle($id);
        $this->template->comments = $this->commentsRepository->fetchArticleComments($id);
    }

Vytvoříme příslušnou metodu v PostsRepository:

    public function fetchSingle($id) {
        return $this->connection->table('posts')
                        ->where('id = ?', $id)
                        ->fetch();
    }

A také v CommentsRepository:

    public function fetchArticleComments($post_id) {
        return $this->connection->table('comments')
            ->where('post_id = ?', $post_id);
    }

Také musíme vytvořit template pro tento požadavek, takže do /app/templates/Homepage/single.latte vložíme:

{block content}
<a n:href="Homepage:">&amp;lt;&amp;lt; 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 $comments}
        {foreach $comments as $comment}
            <p>{$comment['body']}</p>
            <small>{$comment['author']}, {$comment['date']|date}</small>
            <hr>
        {/foreach}
    {else}
        Ke článku zatím nebyly napsány žádné komentáře. Buďte první!
    {/if}
</div>

Nyní můžete v prohlížeči zkusit otevřít třeba /Homepage/single/2.

Odkazy

Aby se sem dostal i běžný uživatel, potřebujeme nějaké odkazy z hlavní stránky. K tomu můžeme použít makro n:href. Předělejme tedy view titulní stránky default.latte na:

{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 n:href="Homepage:single $post['id']">Více…</a>
        </div>
        {/foreach}
    {else}
        Zatím nebyl napsán žádný článek.
    {/if}
</div>
{/block}

Všimněte si, že místo HTML atributu href jsme použili n:makra n:href. Jeho hodnotou pak není URL, jak by tomu bylo v případě atributu href, ale rovnou akce presenteru. Po kliknutí na odkaz se dostane ke slovu metoda HomepagePresenter::renderSingle() a jako parametr $id ji bude předána hodnota proměnné $post[‚id‘].

V naší pravidelné minutovce teorie bych nyní rád vyzdvihl dvě věci:

  • Všimněte si helperu truncate:300 a jeho efektu.
  • URL je zpětně vytvořeno tak, aby odpovídalo routám v bootstrap.php a 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 Form. Třída Form 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\UI;
    public function createComponentCommentForm() {
        $form = new UI\Form();
        $form->addText('author', 'Jméno: ')
                ->addRule($form::FILLED, 'To se neumíš ani podepsat?!');
        $form->addTextArea('body', 'Komentář: ')
                ->addRule($form::FILLED, 'Komentář je povinný!');
        $form->addSubmit('send', 'Odeslat');
        $form->onSuccess[] = callback($this, 'commentFormSubmitted');
        return $form;
    }

    public function commentFormSubmitted(UI\Form $form) {
        $data = $form->getValues();
        $data['date'] = new \DateTime();
        $data['post_id'] = (int) $this->getParam('id');
        $id = $this->commentsRepository->insert($data);
        $this->flashMessage('Komentář uložen!');
        $this->redirect("this");
    }

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.latte‘.

Do CommentsRepository musíme přidat použitou metodu:

    public function insert($data) {
        $this->connection->table('comments')
                ->insert($data);
    }

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 v presenteru Homepage upravíme:

    public function renderSingle($id) {
        if (!($post = $this->postsRepository->fetchSingle($id))) {
            $this->error('Článek nebyl nalezen'); //pokud clanek neexistuje, presmerujeme uzivatele
        }
        $this->template->post = $post;
        $this->template->comments = $this->commentsRepository->fetchArticleComments($id);
    }

Poslední věc, která zbývá, je úprava naší šablony single.latte:

{block content}
<a n:href="Homepage:">&amp;lt;&amp;lt; 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 $comments}
        {foreach $comments as $comment}
                <p>{$comment['body']}</p>
                <small>{$comment['author']}, {$comment['date']|date}</small>
                <hr>
         {/foreach}
    {else}
        Ke článku zatím nebyly napsány žádné komentáře. Buďte první!
    {/if}
</div>

{control commentForm}

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í.

A to je vše, jednoduché co? Snad jsem vše vysvětlil jasně a pochopili jste některé základy. Pokud by bylo někomu stále něco nejasné, nechť se ozve v komentáři.

Kompletní zdrojový kód si můžete prohlédnout na Githubu nebo stáhnout zde.

Pokračování návodu a doplnění blogu o přihlašování a administraci najdete hned v dalším článku zde.

Štítky:
  • http://php.vrana.cz/ Jakub Vrána

    Moc pěkný článek. Mám jen pár drobných připomínek:

    Třídy se jmenují *Repository, ale uživatel je vyzván k jejich uložení do souborů *Model.

    Název metody CommentsRepository::fetchAll() není úplně nejšťastnější, protože časem můžeme potřebovat metodu pro získání všech komentářů nezávisle na článku (např. do comment feedu).

    Metoda CommentFormSubmitted by podle konvence měla začínat malým písmenem.

    Při neexistujícím článku je lepší vrátit 404 než přesměrování. Asi to příklad trochu zesložití, ale pořád lepší, než uživatele učit antipatterny.

    V poslední šabloně je zapomenutý {dump}.

    • Jerry Klimčík

      Mockrát děkuji za reakci. Článek vychází ze staršího návodu a tak jsem některé věci, jako například zrovna název té metody fetchAll(), ponechal a některé věci, jako například model, přepsal (i když uznávám, že jsem si těch *modelů nevšiml). Zbytek se pokusím upravit, je to můj první návod a tak jsem rád za každou kritiku.

      • Járon

        Mohl bys tam zkusit implementovat počítadlo počtu zobrazení pomocí eventů
        http://filip-prochazka.com/blog/eventy-a-nette-framework

        • Jerry Klimčík

          Možné by to bylo, nicméně článek je určený pro začátečníky Nette a tak implementace počítadla pomocí eventů nepovažuji za klíčový základ pro pochopení frameworku.

  • igor

    moc pekne, velmi bych ocenil, kdybyste doplnil tvorbu administrace a prihlasovani. dekuji

    • Jerry Klimčík

      Dobrý den, pokračování blogu najdete hned v mém dalším článku

  • Edmund

    Ahoj, zkusil jsem si projet návod a narazil jsem na pár drobností, které by bylo dobré opravit.
    – ve větě: Oba soubory s definicí tříd uložte do složky /app/models… by mělo být jen model bez s na konci. Alespoň podle sandboxu, který jsem dneska stáhnul.
    – v prvních zdrojácích pro PostsRepository.php a CommentsRepository.php jsou překlepy, v jednom je navíc středník a ve druhém je místo > >
    – není napsáno, do jakého souboru patří třída abstract repository.

    To je asi všechno, čeho jsem si jako začátečník všiml. Každopádně díky za návod, hodně pomůže do začátků.

    • Jerry Klimčík

      Ahoj, díky za upozornění, již jsem to spravil. Jinak název adresáře není nijak důležitý, auto-loading tříd nám to zajišťuje. Abstraktní třída Repository patří taktéž do adresáře app/model/, z ní pak dědí další repozitáře. Taktéž může být umístěna jinde, nicméně kvůli určité ucelenosti to není doporučeno (proč si to také více komplikovat).

  • Marty

    Ahoj,

    zda se, ze odkazy na obrazky jsou rozbite.

    Pekny tutorial!

    • Jerry Klimčík

      Opraveno, obrázky byly použity z oficiální stránky Nette a bohužel došlo k jejím odstraněním, nahradil jsem je tedy svými screenshoty.

  • Martin

    Ahoj, měl bych dotaz ke zde uvedenému článku. Jsem zatím úplný začátečník s nette, takže pokud by měl být můj dotaz stupidní, tak se předem omlouvám.

    V HomepagePresenter.php je definice metody inject(). Mě by zajímalo, kdy a kde se tato metoda volá? Stará se o to nette samotné nebo bych to měl někde pořešit sám? Kde bych se případně o tomto mohl dočíst více?

    Děkuji za přínosný článek i za odpověď.
    Martin

    • Jerry Klimčík

      Zdravím, inject() se volá na začátku presenteru aby získala objekt z repositáře. O vše se stará Nette a není tak třeba nic řešit, stačí vytvořit proměnné s anotací na daný repositář a v metodě inject() je předat jako parametry potřebných typu tříd (resp. repositářů). Injection jako takové je vlastně dependency injection, o kterém se můžete dočíst více zde nebo pohledat na netu, je toho nepřeberné množství. V tutoriálu na Nette můžete najít místo injection metodu pomocí contextu (Nette\Database\Context), která má však stejný účel – předat data z repositáře do presenteru. Snad jsem trochu pomohl.