Hallo allemaal,

Een tijdje geleden ben ik bij het werk wat ik doe bij iemand op een site een MVC pattern tegengekomen. Aangezien ik toen nog niet veel werkte met mvc's was ik erg onder de indruk van de werking ervan. Ik heb een tijdje gezocht naar wat tutorials over hoe je een dergelijk systeem op zou moeten zetten, ik vond deze tutorial die mijnsinziens erg handig was: http://www.onlamp.com/pub/a/php/2005/09/15/mvc_intro.html.

Nadat ik me redelijk ingelezen had in de werking van OOP, ben ik begonnen met het bedenken van een eigen framework. Inmiddels is dit al een tijdje af, en werkt het voor 90% precies zoals ik zou willen. Toch ik heb ik het idee dat ik het niet helemaal 'the right way' doe. Ik vraag me af, omdat ik nu aan een nieuw project ben begonnen of het niet simpeler en efficienter kan.

Bij werkt het nu ongeveer zo:

Bij een request wordt alles naar index.php geredirect, dat is mijn controller. Hierin bevint zich een __autoload functie. Mbv die functie wordt de gewenste file geinclude en een class geladen. Deze class extend weer een class, en die misschien ook. En op die manier wordt door telkens de parent class constructor te callen de database connection en authentication classes geladen. Daarna wordt binnen die gerequeste klasse het event gecalled uit de request (anders het default event (__default)).

Daarna geeft de controller het hele object van die class mee aan de class presenter die dan vervolgens een layout required waar op diverse plaatsen templates in worden geladen. dit wordt allemaal teruggegeven aan de client.

Het is misschien wat vaag, maar het verschilt in iedergeval een hoop met een normaal MVC patroon. Zo heb ik bijvoorbeeld geen getters en setters, geen singleton, en dus eigenlijk geen models. Kortom ik zit een beetje vast in de ontwikkeling ervan. Eigenlijk werkt het prima, maar het voelt niet echt goed aan omdat ik weet dat het niet the way to go is.

Ik kan me goed voorstellen dat het niet helemaal duidelijk is voor iedereen, dus mochten jullie meer willen weten, of willen jullie bijvoorbeeld de controller zien, dan zet ik het er graag op.
Heb je een voorbeeld van hoe je nu een pagina, bijvoorbeeld een lijst met posts (in een forum-applicatie) of een lijst met artikelen in je winkelmandje (in een webshop-applicatie) zou oplossen in je huidige opzet?

Verder klinkt het alsof je 'extend' niet helemaal goed gebruikt. Klopt het dat je voor een klasse die gebruik maakt van de database de database-klasse extend? Zo ja, hoe ga je dat dan oplossen wanneer je en database-functionaliteit nodig hebt, en de functionaliteit van een andere klasse, bijvoorbeeld een event-handler? Daarnaast ben je gewoon PHP aan het nabouwen, want uiteindelijk wordt mysql_connect & fsockopen gewoon $this->connect en $this->socketOpen. Het enige verschil is dat je er $this voor hebt staan. Je bent nog steeds zonder een echte context aan het programmeren (want wat als je bijvoorbeeld 2 socket-verbindingen nodig hebt, tegelijkertijd?) Daarom alleen extenden als "de nieuwe klasse is een overgeërfde klasse" waar is. Verder gewoon compositie gebruiken, wat dus inhoudt dat je instanties aanmaakt en de objecten aanroept wanneer je ze nodig hebt. En daar kan je goed Singleton of een Registery Pattern gebruiken.
Ik denk dat dat inderdaad een beetje het probleem is wat ik heb.

Op de vraag hoe ik dat nu op zou lossen, kijk ik even naar het winkel mandje. Want dat gebruik ik nu ook echt.

Ik maak de functie / methode winkelmand, in de klasse shop aan shop extend op dit moment de klasse database, en database extend de klasse base. in de base klasse zitten allerlei handige functies, en daarin worden bijvoorbeeld ook de verschillende default waarden in vastgelegd.

in de methode winkelmand selecteer ik alle producten op die in de winkelmand zijn geplaatst op. En hun verdere gegevens mbv. sql statements. ik zet de variabele $this->page['template'] op templates/winkelmand.php. Het hele object word aan de present class meegegeven, die zet de juiste template in de layout, waarna a.d.h.v. placeholders de juiste waarden ingevuld worden.
En hoe zou je nu een lijst met artikelen weergeven die een persoon kan kopen? Dan moet je weer zo goed dezelfde query in een method van Shop zetten. Op een gegeven moment, als je shop wat uitgebreider wordt, heb je een boel queries die een hele hoop op elkaar lijken. En waarschijnlijk niet alleen queries, maar ook andere stukken code zullen algemeen goed worden.

