De perfecte opbouw van een webapplicatie
Begin dit jaar schreef Tri Pham een interessant artikel op Scriptorama over de perfecte webdeveloper. Zijn conclusie was simpel maar doeltreffend:
De perfecte web developer bestaat misschien niet, maar er naar streven doet geen vlieg kwaad.
Een manier om hier naar te streven is voor mij een goede opbouw van een applicatie. De laatste tijd heb ik verschillende verzoeken ontvangen om te adviseren bij het ontwikkelen van een webapplicatie. Omdat het hier niet om kleine websites gaat, is de opbouw van groot belang. In dit artikel ga ik proberen de perfecte web developer uit te hangen en mijn mening geven over de perfecte opbouw van een webapplicatie.
Dit artikel is eerder geplaatst op EdwinV.nl.
A key thing about patterns is that you can never just apply the solution blindly, which is why pattern tools have been such miserable failures. I like to say that patterns are "half baked," meaning that you always have to finish them off in the oven of your own project. Every time I use a pattern I tweak it a little here and a little there. You see the same solution many times over, but it's never exactly the same.
De bovenstaande uitspraak komt uit het boek Patterns of Enterprise Application Architecture van Martin Fowler. In dit artikel zullen veel design patterns aan bod komen, patterns die ik zelf handig vind om te gebruiken. Luister naar Fowler, blijf altijd nadenken over de opbouw van je eigen applicatie!
De opbouw van webapplicaties is meestal in grote lijnen met elkaar te vergelijken. Je hebt te maken met de weergave van de applicatie in een webbrowser. Daarnaast zal er een bepaalde gegevensbron zijn, bijvoorbeeld in de vorm van een database. Het laatste element is de koppeling tussen de weergave en de database. Dit element zal de aanvraag van een client omzetten naar een weergave van gegevens uit de database.
De drie bovenstaande elementen zijn beter bekend als Model, View en Controller. Het MVC pattern is in mijn ogen daarom een goede basis voor een webapplicatie.
Model
Het model van een webapplicatie bestaat uit de koppeling tussen de applicatie en een gegevensbron. In veel gevallen hebben hier te maken met een database. De gegevens in een database bestaan uit verschillende entiteiten die inhoud kunnen bevatten. In verschillende projecten in het verleden heb ik ervoor gekozen om voor elke tabel een klasse aan te maken die er globaal als volgt uit ziet:
-
class PageModel extends Model {
-
-
public function getPages(){
-
return $this->query("SELECT * FROM pages");
-
}
-
-
public function getPage($iId){
-
return $this->query("SELECT * FROM pages WHERE id = ".$iId);
-
}
-
-
public function addPage($sName, $sLocation, $bPublished){
-
return $this->query("INSERT INTO ...");
-
}
-
-
public function editPage($iId, $sName, $sLocation, $bPublished){
-
return $this->query("UPDATE pages SET ...");
-
}
-
-
public function deletePage($iId){
-
return $this->query("DELETE FROM ...");
-
}
-
-
}
De methoden werden meestal aangevuld met controles op de gegeven parameters. De Model klasse bevatte alle functionaliteit voor de koppeling met de database. Vaak gaf de methode 'query' al associatieve arrays terug met gegevens zodat het in de applicatie verder niet nodig was om te werken met mysql_* functies. Het voordeel hiervan was ook de uitwisselbaarheid met andere database typen, zoals PostgreSQL.
De bovenstaande opzet van een model klasse werkt, alleen doe je regelmatig dubbel werk. De queries in de verschillende klassen zullen voor het grootste gedeelte gelijk zijn. Daarnaast mis je veel flexibiliteit. Zodra je in een controller of view een andere sortering van de gegevens wilt dien je gelijk aanpassingen te maken in de model klasse. Er is dus nog veel ruimte voor automatisering van een model klasse.
Dat laatste is iets waar ik veel belang aan ben gaan hechten. Dubbel werk moet niet nodig zijn als je een goede opbouw van je applicatie hebt, DRY(Don't Repeat Yourself) dus. Helemaal als je gebruik gaat maken van object-georiënteerd programmeren. Versie 5 van PHP biedt de hiervoor voldoende mogelijkheden, al ben ik zelf meer een Ruby aanhanger aan het worden. Ruby is zelf een native OOP taal waardoor er veel meer mogelijk is op dat gebied.
Een goed design pattern voor het model is in mijn ogen ActiveRecord. Binnen verschillende frameworks wordt dit pattern toegepast en mijn ervaring is dat het geweldig werkt. Het biedt alle mogelijkheden om DRY te werken, maar je kunt altijd nog zelf je queries gaan schrijven als het nodig is.
Een simpel voorbeeld:
-
class Person extends ActiveRecord {
-
}
-
-
// SELECT * FROM persons WHERE id = 1
-
$person = Person::find(1);
-
-
// UPDATE persons SET name = 'Edwin V.' WHERE id = 1
-
$person->name = "Edwin V.";
-
$person->save();
Zoals je ziet is het overbodig om queries te schrijven, ActiveRecord stelt zelf de queries samen. Het hangt van de implementatie van ActiveRecord af wat allemaal mogelijk is. De implementatie binnen Ruby on Rails biedt bijvoorbeeld functionaliteit voor associaties en gegevens validatie. Het Zend Framework biedt helaas geen implementatie van ActiveRecord aan, maar wel andere handige database klassen.
Een interessant overzicht van object-relational mapping implementaties in PHP is te vinden op de PHP wiki.
View
Een net zo belangrijk onderdeel van een applicatie is de weergave richting de gebruiker. Bij een webapplicatie gebeurd dat meestal in een browser, dus de weergave bestaat uit HTML en de bijbehorende afbeeldingen, JavaScripts en stylesheets. Daarnaast is het natuurlijk mogelijk om XML als weergave te hebben, voor bijvoorbeeld gebruik in een andere applicatie of als RSS feed.
Een laatste - redelijk nieuwe - techniek is JavaScript. In dat geval zal als reactie op een Ajax request vanuit de browser een stukje JavaScript terug gezonden worden om uitgevoerd te worden in de browser. Deze manier van werken wordt de laatste tijd meer toegepast door de introductie van RJS templates in Rails. Een korte introductie van RJS is te vinden op de weblog van Cody Fauser.
De belangrijkste taak van de view is dus de gegevens uit de controller omzetten naar een passend formaat. Hiervoor kan je vaak gebruik maken van een template engine zoals Smarty of Yapter. Mijn ervaring is dat template engines meestal overbodig zijn omdat PHP zelf al een perfecte template engine is.
-
<ul>
-
<?php foreach ($items as $item) { ?>
-
<li><?php echo $item; ?></li>
-
<?php } ?>
-
-
</ul>
Het bovenste voorbeeld zou uit een template engine kunnen komen. Het doet precies wat je wilt, maar absoluut niet meer. Het onderste voorbeeld is dezelfde code in PHP. Als nadeel kan je het aantal tekens noemen, maar dat weegt zeker op tegen de voordelen. Je kunt namelijk gewoon alle functionaliteit gebruiken die PHP te bieden heeft.
Een goede implementatie van een View is die van het Zend Framework. Hierbij ken je in de controller waarden toe aan variabelen in het view object. Deze variabelen kan je vervolgens weer gebruiken in je template.
Binnen templates kan het handig zijn om met helpers te werken. Dit principe kennen we onder andere van Rails en zit wederom in het Zend Framework. Helpers bevatten functies die veel gebruikte HTML tags genereren. Ze kunnen bijvoorbeeld helpen bij het samenstellen van een formulier voor een bepaald object.
Controller
De taak van de controller klassen is een request van een gebruiker omzetten naar een bepaalde weergave van de staat van het systeem. Een request van een gebruiker bestaat meestal uit een URL met daarin bepaalde informatie over de op te vragen gegevens. Een veel gebruikte opzet bestaat uit:
- Controller: De entiteit die opgevraagd wordt, bijvoorbeeld 'person'
- Actie: De actie die uitgevoerd wordt op de entiteit, bijvoorbeeld 'show'
- ID: De unieke identifier naar de entiteit, bijvoorbeeld '1'.
Een aanvraag kan er dus uit zien als 'index.php?controller=person&action=show&id=1', maar vaak wordt gebruik gemaakt van nette URL's en ziet de URL er zo uit: '/person/show/1'. Voor elke entiteit bestaat vervolgens een controller:
-
class PersonController extends ApplicationController
-
{
-
public function showAction(){
-
// Show Person with given ID
-
}
-
}
Een controller bevat voor elke mogelijke actie een methode. In het verleden heb ik wel gewerkt met speciale klassen voor elke actie, maar mijn ervaring is dat dit erg onoverzichtelijk is. Het is aan te raden om voor elke actie een methode te gebruiken.
Het omzetten van de URL naar een aanroep van een methode in een controller gaat meestal via routing. De implementatie van controllers in het Zend Framework bevat een RewriteRouter waarmee dit makkelijk te realiseren is. Zodra het routing gedaan is kan de methode de benodigde acties uitvoeren. Een voorbeeld van een controller met twee acties is:
-
class PersonController extends ApplicationController {
-
-
public function showAction(){
-
$view = new View();
-
$view->person = Person::find($this->params['id']);
-
return $view->parse('person/show.tpl');
-
}
-
-
public function editAction(){
-
if($this->request->isPostMethod()){
-
$person = Person::find($this->params['id']);
-
$person->name = $this->params['person']['name'];
-
$person->save();
-
return $this->redirect('person', 'show', $person->id);
-
} else {
-
$view = new View();
-
$view->person = Person::find($this->params['id']);
-
return $view->parse('person/edit.tpl');
-
}
-
}
-
-
}
Conclusie
Zoals uit het artikel blijkt ben ik zelf een grote fan van Ruby on Rails en het Zend Framework. Het Rails framework geeft webdevelopers een kant-en-klare oplossing voor de opbouw van een webapplicatie. Zowel het MVC pattern als ActiveRecords is hierin terug te vinden, beide met een zeer goede implementatie.
Het Zend Framework is helaas nog in een beta status en dwingt niet de opbouw van een applicatie af. Ze geven echter wel een goede basis met verschillende klassen ter ondersteuning van MVC en object-relation mapping. Hoewel ik de laatste tijd veel werk met Ruby, zou ik het framework zeker gebruiken als basis voor een PHP applicatie.
De genoemde opbouw van een applicatie is in mijn ogen een manier om DRY(Don't Repeat Yourself) te werken en toch voldoende flexibiliteit te hebben. De methode heeft al meerdere keren goede dienst bewezen bij mij. En om net zo'n pakkende conclusie als Tri Pham te geven:
Een perfecte opbouw van een webapplicatie bestaat niet, maar er naar streven doet geen vlieg kwaad.
Volg Scriptorama via RSS!
Reageer ook!
Een leuk verhaal, hoewel het onderwerp misschien wat uitgemolken is.
Zelf heb ik altijd wat moeite gehad met de controllers: het begin zag er leuk uit, maar op den duur begon het steeds meer te lijken op procedurele code in een OO jasje.
Inmiddels experimenteer ik (na een tip van Tri) met CodeIgniter. Dit framework is een echte aanrader voor eenieder die bekend wil raken met MVC, of uberhaupt een goed PHP framework zoekt om mee te werken.
Door Ruud
op 09.05.06 @ 10:41 am | Permalink
Toevallig heb ook ik een tip ontvangen voor CodeIgniter. Ik heb schijnbaar meer tijd gehad om te expirimenteren, want ik ben inmiddels al aan het ontwikkelen op CI.
CI heeft zeker voordelen, het is erg goed doordacht en je verminderd het probleem van procedurele code in je controllers, al is dat niet helemaal te voorkomen lijkt het.
Door berry__
op 09.05.06 @ 1:22 pm | Permalink
"CI heeft zeker voordelen, het is erg goed doordacht en je verminderd het probleem van procedurele code in je controllers, al is dat niet helemaal te voorkomen lijkt het."
Als ik de screencasts en manual zo bekijk, lijkt CI qua opbouw sprekend op Rails, Zend Framework of CakePHP. Op welke manier zorgt CI ervoor dat je minder procedurele code in je controllers hebt staan dan de genoemde frameworks?
Door Edwin V.
op 09.05.06 @ 3:32 pm | Permalink
Je code komt me griezelig bekend voor :)
Over CodeIgniter: ik gebruik 't niet om twee redenen:
- routing is erg beperkt: geen regexps en defaults per URI component (zie bijv. http://routes.groovie.org/manual.html), twee types wildcards is wel erg summier
- geen support voor verschillende omgevingen: je kunt niet makkelijk switchen tussen ontwikkeling, test en productie. Je zult ongetwijfeld wat logica in je config kunnen prutsen (met bijv. $_SERVER['HTTP_HOST']), maar ik heb 't graag ingebouwd.
Maar misschien heb ik te snel geoordeeld en is er meer mogelijk...
Door Michel
op 09.05.06 @ 3:56 pm | Permalink
@Edwin V.
> Als ik de screencasts en manual zo bekijk, lijkt CI qua opbouw sprekend
> op Rails, Zend Framework of CakePHP. Op welke manier zorgt CI ervoor dat
> je minder procedurele code in je controllers hebt staan dan de genoemde
> frameworks?
Niet. Dat beweerde ik ook niet, ik had het over MVC zonder framework. Immers,
voor het MVC pattern heb je geen framework nodig (het maakt het af en toe alleen handiger).
Het feit is dat ik erg veel if en elsejes had en CI heeft dit sterk verminderd. En een voordeel
van CI t.o.v. ZF en Cake vind ik de duidelijke documentatie en de lage drempel.
@Michel
> routing is erg beperkt
Klopt, overigens ben ik nog niet echt tegen probleemsituaties aangelopen.
> geen support voor verschillende omgevingen
Nee, daar heb je gelijk in. Dat vindt ik ook een manco van CI.
> Je zult ongetwijfeld wat logica in je config kunnen prutsen
Uh-huh :)
Door berry__
op 09.05.06 @ 8:21 pm | Permalink
Waarom ik anderen heb getipt over CI over CakePHP en Zend Framework is simpel. Ten eerste zit er in CakePHP een mega design fout. Je moet in elke controller en model var $name = "model|controller_naam"; zetten. Dit is gewoon heel slecht.
Over ZF ben ik weer niet blij met de helpers, deze zitten gewoon in een OO jasje. Naar mijn mening moeten deze helpers in de globale namespace zitten. Dit is wat Rails en CI dus wel goed doen. Ook kent ZF niet zoveel helpers en is het nog lang niet stable wat CI wel is. Ik wil niet zeggen dat ZF slecht is, integendeel. Je moet alleen nog lang wachten op 1.0 (nog een jaar?). Alleen voor PHP coders die nu al een framework willen gebruiken en niet kunnen wachten op ZF, kunnen CI gebruiken.
Door Tri Pham
op 09.05.06 @ 11:48 pm | Permalink
"Als ik de screencasts en manual zo bekijk, lijkt CI qua opbouw sprekend op Rails, Zend Framework of CakePHP."
Natuurlijk. Bijna alle frameworks zijn geinspireerd door Rails.
"Op welke manier zorgt CI ervoor dat je minder procedurele code in je controllers hebt staan"
Dat heef niet zozeer met CI te maken, maar meer met mijn eigen beleving. Het frameworkje dat ik eens gemaakt heb liet te veel ruimte in de controllers. CI heeft me voornamelijk geholpen met de juiste inzichten.
Door Ruud
op 09.06.06 @ 6:43 am | Permalink
Tri:
> Ten eerste zit er in CakePHP een mega design
> fout. Je moet in elke controller en model var
> $name = "model|controller_naam"; zetten. Dit
> is gewoon heel slecht.
Lijkt me een bewuste keus om case-sensitive class names te definiƫren in PHP4. In PHP5 is dit uiteraard niet nodig, daar hadden ze een check voor kunnen inbouwen.
Tri:
> Naar mijn mening moeten deze helpers in de
> globale namespace zitten. Dit is wat Rails en
> CI dus wel goed doen.
In Rails zijn helpers niet global, het zijn 'mixed-in' methods van helper classes.
Dat is ook zeer aan te raden om de global namespace niet te vervuilen en naamgeving-conflicten te voorkomen. Het grootste argument voor helper methods is echter de scope van je view. Op deze manier kun je bijv. view variabelen koppelen aan form elementen (data binding).
In PHP zijn mixins niet fatsoenlijk mogelijk.
Struikelblok is syntax: in PHP typ je je helemaal lens met $this->, Ruby gaat uit van een impliciete self (dat had van mij in PHP ook gemogen ;)).
Je kunt natuurlijk in PHP je template code laten herschrijven maar dan kom je al snel in de knoop met bestaande symbolen en hun betekenis/functie.
Door Michel
op 09.06.06 @ 10:19 am | Permalink
Mijn verwoording is een beetje ongelukkig, nog een keer :)
Naar mijn mening moeten de helpers ook buiten de view namespace zitten, dus ook in controllers (global is iets te grof verwoord). In Rails kan dat heel goed, in de controller kan je aangeven welke helpers je wilt gebruiken (helper :mijn_helper) of inpluggen in application_helper.rb.
Ik begrijp dat PHP hier moeite mee heeft, maar in CI vind ik de helpers wel nuttig in global namespace. Dat er collions kunt krijgen, vind ik geen probleem. Je gebruikt immers een framework en je mag van mij daar eenmaal rekening mee houden.
Door Tri Pham
op 09.06.06 @ 11:48 pm | Permalink
Tri:
> Naar mijn mening moeten de helpers ook buiten de
> view namespace zitten
Dat kan zeker nuttig zijn. Als objecten kun je ze natuurlijk in de scope van elk object 'inpluggen'.
> Dat er collions kunt krijgen, vind ik geen
> probleem.
Prima, dat hoeft ook niet in elke situatie een punt te zijn (of een framework nuttig moet zijn in de meest uiteenlopende situaties is weer een andere discussie ;). In de gemiddelde PHP installatie leven er immers al snel meer dan 1000 functies vrolijk naast elkaar ;)
Als je geen koppelingen wilt met je view of controller wenst dan kunnen PHP functies een prima oplossing zijn. Een framework beperkt zich echter nogal met deze keus vind ik.
Door Michel
op 09.07.06 @ 1:43 pm | Permalink
Line and paragraph breaks automatic, e-mail address never displayed, HTML allowed:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>