Původně jsem chtěl dnes napsat článek o řešení URL mapování, které používám,
nicméně ještě to odložím (tím odložím i to zveřejnění
zdrojových kódů blogu). Dnes bych se rád zmíníl o tom, jak správně uchopit
Model View Controller (MVC) návrhový vzor a to
jak obecně, tak z pohledu PHP a Zend Frameworku.
Nebudu popisovat úplné základy a budu předpokládat, že čtenář už má s použitím
MVC nějaké zkušenosti.
Další věc, kvůli které článek píšu je to, že jsem na základě uvedených úvah
přepsal domain třídy (v MVC část model) a nedosáhl jsem úplně toho, co jsem
od toho čekal.
Co je a k čemu se používá návrhový vzor MVC asi většina lidí, co to bude číst ví. Hlavním důvodem k použití MVC v aplikacích je větší přehlednost projektu a rozdělení na vrstvy, kde každá má na starost určitou část aplikace. Aplikace se tím stává přehlednější a lépe udržovatelná, což je potřeba zejména při rozsáhlejších projektech.
Jako jeden z hlavních důvodů pro použití MVC je dost často uváděno oddělení funkcionality tříd, které patří do modelu, view a controlleru, pro jednotlivé části (vrstvy) mám trochu restriktivnější požadavky, inspirovaly mě zejména některé popisy java enterprise patternů.
Požadavky na Model
Model by měl obsahovat domain (business) objekty, které nám dávají náhled na to, s čím vlastně naše aplikace pracuje. Typicky jde o objekty jako článek, osoba, objednávka atd. Tyto objeky by měly být navrženy tak, že jsou pokud možno nezávislé na použité technologii pro ůložiště dat (např. databáze, XML soubory, web services). To vede na použití DAO (Data Access Object) tříd s jednotným rozhraním, které budou naše domain objekty používat pro přístup k datům.
Často se jako základu pro modelu používají různá ORM řešení, založená třeba na ActiveRecord nebo Data Mapperech. V Javě je to zejména Hibernate, v PHP například Propel. Tyto mají DAO již v sobě, nevýhodou je pak většinou omezení na relační databáze. Myslím si, že data z XML souborů nebo web services přes takováto ORM asi nedostanete. K tomu, jak je to v PHP se ještě vrátím.
Požadavky na View
View by měl tvořit systém šablon, do kterých jsou v controller objektech předávána data z domain tříd. Pro maximální separaci view od business logiky a controlleru pak hovoří použití něčeho jako jsou Data Transfer objects (DTO), nebo Value objects (VO). Pak při změnách v domain objektech není třeba vůbec měnit view. Často se ale používají přímo domain objekty. Pro složitější akce ve view se použijí view helpery, ty dostanou data z modelu a to zase nejlépe prostřednictvím DTO nebo VO.
Conroller na propojení view a modelu.
V controlleru se budou provádět use cases naší aplikace, pracovat s domain třídami a jejich obsah pak nejlépe prostřednicvím DTO/VO předávat do view. Jak jsem zmínil výše, často se používají přímo domain objekty.
DTO are evil
Tak jak jsem to popsal, dosáhneme téměř dokonalého oddělení jednotlivých částí aplikace. V praxi (zejména ve světě Javy se o tom dost často debatuje) se ale ukazuje, že dochází k hromadění kódu pro DTO, což jsou v podstatě jen gettery a settry. DTO se z každou aplikací musí psát pořád dokola, což je neefektivní a dost otrava. Proto se většinou domain objekty (nejčastěji vytvořené prostřednicvím nějakého ORM) vystavují přímo do View. To je většinou OK, je nutné si pak uvědomit, že při voláních metod domain objektů ve view dochází k interakci se zdrojem dat. Tím se pak vlastně obchází controller a je přímá vazba modelu a view.
V PHP se dají DTO často nahradit asociativním polem, zejména při použití jako prostředník mezi controller a view, ale i tak se mi zdálo, že je to nějak moc kódu a dost zbytečné, nakonec jsem se také rozhodl pro použití domain třídy přímo ve view - samozřejmě prostřednicvím controlleru.
Model v MVC a Zend Framework.
Co nám nabízí Zend Framework pro práci s našimi domain objekty? Zend Framework na to jde šalamounsky. View a controller jsou celkem v pohodě. Zend Framework je má dobře podchyceny, s modelem to ale tak není. Existují třídy Zend_Db_Table a Zend_Db_Table_Row. Podle dokumentace jde o implementaci Table Data Gateway a Row Data Gateway patternů. Zkusil jsem je tedy použít pro model. Když jsem to dopsal, výsledek se mi moc nelíbil. Třeba controller metoda pro zobrazení formuláře k editaci článku a jeho členství v kategorii obsahuje tento kód:
public function editEntryAction()
{
//Article,Author and Category classes are descendants of the Zend_Db_Table,
//these are our domain classes
$article=new Article();
$author=new Author();
$currentArticle=$article->find( $this->_getParam("itemId") )->current();
$articleCategories=$currentArticle->findManyToManyRowset('Category', 'CategoryArticle');
//exposes data from domain objects into view
$this->view->article=$currentArticle;
$this->view->authors=$author->fetchAll();
$this->view->allCategories=$category->fetchAll();
$this->view->articleCategories=$articleCategories;
}
Ve view pak zobrazujeme data z domain objektů ve formuláři:
<form id="blogEntryForm" method="post" action="/blog/admin/updateEntryToDb">
<div>Title</div>
<input id="title" name="title" value="<?php echo $this->article->title?>" />
<div>Assigned categories</div>
<select id="articleCategories" name="articleCategories[]" multiple="multiple" size="10">
<?php foreach($this->articleCategories as $category):?>
<option value="<?php echo $category->id?>"><?php echo $category->name?></option>
<?php endforeach;?>
</select>
<div>
<input type="submit" />
<input type="reset" value="Reset" />
</div>
<input type="hidden" name="entryId" value="<?php echo $this->article->id?>" />
</form>
Nelíbila se mi závislost view na pojmenování sloupců v databázi (dá se to vyřešit inflexí v Zend_Db_Table_Row) a hlavně posléze se mi začalo zdát, že Zend_Db_Table není ORM, je to prostě dobrá objektová abstrakce tabulky s relacemi, více a více mi připadala jako výborné DAO a volat DAO v controlleru se mi nezdálo.
Proto jsem se nakonec odhodlal k tomu předělat všechy své domain objekty a použít v podstatě obyčejné PHP objekty s DAO třídami založenými na Zend_Db_Table, jak o tom píše i Jakub Podhorský ve fóru. Získal jsem tím to, že veškeré operace s daty jsou v domain třídách, mám tedy nezávislé view a controller na tom, co je zdroj dat. Odsunul jsem tedy DAO za domain objekty. Nevýhoda je v tom, že domain třídy jsou dost často jen proxy pro DAO. Controller metoda pro hledání článku:
$articleTitleUrl=$this->_getParam("entry");
$currentArticle = new Article();
$found=$currentArticle->findByTitleUrl($articleTitleUrl);
//false - article not found
if (!$found) {
...
}
...
Metoda findByTitleUrl
domain objektu Article je v podstatě proxy metody se stejným názvem
DAO třídy založené na Zend_Db_Table.
public function findByTitleUrl($titleUrl)
{
$row=$this->_dao->findByTitleUrl($titleUrl);
if (is_null($row)) {
return false;
} else {
$this->_setFromDaoRow($row->toArray());
return true;
}
}
Zase tak dochází ke zbytečnému psaní a duplikaci, možným řešením asi bude použít metody DAO tříd přímo v domain třídách:
public function findByTitleUrl($titleUrl)
{
$condition=$this->_dao->getAdapter()->quoteInto('titleURL = ?',$titleUrl);
$row=$this->_dao->fetchRow($condition);
if (is_null($row)) {
return false;
} else {
$this->_setFromDaoRow($row->toArray());
return true;
}
}
I tak je to dost práce navíc, časem se možná vrátím k variantě domain tříd jako potomků Zend_Db_Table s případnými vlastními row objekty. Jak řešíte model v Zend Frameworku vy? Používáte Zend_Db_Table jako DAO nebo jako domain objekty?
Komentáře (7)
Já když jsem migroval na ZF tak jsem měl používané třídy, které zapouzdřovaly operace nad databází pro business logiku jako Table_Row. Tedy jedna instance zapouzdřuje jeden řádek v tabulce. Parametr konstruktoru je právě primárním klíčem a pro každý atribut jsou deklarovány gettery.
Není to sice pravé ořechové, ale taky to funguje...
Jedinou výhradu mám k tomu, že nemám rád, když se třída, která zapouzdřuje databázovou tabulku jmenuje v jednotném čísle. Tj. Místo Author by se měla jmenovat Authors. Instance třídy Author by měla zapouzdřovat konkrétní řádek v tabulce.
Tohle celé okolo ORM je hotová věda. Do podrobna to řeší propel nebo PHP doctrine. Něco málo o ORM padlo na PHP konferenci
http://openmeeting.biz/dokumenty/videozaznam-ze-schuzky-php-profesionalne-21-8-2007-v-praze/
Tahle problemtatika si zašlouží mnohem větší pozornost
mně se hodně líbí jak to řeší vavru čili pomocí servisních tříd která obsahuje instanci Zend_Db_Table a pak jednotlivé metody který řeší jednotlivý metody nad modelem...v controlleru jsem tak od modelu odstíněnej action metody nejsou zbytečně velký ale zase se nafukuje kód v těch servisních třídách :)
jinak pěknej článek ještě doporučuje k přečtení http://www.phpguru.cz/clanky/model-neni-pouze-databaze
[1] - s těmi názvy tříd odvozených od Zend_Db_Table máš pravdu, já jsem to tak nechal, protože i názvy tabulek v DB mám v jednotném čísle. Jinak jsem ale spíš příznivcem
Vlastně i to byl jeden důvod proč jsem udělal model třídy jako samostatné a ne poděděné od Zend_Db_Table[2] - princip servisních tříd jsem u sebe rovnou přesunul do doménových tříd, jinak se moc neliším od toho, jak to má V. Vavru.
Článek na www.phpguru.cz je pěkný.
Asi platí, že žádná cesta není úplně špatná, v praxi často není potřeba mít tak dokonalou izolaci M-V-C a robustní model, záleží na konkrétní situaci a použití.
Co se píše v knize Iljy Kravala "Základy objektově orientovaného programování" http://www.objects.cz/ ?
Objekt jako reprezentace nějaké entity by měl mít 3 vrstvy. Aplikační (business), prezentační a datovou.
Aplikační část řeší práci s daty toho objektu. Prezentační vrstva řeší, jak se mají data zobrazit. A datová, jak data získat a uložit z/do datového úložiště.
Nenechte se mýlit, to není MVC vzor. To je obecné rozvrstvení objektu jako entity v programování.
Tyto tři vrstvy mohou být implementovány třemi objekty. Pak když se změní úložiště dat, tak nemusíme přepisovat aplikační ani prezentační logiku. Když se změní zobrazení dat, tak zase nemusíme měnit ty další dvě vrstvy.
Pak tam psal, že nejlépe pokud jsou objekty prezentační a datové vrstvy navázány do aplikační jako atributy.
Takže ve webové aplikaci bych si to představil asi takto.
Máme model, což je objekt představující naší enitu. Ta má svoje atributy + 2 atributy s datovým a prezentačním objektem. Datový objekt by mohl být ručně psaný objekt přímo s metodami, kde budou raw selecty. Lepší by bylo asi nějaké DAL, ještě lepší ORM.
V prezentačním objektu pak různé gettery na data. Třeba do html by se data escapovala apod.
V Controlleru můžeme tedy volat metody nad naším "aplikačním" objektem. Ten pokud data ve svých atributech nemá, tak si je naplní z datové vrstvy.
No a pak ze šablony (View) bysme měli volat gettery prezentační vrstvy našeho objektu.
Ano teoreticky by se dal nechat probublat požadavek na data ze šablony až do datové vrstvy našeho objektu. Ale pak by to chtělo mít ošetřeny chyby vzniknuvší "uprostřed šablon". Ale pro větší srozumitelnost kódu co chceme na stránce zobrazit, bych to stejně nechal v Controlleru.
No a pak by to chtělo jen implementovat použití komponent v Controllerech a Layoutů ve View, abysme nemuseli podobné kusy kódu psát stále dokolečka.
Větší smysl tento nastíněný přístup 3 vrstevné architektury každého objektu zřejmě měl v desktopovém programování, kdy byla jedna hlavní funkce main(), data z databáze se netahala při každém "requestu" a prezentační část se také nepřekreslovala stále.
Možná, ale že se k takovémuto přístupu webové aplikace dostanou, protože už neřeší jen jednoduché úkoly, ale složité aplikace.
V podstatě s Vámi souhlasím, až na navázání "prezentačních" atributů přímo v aplikačních (doménových) objektech, to se mi nelíbí, přece jen mám raději když je prezentační vrstva naprosto oddělená a nezávislá, usnadňuje to znovupoužitelnost. Jsem zastáncem toho, že aplikační objekty žádnou prezentační vrstvu nepotřebují a nemají mít o ní ani tucha. Např. escapování dělat až ve view, které bude escapovat výsledky gettrů od aplikačních objektů.
Že rozvrstvení aplikace nerovná se MVC vzor chápu a snad je to jasné i z článku, MVC je jedním ze způsobů, jak toto rozvrstvení implementovat a právě v této implementaci bych způsob "prezentačních" atributů nepoužil.
Komponenty určitě ano, to bude trend, viz dagi. Komponenty mohou dost zjednodušit práci s view.
[5]
Že rozvrstvení aplikace nerovná se MVC vzor chápu a snad je to jasné i z článku, MVC je jedním ze způsobů, jak toto rozvrstvení implementovat...
To je právě ta ironie. MVC nám říká, že view komunikuje přímo s modelem. Tedy nikoliv *View by měl tvořit systém šablon, do kterých jsou v controller objektech předávána data z domain tříd.*, ale view si pro data šáhne přímo z modelu, ke kontroleru nemá vůbec přístup.
Co to znamená v praxi? Podoba modelu je podřízena chování prezentační vrsty. Pokud budu chtít v MVC implementovat vkládání komentářů, jako funguje zde na webu s náhledem, budu muset podporu pro náhledy přidat i do modelu. Protože po stisknutí tlačítka "náhled" uloží kontroler obsah formuláře někam do modelu, aby si jej od tama přečetl view.
Tohle je přece velmi neefektivní. Proč tedy trvat na MVC vzoru?
[6] - Já si právě nejsem jistý, zda MVC říká, že view komunikuje přímo s modelem, často se to tak dělá, pro větší separaci je ale možné použít nějaký "transfer" a pak skutečně funguje to, že View je systém šablon, do kterých jsou v controller objektech předávána data z domain tříd. Náhled komentářů jsem proto udělal právě takto.
Pokud se odešle formulář tak controller zavolá view, do kterého předá text z formuláře, view pak provede rendering a výsledek vrátí, s modelem tedy nekomunikuje, pouze využívá statickou metodu modelu pro zpracovani textu, která by ale klidně mohla být součástí controlleru, nebo něčeho jiného.
MVC bude určitě mít mnoho způsobů použití a implementace, které se budou někdy i dost odlišovat. Určitě se nehodí na všechno, ale evidentně se to u web aplikací docela chytlo, dokonce i Microsoft už pracuje na ASP.NET MVC.
Komentáře jsou uzavřeny.