Het is mogelijk om MVC op zo'n manier toe te passen dat je in principe maar op een paar plekken een query hoeft neer te zetten (in je method die je model opslaat, en in de method die je models uit de database haalt) waarna je dus veel simpeler aanpassingen aan je datamodel kan doen. Dat is het idee van MVC: Je scheidt de onderdelen in hun eigen lagen. De lagen kan je individueel weer hergebruiken.

Dus stel dat je jouw Shop implementatie zou herschrijven:
We houden de klasse Shop, dat wordt de controller. De controller zoekt data bij elkaar, manipuleert deze eventueel, en zorgt ervoor dat er een view komt.

In je shop hanteer je artikelen, items. Je neemt dus een klasse Item, welke een artikel representeert. Dus properties als $name, $price etc. Daarnaast maak je een method die de artikelen uit de database haalt. Je kan een static method in Item daarvoor nemen, of je maakt een Factory (zie [google]Factory Pattern[/google]) om dat te doen. Uiteindelijk komt het op hetzelfde neer. Deze method doet niets meer dan een query uitvoeren om je items op te halen (je kan eventueel argumenten & parameters gebruiken om je query uit te breiden) en de resultaten om te zetten in een array met instanties van Item. En die array geeft hij terug.

Nu kan je in Shop zo goed als al je database-queries al weg doen, en vervangen door aanroepen naar de method/Factory. Als we het opslaan van Items, artikelen even negeren, heb je nu als het goed is geen database meer nodig in je Shop. Nu kan je die extend dus weg doen.

Het fetchen van die instanties van Item heeft daarentegen wel een database nodig. Maar is een instantie van Item een Database (de "is een" test) Nee, dus niet extenden. Je maakt je database daarentegen singleton, zodat je overal vandaan de instantie, en dus de verbinding op kan pikken. (zie dit topic voor een voorbeeld van hoe je gemakkelijk Singleton kan implementeren) Waneer je nu je artikelen nodig hebt roep je je factory aan, en in de factory vraag je om een instantie van de database klasse.

Views kan je op jouw manier doen, ikzelf laat dat ook door de controller regelen.

Ik heb 1 MainController. Deze wordt altijd aangeroepen, en is als het ware mijn index.php. Vanuit hier kijk ik welke Controller er nodig is, en die laadt ik in en roep ik aan. Van die controller, ik noem hem even subcontroller, krijg ik een instantie van een View terug. Deze plak ik dan als het ware in mijn MainView van de MainController, en als laatste roep ik 'draw' aan op die instantie van mijn MainView. Daarbij tekent hij meteen de subview. Echter, voordat ik MainView::draw aanroep, vraag ik eerst aan mijn subview of hij wel in een context getekend kan worden, of dat hij al compleet is. Denk bijvoorbeeld aan een plaatje; die binaire data kan je niet mixen met html.

Op deze manier kan ik de controller laten bepalen wat voor soort view nodig is, en kan ik MVC zonder aanpassingen zelfs op plaatjes of tekstbestanden, of wat dan ook toepassen.

Nog een voordeel van controllers die zelf views teruggeven is dat ik als het ware vanuit subcontroller a subcontroller b kan aanroepen. Subcontroller b geeft subview b terug, en die hang ik weer aan subview a. Subview a gaat weer terug naar de MainController, en komt terecht in mainview a. Mainview a wordt getekend, en zo worden alle subviews ook meteen erbij betrokken. Ik kan controllers op elkaar stapelen, en stukjes nog abstracter maken. Stel dat ik bijvoorbeeld, net als op PHPhulp, linksonder wil weergeven hoeveel mensen er online zijn in een forum, dan kan ik hier een aparte controller voor maken, en een klein viewtje (die alleen verantwoordelijk is voor dat kleine stukje, die ene div) en die kleine controller kan ik dan vanuit grotere subcontrollers, zoals TopicController, aanroepen. Of ik kan een snelreageren blok opnemen in de pagina, door PostController::addPost(Topic $x) -> view met dit formuliertje op te nemen in TopicController::showTopic($x). Echter kan ik PostController::addPost(Topic $x) ook nog los aanroepen, en krijg ik alleen dit formuliertje. Bijvoorbeeld zoals waneer je hier op PHPhulp op het quote-knopje drukt. Maar ik hoef maar 1 formuliertje te maken. Ik hoef zelfs maar 1 plek te maken waar hij dit formuliertje afhandelt, namelijk PostController::addPost.

