Nette Framework: Refactoring

Minule jsme si ukázali vývoj jednoduché webové aplikace podle architektury Model-View-Presenter v Nette Frameworku. Dnes se ji pokusíme vylepšit, poukázat na kritická místa a předvést jejich správné řešení.

Seriál: Začínáme s Nette Framework (17 dílů)

  1. Nette Framework: zvyšte svoji produktivitu 10.3.2009
  2. Nette Framework: Odvšivujeme 17.3.2009
  3. Nette Framework: MVC & MVP 24.3.2009
  4. Nette Framework: Refactoring 31.3.2009
  5. Nette Framework: Chytré šablony 7.4.2009
  6. Nette Framework: adresářová struktura aplikace 14.4.2009
  7. Nette Framework: AJAX 21.4.2009
  8. Nette Framework: AJAX (pokračování) 28.4.2009
  9. Nette Framework: AJAX (dokončení) 5.5.2009
  10. Nette Framework: Sessions 12.5.2009
  11. Nette Framework: Přihlašování uživatelů 19.5.2009
  12. Nette Framework: Ověřování oprávnění a role 26.5.2009
  13. Nette Framework: Neprůstřelné formuláře 2.6.2009
  14. Nette Framework: Neprůstřelné formuláře II 9.6.2009
  15. Nette Framework: Neprůstřelné formuláře III 16.6.2009
  16. Nette Framework: Cache 23.6.2009
  17. Nette Framework: Co se do seriálu nevešlo? 30.6.2009

Automat na kávu v minulém díle demonstroval, jak lze webovou aplikaci rozdělit do tří logických vrstev, tedy model, presenter a pohled (view). Zůstaňme u tohoto dělení i nadále a podívejme se, jak můžeme jednotlivé vrstvy vylepšit.

Model

Tuto rovinu aplikace v seriálu záměrně ošidím. Nette Framework je zaměřený především na podporu prezentační vrstvy, přičemž pro model nabízí „jen“ vyšší standard programování v PHP reprezentovaný třídami Object a Debug, dále autoloading nebo třídu Environment, které na vás čekají v šestém díle seriálu. Žádné jiné konkrétní knihovny, třeba pro práci s databází, v něm nenajdeme.

Jde o poměrně důležitý rys frameworku, který lze přeložit jako: „používejte knihovnu která vám vyhovuje, žádný styl práce vám nenutíme.“ Důsledná nezávislost na databázové vrstvě vám dovolí používat ORM nástroje jako je Doctrine či Propel, knihovnu Zend_Db_Table nebo z opačného ranku například Dibi. Poslední jmenovaná knihovna je preferovaná díky úzké vazbě na Laděnku a dokonce ji najdete přímo v distribučním archívu.

Pro hnidopichy: pokud máte pocit, že prezentuji jako přednost něco, co je ve skutečnosti nedostatek frameworku, pak si prostě představte, že databázovou vrstvu frameworku tvoří Dibi a rozdělení do dvou samostatných projektů berte jen jako politické rozhodnutí.

Model tedy refaktoringu ušetříme, nikoliv však proto, že by to nepotřeboval, ale protože to seriál neposune dál.

Presenter

Na rozdíl od modelu nás u presenteru čeká pořádná práce. Jak už jsem naznačoval minule, reakce uživatele zhmotněná do odkazu může žádat o:

  1. změnu pohledu, presenteru nebo stavu (stavem je například hodnota parametru $money)
  2. vykonání příkazu (po vhození mince, stisknutí tlačítka)

Zapamatujte si důležitou zásadu: po vykonání příkazu by vždy mělo následovat přesměrování na další stránku. Nebo jinými slovy, URL žádající o provedení příkazu mohou být součástí HTML stránky, ale v adresním řádku prohlížeče by měly být jen URL určující stav. Po vykonání příkazového URL přesměrujeme na stavové URL.

