Internet Info, s.r.o. Lupa Měšec Podnikatel Root Zdroják DigiZone Slunečnice Vitalia TopDrive KupDnes Navrcholu NovýTarif Dobrý web Weblogy Woko Jagg Computer.cz SK: MojeLinky

Hlavní navigace

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

Tweetni to Twitter Jaggni to! Jagg Del.icio.us Delicious

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.

Anketa

Refaktorujete často?

       

David Grudl

David Grudl

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

Školení Google+ pro firmy

DW - Školení PPC
  • Jak využít Google+ pro firemní komunikaci a marketing.
  • Čím se liší Google+ od Twitteru a Facebooku z pohledu firemního využití.
  • Jak využít Google+ v souladu s pravidly užívání.
  • Založení Google+ Page (Stránky) krok po kroku, včetně praktických tipů.

Detailní informace o školení Google+ »

Přehled názorů

...
ja 31. 3. 2009 06:08
Nový
├ 
Re: ...
v6ak 31. 3. 2009 09:41
Nový
├ 
Re: ...
mat 31. 3. 2009 10:12
Nový
├ 
Re: ...
anonymní uživatel 31. 3. 2009 12:30
Nový
└ 
Re: ...
David Grudl 31. 3. 2009 16:40
Nový
 
├ 
Re: ...
v6ak 31. 3. 2009 16:49
Nový
 
│
└ 
Re: ...
David Grudl 31. 3. 2009 17:00
Nový
 
│
 
└ 
Re: ...
v6ak 31. 3. 2009 17:08
Nový
 
│
 
 
└ 
Re: ...
David Grudl 31. 3. 2009 17:28
Nový
 
│
 
 
 
└ 
Re: ...
v6ak 31. 3. 2009 17:39
Nový
 
│
 
 
 
 
└ 
Re: ...
David Grudl 31. 3. 2009 18:02
Nový
 
│
 
 
 
 
 
└ 
Re: ...
v6ak 31. 3. 2009 21:09
Nový
 
└ 
Re: ...
ja 31. 3. 2009 20:11
Nový
 
 
└ 
Re: ...
David Grudl 31. 3. 2009 20:15
Nový
 
 
 
└ 
Re: ...
yeah 31. 3. 2009 22:14
Nový
 
 
 
 
└ 
Re: ...
David Grudl 2. 4. 2009 14:06
Nový
 
 
 
 
 
└ 
Re: ...
yeah 2. 4. 2009 16:23
Nový
 
 
 
 
 
 
└ 
Re: ...
Václav Šír 2. 4. 2009 23:26
Nový
OT: v jakém SW vznikl obrázek životního cyklu?
Baset 31. 3. 2009 12:22
Nový
├ 
Re: OT: v jakém SW vznikl obrázek životního cyklu?
David Grudl 31. 3. 2009 16:41
Nový
└ 
Re: OT: v jakém SW vznikl obrázek životního cyklu?
Abraxis 31. 3. 2009 18:13
Nový
spring mvc
anonymní uživatel 2. 4. 2009 21:03
Nový
├ 
Re: spring mvc
Roman 2. 4. 2009 23:27
Nový
│
└ 
Re: spring mvc
anonymní uživatel 3. 4. 2009 09:39
Nový
│
 
├ 
Re: spring mvc
Jiří Knesl 3. 4. 2009 10:20
Nový
│
 
├ 
Re: spring mvc
anonymní uživatel 3. 4. 2009 10:41
Nový
│
 
└ 
Re: spring mvc
Aleš Roubíček 3. 4. 2009 10:46
Nový
│
 
 
└ 
Trefné
v6ak 3. 4. 2009 11:01
Nový
└ 
Re: spring mvc
David Grudl 3. 4. 2009 01:41
Nový
 
├ 
Re: spring mvc
anonymní uživatel 3. 4. 2009 09:52
Nový
 
│
├ 
Re: spring mvc
roman 3. 4. 2009 10:43
Nový
 
│
└ 
Re: spring mvc
David Grudl 3. 4. 2009 16:46
Nový
 
│
 
└ 
Re: spring mvc
anonymní uživatel 4. 4. 2009 07:09
Nový
 
│
 
 
├ 
Re: spring mvc
Robert Novotny 4. 4. 2009 07:11
Nový
 
│
 
 
│
└ 
Re: spring mvc
David Grudl 6. 4. 2009 14:31
Nový
 
│
 
 
└ 
Re: spring mvc
David Grudl 6. 4. 2009 14:23
Nový
 
└ 
Re: spring mvc
anonymní uživatel 3. 4. 2009 16:32
Nový
Nette - nieco mi na CoffeeMachine nesedi
Srigi 7. 1. 2010 13:08
Nový
└ 
Re: Nette - nieco mi na CoffeeMachine nesedi
Srigi 7. 1. 2010 13:35
Nový
Nepřehlednost
Froyo 3. 11. 2010 20:36
Nový
problem pri zmene surobu?
nemesisqo 27. 2. 2011 19:11
Nový
       

Tento text je již více než dva měsíce starý. Chcete-li na něj reagovat v diskusi, pravděpodobně vám již nikdo neodpoví. Pro řešení aktuálních problémů doporučujeme využít naše diskusní fórum.

Zasílat nově přidané příspěvky e-mailem