Waarom zijn mijn posts altijd zo lang...
Ok, ik begin langzamerhand het idee een beetje te begrijpen.

In de eerste plaats het singleton gebeuren. Begrijp ik het goed als ik denk dat deze manier van werken ervoor zorgt dat je overal, met dezelfde methode nl. (instance) de class aan kan roepen, maar dat hij slechts een keer geconstruct wordt. Je kan niet new gebruiken, omdat de construct private is.

Nu even over jou MainController, je request gaat hier dus heen en dit is waarschijnlijk ook een class. Daarna wordt op basis van de request de juiste subcontroller geladen. En daarin worden alle bewerkigen gedaan. Het model zorgt voor de data. Als die klaar zijn zorgt je MainController dat de pagina gevuld word. (ik hoop dat het hiet nog goed gaat)

Nu een vraag.Wat als je bij jou een set Items wilt bewerken? Gebruik je dan telkens een nieuwe instantie van Item? Ik gebruik namelijk voor sorteringen vnl SQL zelf, lijkt me verschrikkelijk dat met php te moeten doen, en daarnaast zijn 30 queries ipv 1 traag.

Daarnaast, hoe geef je die instantie van een view terug, returnen al jou methoden een view? De data overdracht in mijn app gebeurt nu door twee public variabelen in de base class page & data daar stop ik alles in (een grote array) en dat plak ik dan weer in mn template.
Sorteren laat ik ook mooi door de database gebeuren. Stel dat ik 300 producten heb, en ik wil een overzicht van alle producten, dan roep ik die factory aan met wat argumenten, of zelfs een stukje SQL dat ik dan aan de SELECT-query laat plakken. Zo houd je wel zelf volledige controle over wat er uit de database gehaald wordt, en heb je toch direct toegang tot alle data van je artikelen (namelijk via die objecten)

Simpel voorbeeld:

<?php
$items = ItemFactory::find('price < 400.0', 'Name ASC', array(0, 40));
// binnen ItemFactory::find wordt er dan 
?>
SELECT 
*
FROM
	Items
WHERE
	price < 400.0
ORDER BY
	Name ASC
LIMIT
	0,40

Je kan dat allemaal nog wat abstracter maken, of meer automatisch. Wat jezelf wilt.

Al mijn methods in de controllers geven een View-object terug. Dit heeft juist als voordeel dat ik 2 methods in 1 controller kan aanroepen (bijvoorbeeld voor het weergeven van een post, en het weergeven van het bewerken-veld) zonder dat de resultaten door elkaar gaan lopen.

Waar jij zeg maar doet
a roept b
a roept c
a roept d
doet, doe ik
a roept b
b roept c
c roept d
A hoeft zo alleen maar rekening te houden met de uitvoer van b, b alleen met die van c etc. Zo kan je eeuwig stapelen zonder dat a daar ook maar iets van hoeft te weten. Lijkt mij lekker flexibel.

Views zijn (kleine stukjes) HTML, of andere presentatie-data. Views hebben eigenlijk 1 method, namelijk draw(), welke ervoor zorgt dat de inhoud geëchood wordt. Bij draw() wordt er eigenlijk gewoon een PHP bestand met bijv. HTML erin ge-include. Vanuit dat bestand roep ik dan weer $this->subview->draw() aan, waarbij het hele feestje voor subview gebeurt. Data toewijzen doe ik gewoon door $view->variabele = $waarde te doen, waarbij je van binnenuit dus bij de variabele kan via $this.

// a.phtml
Hello <?=$this->name?>,

<? $this->online->draw() ?>


// onlinesubview.phtml
Er zijn <?=$this->onlinePeople?> mensen online

Eindresultaat

Hello Jelmer,

Er zijn 0 mensen online
Ik kwam dit http://www.phpit.net/article/simple-mvc-php5/1/ net even tegen, tewijl ik verder aan het zoeken was naar wat info over mvc's.