Vezměme si automat na kávu. Pokud už v něm jsou 2 Kč a vhodím pětikorunu, tak prohlížeč zavolá příkazové URL:

  • index.php?money=2&do=insert&coin=5

ze kterého, jak káže moudré pravidlo, bychom měli přesměrovat na stavové URL říkající „v automatu je 7 Kč“:

  • index.php?money=7

Kdyby k přesměrování nedošlo, byl by to prohřešek proti logice HTTP protokolu, kde URL mají reprezentovat stav, proti SEO, neboť vyhledávač vidí stejný obsah na více rozdílných URL, a především proti použitelnosti. Do historie prohlížeče by se nám dostala adresa, po jejímž otevření, ať už tlačítkem obnovit nebo zpět, by došlo k nechtěnému vhození dalších mincí do automatu. Což zamrzí obzvlášť proto, že automat nevrací.

Nebylo by jednodušší generovat rovnou stavové URL? V tomto případě by to asi možné bylo, jsou ale situace, kdy to nelze. Představme si například katalog produktů řazených podle abecedy s odkazem na předchozí a následující položku. Zjištění konkrétního ID není úplně triviální a pokud by takových odkazů bylo na stránce hodně, mohlo by to aplikaci zbytečně brzdit. Stejně tak pokud se obsah katalogu často mění, nelze v době vykreslování přesně říci, který produkt bude při kliknutí na odkaz tím předchozím nebo následujícím. Odkaz proto bude lepší sestavit z ID aktuálního produktu s příkazem next nebo  prev.

Ale stále platí pravidlo o přesměrování. Jinak by se mohlo stát, že na e-shopu, kde nabízejí od šroubku po lokomotivu, si budete prohlížet produkt „záložní zdroj“, načež kliknete na odkaz „další produkt“ a dostanete se na „zdarma parfém Sergio Tacchini“. To je něco pro přítelkyni, řeknete si, a odešlete jí odkaz e-mailem a připíšete: Na tohle by ses měla vážně podívat! Bohužel půjde o příkazový odkaz, jako třeba index.php?id=zalozni-zdroj&do=next, a dříve, než se přítelkyně na stránku podívá, obchod rozšíří sortiment mimo jiné o přípravek proti „zápachu z úst“. Abecední řazení ho neomylně umístí ihned za záložní zdroj a zadělá vám tak na krizi vztahu. I kdyby to vaše polovička přešla, vy situaci znovu rozdmýcháte večer nevinnou otázkou: „Miláčku, dostala jsi ten e-mail? Co na to říkáš?“

K přesměrování slouží metoda redirect(), která se používá stejně, jako metoda pro generování odkazů link(). Takže upravíme metodu handleInsert tak, aby po provedení úkonu přesměrovala. A kam? To je dobrá otázka. Cílem bude aktuální stránka, aktuální stav a k tomu použijeme speciální slovo  this.

public function handleInsert($coin)
{
        // zvýšíme hodnotu vhozených mincí
        $this->money += max(0, (int) $coin);

        // po příkazu musí následovat přesměrování
        $this->redirect('this');
} 

Presenter a jeho pohledy

Řetězec this doslova znamená aktuální pohled. Až dosud jsme si vystačili s presenterem majícím jediný pohled a jedinou šablonu, jehož název byl default. Odsud třeba název šablony Machine.default.phtml složený z názvu presenteru, pohledu a přípony .phtml. Přesměrování na this je pak ekvivalentní s přesměrováním na  $this->redirect('default').

Kromě toho, že pohled určuje, která šablona se má zobrazit, zavolá Nette Framework také metodu render{NazevPohledu}(), kde je možné naplnit šablonu daty. V případě pohledu default by se tedy volala metoda renderDefault(). Její existence je přitom nepovinná.

Na řadě je refactoring metody handleBuy(). Navrhl bych následující postup: pokud se koupě kávy zdaří, přesměrovat na nový pohled coffee, tedy takové potvrzení objednávky. V případě neúspěchu nepřesměrovávat vů­bec.