Maar het is me nog steeds niet helemaal duidelijk, deze tut. heeft bijvoorbeeld geen model, maar een router class en een register. Het nu van het register zie ik wel in. Maar het is me nog steeds niet helemaal duidelijk, door de hoeveelheid info krijg ik het idee dat niet iedereen het eens is over wat MVC eigenlijk is. Zelfs de werking van een controller, begint me nu langzaam onduidelijk te worden. Heeft iemand misschien nog een goede aanrader voor een mvc tutorial?
Die tutorial is op zich wel ok. Router - die ik niet heb vermeld maar wel degelijk gebruik - wordt goed uitgelegd. Paar dingen die ik anders doe dan zij:

In plaats van een startup-script heb ik een MainController. In weze doet deze hetzelfde: de url aan de router geven, de door de router teruggegeven controller uitvoeren, view verwerken en weergeven.

In plaats van een register gebruik ik zelf vaak Singleton. Voordeel van singleton is dat je pas een instantie aan hoeft te maken wanneer je het object ook echt nodig hebt, waar je bij een register eerst het hele register moet vullen - of je de dingen die erin komen nu nodig hebt of niet. Nadeel is dat je met stricte singleton aan 1 instantie vast zit. In mijn database-klasse (wat niet meer is dan een uitbreiding op PDO) heb ik daarom Storage::default() in plaats van Storage::instance() gebruikt, en de constructor gewoon public gehouden. Zo kan je 1 instantie als default gebruiken, waar je nog 'normale' instanties ernaast kan maken. Maar die zal je dan zelf moeten meesluren of in een register zetten.

Een ander verschil is dat ik de controllers altijd views laat teruggeven in plaats van dat zij alleen een algemene view moeten opvullen. Het voordeel daarvan had ik al uitgelegd: je bent veel flexiebeler, en kan hele hiërarchieën maken van views.

Models leggen ze niet uit in de tutorial, maar models zijn ook niet gebonden aan de MVC implementatie. Eigenlijk gebruik je models altijd wanneer je object geörienteerd programmeert. Meestal gebruik je models die op een of andere manier het Row Data Gateway pattern implementeren. Daarbij hoor t dan weer een factory die Table Data Gateway implementeert.
Ja op het moment denk ik dat ik wel een goed beeld heb van wat er aan de hand is. Op dit moment heb ik volgens mij alleen een controller en views met voor iedere view een eigen template. Ik moet mn controller dus OOP gaan maken want die is nu gewoon oude stijl zeg maar. Verder kan ik models gaan gebruiken voor mn database access, ik denk dat dat ook wel practisch is.

Ik ben er alleen nog niet helemaal uit hoe ik de boel aan elkaar moet gaan naaien. Dat zal waarschijnlijk in de controller moeten gebeuren, hoewel de tutorial dat met de router class doet. Ik zie daar het nut eigenlijk niet echt van in. Ik doe dit op dit moment met de autoload functie en de controller(mijn startup). Beter gezegd, de methode $router->getController is bij mij gewoon in de autoload geintergreerd. en dispatch gebeurt in mijn startup script (wat ik mijn controller noem).

Ik zal voor het begrip even de code daarvan hier posten, ik hoop dat het niet te veel is anders edit ik het er wel weer uit
Mijn startup script:

<?php
// SESSIE STARTEN
session_start();
error_reporting(E_ALL);

// Class loader

require_once "autoload.php";

$request 	= substr(str_replace($page['base'],"",$_SERVER['REQUEST_URI']),1);

$parts 		= explode("/",$request);


if(isset($parts[0]) && $parts[0] != "")	{
	$page['request']['class'] 	= $parts[0];
	if(isset($parts[1]) && $parts[1] != "")	{
		$page['request']['event']		= $parts[1];
	}
}

// De andere waarden instellen als $key => $val paar

for($i=2;$i<count($parts);$i+=2)	{
	$page['request'][$parts[$i]] = $parts[$i+1];
}

// Default class & event 

$class = "FW_mod_shop";
$event = "__default";


// Overschrijven defaults door een request;

if(isset($page['request']['class']) && $page['request']['class'] != "")	{
	$class = "FW_mod_" . $page['request']['class'];
}
if(isset($page['request']['event']) && $page['request']['event'] != "")	{
	$event = $page['request']['event'];
}
// Class laden
if(class_exists($class))	{
	
	// Constructor
	$object = new $class;
	
	// Event executen
	if(method_exists($object,$event))	{
		if($object->login() == true)	{
			$object->$event($page);	
		}
		if($_SESSION['level'] >= $object->level )	{
			$presenter = new FW_fw_presenter($object);
		}	else	{
			$object->low_level();
			$presenter = new FW_fw_presenter($object);
		}
	}	else	{
		echo "No such method: $event()";
	}
}	else	{
	echo "No such class: $class";
}
Dat lijkt inderdaad erg op een router. Persoonlijk zou ik een router in een klasse stoppen, zodat deze wat abstracter wordt.

Zelf heb ik nu een interface voor de router, en die vereist 2 methods: dispatchURI en makeURI. Verder kan je bij de meeste routers (ik heb meerdere implementaties) een fallback-router instellen. Zo krijg je meerdere niveaus.

Om het simpele even wat ingewikkelder te maken:
Op top heb ik een router die aan de hand van regular expressions een url uit elkaar probeert te halen. In mijn startup, waar ik MainController voor gebruik, initialiseer ik deze router met een setje regexen.
Daarnaast maak instantieer ik nog een router, een simpelere, die aan de hand van standaard get argumenten de goeie controller uitkiest. Deze stel ik op de regex-router in als fallback.
Nu voer ik een URI aan de regex-router, deze kijkt of een van z'n patterns matchen. Zo nee, dan voert hij de URI aan de simpele router. Weet deze niets, dan gooit hij een exception (of voert hij hem aan zijn fallback router, maar die heeft hij niet)
Op die manier kan ik meerdere URI schema's gebruiken. Regex voor de mooien, en parameters voor de pagina's die nog niet ver genoeg ontwikkeld zijn.

(Verder durf ik zelfs zover te gaan alle links te laten genereren door de routers. Ik voer ze de naam van de controller en de method, en de argumenten die deze method moet ontvangen - zij maken voor mij de URI. Waneer ik deze URI dan aanroep, wordt als het goed is de door mij genoemde controller::method aangeroepen met de juiste argumenten. Zo kan ik tijdens development het hele URI schema overhoop gooien, en is het zootje allemaal nog net wat abstracter. Maar ik moet zeggen dat dit wel ver gaat en ik zelf ook nog zo m'n twijfels heb aan deze manier. Desalniettemin leuk experiment)


Maar wat ik probeer te zeggen: Waneer je je onderdelen abstracter maakt kan je er beter mee schuiven. Je hebt 3 stappen nodig:
1) URI omzetten naar controller::method(argumenten) (ROUTER)
2) controller aanmaken, method(argumenten) aanroepen (MAINCONTROLLER)
3) uitvoer teruggeven (view::draw()) (MAINCONTROLLER?)
Als je die 3 stappen scheidt, kan je ze makkelijker vervangen en uitbreiden, en wordt het geheel overzichtelijker en logischer.
Jaja.. het wordt me alweer iets duidelijker.. Ik zoek nog steeds verder ondertussen, terwijl ik een schematisch beeld probeer te vormen van, wat er in jou implementatie gebeurt en hoe zich dat verhoud tot de eerdergenoemde tutorial en mijn eigen systeempje.

Daarnaast probeer ik nog wat kern punten duidelijk te krijgen, ik wil niet over een week weer opnieuw beginnen.

In de eerste plaats vraag ik me af of ik iets essentieels mis of dat het gewoon zo is dat een factory pattern, het creeren van objecten is. NIET via een switch, maar via een switch in een class?

Ten tweede vraag ik me af hoe ik precies een model op moet zetten. allemaal private vars voor de DB fields, allemaal getters en setters voor die private vars. en misschien een paar methods voor het selecten updaten verwijderen en het inserten van de rows. Maarja, doorgaans gebruik je diverse specifieke select queries, moet dat allemaal in een method gepropt worden of moet je method gewoon vijf zes parameters accepteren zodat je met een methode zelfs joins groups en string concat's kan doen.

En tenslotte, waar moet je beginnen. De mainController waarschijnlijk, maar ik vraag me af wat je daar precies in moet stoppen, de call moet naar een subController gaan, maar extend die niet gewoon mainController?

Er is mij inmiddels wel pijnlijk duidelijk dat lang niet iedereen hetzelfde denkt over wat een MVC precies is ;)

Daarnaast wil ik even mijn dank uitspreken voor de goede support jelmer! moge het veel hits opleveren!

Reageren