public function handleBuy()
{
        $model = new Model;
        $result = $model->buyCoffee($this->money);

        if ($result) {
                $this->money = 0;  // vynulujeme částku (automat nevrací)
                $this->redirect('coffee'); // přesměrujeme na pohled coffee

        } else {
                $this->template->display = 'Málo peněz';
                $this->template->robots = 'noindex,noarchive';
        }
} 

Všimněte si, že odkazy na příkazy se od odkazů na pohledy odlišují vykřičníkem. Tj. $presenter->link('buy!') volá metodu handleBuy() (v rámci stále stejného pohledu default), zatímco odkaz $presenter->link('coffee') resp. $presenter->redirect('coffee') vede na pohled coffee. Po přesměrování se Nette Framework pokusí nejprve zavolat metodu presenteru renderCoffee(), která může naplnit šablonu daty, a poté vykreslí šablonu v souboru Machine.coffee.phtml. Protože taková šablona neexistuje, odpovědí bude HTTP chyba 404 – Not Found. Takže šablonu vytvoříme – bude zobrazovat kávovar s kelímkem hotové kávy, bez mincí.

Zbývá vysvětlit, proč v případě chyby přesměrování pomíjím. Je to totiž situace, kde opakování požadavku tlačítkem Obnovit je naopak vítané. Pokud byla chyba na straně automatu (došla voda), po jejím napravení lze objednávku zopakovat a zakončit úspěchem. Pokud by však došlo k přesměrování na stránku s chybovou hláškou, obnovení stránky by nevedlo k dalšímu pokusu provést transakci a přesto, že by se chyba na straně automatu vyřešila, stránka by stále zobrazovala tutéž již neaktuální chybovou zprávu.

Ačkoliv nedojde k přesměrování, doporučil bych stránku vyřadit z indexu vyhledávačů. Za tímto účelem jsem do šablony předal proměnnou  $robots.

Poslední úprava presenteru souvisí s tím, že už máme dva pohledy: default a coffee. V pohledu coffee nestojíme o to, aby se na displeji zobrazovala výzva k vhození peněz, kterou má na svědomí metoda startup(). Naplnění šablony daty proto přesuneme na vhodnější místo – do již zmíněné metody renderDefault().

public function renderDefault()
{
        if (empty($this->template->display)) {
                // na displeji zobrazíme celkovou částku nebo výzvu k vhození peněz
                $this->template->display = $this->money ? "$this->money Kč" : ('Vhoď ' . Model::COFFEE_PRICE . ' Kč');
        }
} 

Zazněly tu názvy metod jako startup(), handle{Příkaz}(), render{Pohled}()… nejvyšší čas se podívat na životní cyklus presenteru:

Životní cyklus presenteru

Životní cyklus presenteru

Netrapte se tím, že obsahuje celou řadu metod, které jsme zatím neprobírali. Dostaneme se k nim později. Teď je důležité pořadí, v jakém se jednotlivé metody volají (odshora dolů). Vidíte, že metoda renderDefault() se volá později, než handleBuy(). Protože v ní v případě neuskutečněné objednávky naplníme $this->template->display = 'Málo peněz', doplnil jsem renderDefault() o podmínku, jestli je proměnná display prázdná, abychom si ji nepřepsali.

Optimalizujeme pohled

Máme už dva pohledy a také dvě šablony Machine.default.phtml a Machine.coffee.phtml. Obě se přitom liší jen obsahem elementu <body>...</body>, zbytek je stejný. Protože kód by se neměl opakovat ani v případě šablon, přistoupíme k refactoringu spočívajícím v zavedení šablony layoutu. Tu uložíme do souboru @layout.phtml. Zavináč na začátku názvu slouží k přehlednému odlišení od šablon pohledů.

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <meta http-equiv="Content-Language" content="en" />
        <?php if (isset($robots)):?><meta name="robots" content="<?php echo $robots ?>"><?php endif ?>

        <title>Coffee Vending Machine in Nette Framework</title>

        <style type="text/css">
        ...
        </style>
</head>

<body>
        <?php $content->render() ?>
</body>
</html> 

V souborech Machine.default.phtml a Machine.coffee.phtml zůstane jen vnitřek stránky. Vloží se do šablony layoutu v místě volání  $content->render().

V podstatě jsme připraveni na poslední krok, a tím je slibovaná podpora AJAXu. Pokud se však podíváte na šablony, ruku na srdce, nejsou zbytečně moc ukecané a nepřehledné? Co byste řekli na to, kdybychom mohli místo zápisu:

<div id="machine">
        <p id="display"><?php echo htmlSpecialChars($display) ?></p>

        <a href="<?php echo $presenter->link('buy!') ?>"><img id="button" src="images/button.png" alt="Kup kávu" /></a>

        <?php if (isset($coffee)): ?>
                <a href="<?php echo $presenter->link('default') ?>"><img id="cup" src="images/cup.png" alt="Kelímek s kávou" /></a>
        <?php endif ?>
</div> 

používat raději něco přehlednějšího, úspornějšího a hezčího, třeba něco takového:

<div id="machine">
        <p id="display">{$display}</p>

        <a href="{link buy!}"><img id="button" src="images/button.png" alt="Kup kávu" /></a>

        {if isset($coffee)}
                <a href="{link default}"><img id="cup" src="images/cup.png" alt="Kelímek s kávou" /></a>
        {/if}
</div> 

Jak na to si ukážeme v příštím díle.

Zdrojový kód ukázek použitých v článku je k dispozici ke stažení.

Autor článku je vývojář na volné noze, specializuje se na návrh a programování moderních webových aplikací. Pravidelně pořádá školení pro tvůrce webových aplikací, vyvíjí open-source knihovny Texy, dibi a Nette Framework.

Refaktorujete často?

David Grudl školí, je autorem PHP knihoven Nette Framework, databázové vrstvy dibi a formátovače HTML kódu Texy!.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 47

Přehled komentářů

Anonym saso pocmafrany
al Re: saso pocmafrany
mekele Re: saso pocmafrany
starenka Re: saso pocmafrany
gawan Re: saso pocmafrany
ja ...
v6ak Re: ...
mat Re: ...
Anonym Re: ...
David Grudl Re: ...
v6ak Re: ...
David Grudl Re: ...
v6ak Re: ...
David Grudl Re: ...
v6ak Re: ...
David Grudl Re: ...
v6ak Re: ...
ja Re: ...
David Grudl Re: ...
yeah Re: ...
David Grudl Re: ...
yeah Re: ...
llook Re: ...
Baset OT: v jakém SW vznikl obrázek životního cyklu?
David Grudl Re: OT: v jakém SW vznikl obrázek životního cyklu?
Abraxis Re: OT: v jakém SW vznikl obrázek životního cyklu?
Anonym spring mvc
Roman Re: spring mvc
Anonym Re: spring mvc
Jiří Knesl Re: spring mvc
Anonym Re: spring mvc
Aleš Roubíček Re: spring mvc
v6ak Trefné
David Grudl Re: spring mvc
Anonym Re: spring mvc
roman Re: spring mvc
David Grudl Re: spring mvc
Anonym Re: spring mvc
Robert Novotny Re: spring mvc
David Grudl Re: spring mvc
David Grudl Re: spring mvc
Anonym Re: spring mvc
proboha Re: spring mvc
Srigi Nette - nieco mi na CoffeeMachine nesedi
Srigi Re: Nette - nieco mi na CoffeeMachine nesedi
Froyo Nepřehlednost
nemesisqo problem pri zmene surobu?
Zdroj: https://www.zdrojak.cz/?p=2975