[PHP] MVC pattern

Pagina: 1
Acties:

Onderwerpen


Acties:
  • 0 Henk 'm!

  • RedHat
  • Registratie: Augustus 2000
  • Laatst online: 20:43
Hallo medetweakers,

Ik zit al tijden met een "probleem" die ik probeer te doorgronden. Ik heb de afgelopen maanden ontzettend veel over design patterns gelezen. Globaal snap ik het allemaal wel maar de uitvoering is niet heel erg uitgebreid besproken op het weg (wat betreft PHP). Er zijn eigenlijk twee tutorials (PHPit en PHPpro) die tevens vol met fouten staan. Goed; het is een duidelijk opgezet 'iets' maar nou niet echt rocket science. Nu wil ik mijn ervaring in de zoektocht tot MVC gaan bundelen in een tutorial. Nu is het wel fijn dat je het meteen goed aanpakt natuurlijk ;) Ik heb ontzettend veel 'designvragen' die ik wil doorgronden. Nu zijn er ongetwijveld mensen die dit proces hebben doorlopen en een hoop tips hebben ;)

Allereerst de directorystructuur. Allereerst wil ik even melden dat ik op dit moment niet met public & private mappen werk. Dit kan natuurlijk wel omdat alles aangesproken wordt vanuit één punt (index.php). Maar goed, laten we het nu even niet moeilijker maken dan dat het is ;) De structuur die ik (wil) toepassen:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/libs/
/libs/FrontController.class.php //  Frontcontroller, spreekt Controller dus aan
/libs/TemplateParser.class.php // Templateparser, spreekt voor zich
/libs/Authenticate.class.php // User Authenticate class voor userzaken (login,rechten etc)
/libs/db.class.php // Database class (PDO)
/libs/Registry.class.php // set+get+unset functions, spreekt voor zich

/app/
/app/__APPNAME__/
/app/__APPNAME__/static/
/app/__APPNAME__/static/html // html templates
/app/__APPNAME__/static/js // javascript files
/app/__APPNAME__/static/css // css fles
/app/__APPNAME__/static/img // images
/app/__APPNAME__/controllers // controllers
/app/__APPNAME__/models // models
/app/__APPNAME__/views // views

/conf/


Simpele structuur. Je kan alle dynamische zut nog in /private zetten en static en index in /public. Maar goed, daar gaat het niet helemaal om atm :)

Doormiddel van een FrontController wordt de controller opgesnort en geïnitializeerd. Hier wordt naar de url gekeken. Als voorbeeld:
code:
1
http://domain.tld/index.php/gastenboek/view


De FrontController kijkt of de controller gastenboek bestaat met de method view, zoniet schakeld hij naar de standaard controller en laat dan gewoon de index pagina zien. Niet héél fancy en erg basic. Ik zal voor het gemak even de FrontController posten:

PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<?php

class FrontController {
    /**
      * 
      * @param array $aParts
      * @param string $sController
      * @param string $sAction
      * 
      * @desc De FrontController kijkt de PATH_INFO na en zet deze in een array.
      * @desc Met deze info wordt de goede controller aangesproken.
      *
      */
    private $aParts = array();
      
    public function __construct () {
     
        if ( isset ( $_SERVER['PATH_INFO'] ) )
        {
            /**
             * Er wordt gekeken wat er in de URL staat na index.php
             * Voorbeeld: http://www.domain.tld/index.php/guestbook/viewmessages
             * $parts = array ( [0] => guestbook, [1] => viewmessages );
             * Dit wil zeggen dat guestbook de controller is en viewmessages de actie.
             */

            $aParts = explode ('/', strtolower ( trim ( $_SERVER['PATH_INFO'], '/' ) ) );
            
            /**
             * Hier wordt de inhoud van $parts in $sController en $sAction gezet.
             * Als er iets niet klopt wordt de standaard pagina opgehaald.
             */
 
            $sController = ( current ( $aParts ) ) ? array_shift ( $aParts ) : 'default';
            $sAction = ( current ( $aParts ) ) ? array_shift ( $aParts ) : 'index';
        }
        else
        {
            /**
             * De PATH_INFO is niet geset, daarom halen we de normale index op.
             */
            $sController = 'default';
            $sAction = 'index';
        }

        /**
         * $sController en $sAction worden nu aan de dispatcher meegegeven.
         * De dispatcher gaat uiteindelijk echt de controller aanspreken
         */
        $this->dispatch ( $sController, $sAction );     
    }


    public function dispatch ( $sController, $sAction)
    {
        if ( file_exists ( 'app/'. __APPNAME__ .'controllers/' . $sController . '.class.php' ) )
        {
            /**
             * Het bestand bestaat dus we gaan hem includen
             * $sControllername bevat de naam van de controller
             */
            require_once ( 'app/' . __APPNAME__ . 'controllers/'. $sController . '.class.php' );

            $sControllername = ucfirst ($sController) . 'Controller';
               
            /**
             * Nu gaan we kijken of de classe ook echt bestaat.
             * Zoniet vallen we gewoon weer terug op de index pagina.
             * We gaan meteen kijken of de actie ook bestaat.
             * Als hij bestaat voeren we hem ook meteen uit.
             */

            if ( class_exists ( $sControllername ) ) { 
                
                if ( in_array ( $sAction, get_class_methods ($sControllername ) ) ) {
                    
                    $oController = new $sControllername ();
                    $oController -> $sAction ();
                    return;
                }
                    
            }
            else { echo ('hoi');}
        }
        else {
          

        /**
         * Blijkbaar bestaat de controller dus niet, dan is er wat fout gegaan
         */
        echo('De controller bestaat helemaal niet. 404'); //404?
        }
    }
}

?>


De comments zijn duidelijk en spreken voor zich lijkt mij. Ook vrij simpel om te zien wat de FrontController al wel (en niet) doet :P

En hier strand ik meteen. Als ik uitga van de cakephp documentatie wordt er in de controller gezocht naar de juiste model+view. Ik zit te twijvelen; moet dat echt in de controller of kan dat ook al in de FrontController. Immers weet ik nu al dat er een controller aangesproken gaat worden als hij bestaat met een bepaalde actie en ik weet ook dat er een model+view bij hoort. Nu zal mijn denkwijze tot op de grond afgebrand kunnen worden denk ik.

Ik kan héél simpel in de controller kijken hoe de class heet en dan het model+view openen met dezelfde naam als het ware. Maar is dit wel de goede manier? Kortom: Hoe nu verder kwa denkwijze en ontwerpkeuzes.

Dan heb ik ook nog een aantal 'losse' vraagjes die ik niet kon vinden (of wel kon vinden maar ernstig twijvelde aan de waarheid daarvan).

Zo heb ik een class Authenticate. Deze kan bijvoorbeeld zien of er een user is ingelogt, welke rechten erbij horen etc. Nu dacht ik dat dat iets is wat vaak terugkomt en dat je die het beste als 'lib' kan inladen. Aan de andere kant dacht ik als je bijvoorbeeld een forum hebt dat je toch query's hebt die appbased zijn maar waar userrechten zwaar in verwikkeld zijn. In hoeverre moet ik dit apart gaan zien? Is het erg dat er op een bepaald moment een query in het model zit die bijvoorbeeld gelijk op userrechten controlleerd. Vanuit 'gevoel' zou je zeggen dat dat in Authenticate class moet afgehandeld worden maar dan denk ik meteen; eey, je hebt een serieuze ontwerpfout. Bij dit soort dingen is overzicht natuurlijk super belangrijk en het uittekenen van bepaalde zaken. Desalniettemin een ontzettend leerzaam project waar ik ontzettend veel van ga leren en al geleerd heb.

De database class (/lib) wil ik zo klein mogelijk houden. En vooral niet met Singleton werken (lijkt mij ook een ontwerpfout omdat je maar één keer kan initializeren lijkt mij (één instance)). Dus dan wordt het vooral kwa functionaliteit: Querys ophalen, Prepared Statements etc. Die wil ik dan ook zo compact mogelijk houden en ook zo algemeen mogelijk. Dus de corefuncties.

Ik denk dat er véle wegen naar rome zijn maar ik denk ook dat heel veel wegen foutief zijn. Ik denk ook dat een hoop tweakers hier een mening over hebben en ik hoop dan ook dat andere mensen hier ook weer van kunnen leren.

Als disclaimer wil ik wel even zeggen dat dit om een leerprojectje gaat. Denkwijze verbreden etc. Ja ik kan ook gewoon een willekeurig MVC-framework downloaden maar ik kies ervoor om kleinschalig iets zelf te maken om bepaalde dingen te begrijpen. Reacties met 'Download gewoon een framework en concentreer je op de echte problemen' vind ik dan ook niet geheel op zijn plaats. Denk zeg het maar even vooraf :P

Acties:
  • 0 Henk 'm!

  • sky-
  • Registratie: November 2005
  • Niet online

sky-

qn ella 👌

Wat je vaak tegen komt ("libs") moet je gewoon altijd registreren in een register. Singleton (is niet OO) kan wéldegelijk gebruikt worden icm een goed ontwerp, maar is af te raden.

Je frontcontroller hoort idd te kijken of een route aangesproken kan worden (niet naar je model+view!, zie reactie hieronder), kan dit niet dan gooi je een fout of spreek je bijvoorbeeld de default route van je aangesproken controller aan. Kan het wél dan zou je de route kunnen cachen oid, maar dat is voor later.

Dir structuur is niet boeiend, zolang je maar consiquent bent, dat is het belangrijkste.

PS. Je codecommentaar klopt niet (mocht je PHPDoc gaan gebruiken):

code:
1
2
3
4
5
6
7
8
9
10
/**
      * 
      * @param array $aParts
      * @param string $sController
      * @param string $sAction
      * 
      * @desc De FrontController kijkt de PATH_INFO na en zet deze in een array.
      * @desc Met deze info wordt de goede controller aangesproken.
      *
      */

Je hoort per variabele aan te geven wat het is.. Maar goed, dat is de vraag niet ;-).

[ Voor 32% gewijzigd door sky- op 03-11-2009 00:01 ]

don't be afraid of machines, be afraid of the people who build and train them.


Acties:
  • 0 Henk 'm!

  • storeman
  • Registratie: April 2004
  • Laatst online: 00:28
Een database vind ik wel typisch iets om dmv een singleton op te lossen. Je wilt niet voor elke query een nieuwe verbinding met je DB maken, je hebt en wilt slechts 1 verbinding dus deze moet je hier dan ook voor gebruiken. Uiteraard, als je verschillende databases gebruikt, gaat dit niet op. Maar over het algemeen zullen al je modellen hun data vergaren dmv dezelfde verbinding. Connecten is vrij 'duur'.

Je frontcontroller moet niet al kijken naar het model+view. Sommige controllers hebben simpelweg van doen met meer dan 1 model, hoe zou je dat oplossen? Ook views weet je niet zeker, je kunt algemene templates willen gebruiken of voor verschillende uitkomsten van je acties verschillende views gebruiken. Dit moet je dus ook echt pas op controller/actie niveau gaan doen.

Wat authenticatie betreft hoef je dit helemaal niet te verwikkelen. Authenticatie is puur om te kijken welke gebruiker er is ingelogd en of die info klopt. Heb je die gegevens eenmaal gevalideerd, dan kun je die user opslaan in het register of dmv een singleton benaderen ( er kan immers niet meer dan 1 ingelogde gebruiker zijn). Op die manier kun je eenvoudig het user_id onthouden/achterhalen en heeft je authenticatie helemaal niets meer te maken met de rest van je logica.

"Chaos kan niet uit de hand lopen"


Acties:
  • 0 Henk 'm!

  • mithras
  • Registratie: Maart 2003
  • Niet online
Even wat losse opmerkingen / ingevingen die ik krijg als ik je post lees:
Doormiddel van een FrontController wordt de controller opgesnort en geïnitializeerd. Hier wordt naar de url gekeken. Als voorbeeld:
code:
1
http://domain.tld/index.php/gastenboek/view


De FrontController kijkt of de controller gastenboek bestaat met de method view, zoniet schakeld hij naar de standaard controller en laat dan gewoon de index pagina zien. Niet héél fancy en erg basic. Ik zal voor het gemak even de FrontController posten:
Bedenk dat een Front Controller pattern != MVC. Het kan goed gecombineerd worden, maar het zijn twee verschillende zaken.
The Front Controller consolidates all request handling by channeling requests through a single handler object. This object can carry out common behavior, which can be modified at runtime with decorators. The handler then dispatches to command objects for behavior particular to a request.
Controller Model View Controller (MVC) is one of the most quoted (and most misquoted) patterns around. It started as a framework developed by Trygve Reenskaug for the Smalltalk platform in the late 1970s. Since then it has played an influential role in most UI frameworks and in the thinking about UI design.
Ook het MVC pattern is door de jaren heen sinds zijn ontstaan steeds wat verder doorgedacht en geëvolueerd.
En hier strand ik meteen. Als ik uitga van de cakephp documentatie wordt er in de controller gezocht naar de juiste model+view. Ik zit te twijvelen; moet dat echt in de controller of kan dat ook al in de FrontController. Immers weet ik nu al dat er een controller aangesproken gaat worden als hij bestaat met een bepaalde actie en ik weet ook dat er een model+view bij hoort. Nu zal mijn denkwijze tot op de grond afgebrand kunnen worden denk ik.
Een controller, model en view staan los van elkaar. In een BlogController kan je een ArticleModel hebben en een ReactionModel. Vervolgens kan je een ArticleView hebben en een ArchiveView en een RecentArticlesView. Dus één controller kent geen/één/meerdere modellen en geen/één/meerdere views.

Een controller zal hier zelf zijn ding in moeten regelen. Het meest gemakkelijke is om van een autoloader gebruik te maken. Het Zend Framework (kent ook een mvc implementatie) maakt gebruik van de PEAR naming conventies, wat autoloading heel erg makkelijk maakt. Daarnaast is een autoloader ook nog efficiënter dan "require_once", omdat bij die functie je pollt naar het filesystem wat meer resources kost dan een interne lookup in je autloader om te kijken of het bestand wel bestaat.
Ik kan héél simpel in de controller kijken hoe de class heet en dan het model+view openen met dezelfde naam als het ware. Maar is dit wel de goede manier? Kortom: Hoe nu verder kwa denkwijze en ontwerpkeuzes.
Zie dus hierboven. Je moet naming conventies opleggen en op basis hiervan een autloader implementeren. Dingen als:
PHP:
1
2
3
4
5
6
7
8
9
10
11
12
class GuestbookController extends ActionController_Abstract
{
  public function index ()
  {
    $id = ''; //Something
    // Autoloading model
    $guestbook = new Guestbook_Model_Guestbook;
    $guestbook->find($id);
    // Autoloading reaction model inside Guestbook_Model_Guestbook::getReactions()
    $reactions = $guestbook->getReactions();
  }
}
Zo heb ik een class Authenticate. Deze kan bijvoorbeeld zien of er een user is ingelogt, welke rechten erbij horen etc. Nu dacht ik dat dat iets is wat vaak terugkomt en dat je die het beste als 'lib' kan inladen. Aan de andere kant dacht ik als je bijvoorbeeld een forum hebt dat je toch query's hebt die appbased zijn maar waar userrechten zwaar in verwikkeld zijn. In hoeverre moet ik dit apart gaan zien? Is het erg dat er op een bepaald moment een query in het model zit die bijvoorbeeld gelijk op userrechten controlleerd. Vanuit 'gevoel' zou je zeggen dat dat in Authenticate class moet afgehandeld worden maar dan denk ik meteen; eey, je hebt een serieuze ontwerpfout. Bij dit soort dingen is overzicht natuurlijk super belangrijk en het uittekenen van bepaalde zaken. Desalniettemin een ontzettend leerzaam project waar ik ontzettend veel van ga leren en al geleerd heb.
Authenticate != Authorize. Je hebt het herkennen van een gebruiker (en daarbij persistent login -dat je het volgend request ook ingelogd bent-, logout etc) en authorization (heeft deze gebruiker recht om actie X op object Y uit te voeren.

Je kan dit dus geheel loskoppelen. Ik neem even voor het gemak het Zend Framework, omdat ik daar recentelijk veel mee werk. Je hebt hierin een role en een resource. Een role is een gebruiker, maar ook een groep (dit is hiërarchisch opgebouwd). Een resource is een ding waarop je toegang kan krijgen, dus een pagina, module, categorie, domein of wat dan ook. Je kan dus de modules zelf de resources laten toevoegen in deze lijst. En bijvoorbeeld de permissies (welke rollen aan de resources gekoppeld zijn met een bepaalde actie) uit de database halen.
De database class (/lib) wil ik zo klein mogelijk houden. En vooral niet met Singleton werken (lijkt mij ook een ontwerpfout omdat je maar één keer kan initializeren lijkt mij (één instance)). Dus dan wordt het vooral kwa functionaliteit: Querys ophalen, Prepared Statements etc. Die wil ik dan ook zo compact mogelijk houden en ook zo algemeen mogelijk. Dus de corefuncties.
Een singleton is imho niet per sé een ontwerpfout. Het is wat moeilijker met unit testing, maar bijvoorbeeld een front controller kan je prima in een singleton gieten.

Het is voor je database wat lastiger om dat in een singleton te gieten, omdat je soms meerdere connecties wilt gebruiken. Toch is dit in 99% van de gevallen niet zo. Je kan statisch een default connectie opzetten en daarnaast instances gebruiken voor specifieke dingen:
PHP:
1
2
3
$db1 = Db::factory($host1, $user1, pass1);
$db2 = Db::factory($host2, $user2, $pass2);
Db::setDefault($db2);
Dan kan je zonder een db instance te specificeren tóch queries uitvoeren. Maar je hebt altijd de mogelijkheid om de db instance te setten.
Ik denk dat er véle wegen naar rome zijn maar ik denk ook dat heel veel wegen foutief zijn. Ik denk ook dat een hoop tweakers hier een mening over hebben en ik hoop dan ook dat andere mensen hier ook weer van kunnen leren.

Als disclaimer wil ik wel even zeggen dat dit om een leerprojectje gaat. Denkwijze verbreden etc. Ja ik kan ook gewoon een willekeurig MVC-framework downloaden maar ik kies ervoor om kleinschalig iets zelf te maken om bepaalde dingen te begrijpen. Reacties met 'Download gewoon een framework en concentreer je op de echte problemen' vind ik dan ook niet geheel op zijn plaats. Denk zeg het maar even vooraf :P
Tóch maak ik er een opmerking over. Reinventing the wheel is een groot kenmerk van bijna alle php ontwikkelaars. Een framework beperkt je in geen enkele mate. Het zorgt natuurlijk voor snellere ontwikkeling en waarschijnlijk ook betere code. Maar de grootste fout ooit gemaakt is dat je door het zelf te schrijven er meer van leert. Juist door ingewikkelde applicaties te verzinnen die veel vragen van het framework maakt dat je moet nadenken om de right tool for the right job te gebruiken. En ik merk dat ik nu 7 jaar geklungeld heb en 1 jaar met ZF heb gewerkt. En het is zeker niet zo dat het laatste jaar ik minder heb geleerd, ik denk juist het tegenovergestelde :)

Acties:
  • 0 Henk 'm!

  • Patriot
  • Registratie: December 2004
  • Laatst online: 16-09 13:49

Patriot

Fulltime #whatpulsert

mithras schreef op dinsdag 03 november 2009 @ 00:16:
[...]
Tóch maak ik er een opmerking over. Reinventing the wheel is een groot kenmerk van bijna alle php ontwikkelaars. Een framework beperkt je in geen enkele mate. Het zorgt natuurlijk voor snellere ontwikkeling en waarschijnlijk ook betere code. Maar de grootste fout ooit gemaakt is dat je door het zelf te schrijven er meer van leert. Juist door ingewikkelde applicaties te verzinnen die veel vragen van het framework maakt dat je moet nadenken om de right tool for the right job te gebruiken. En ik merk dat ik nu 7 jaar geklungeld heb en 1 jaar met ZF heb gewerkt. En het is zeker niet zo dat het laatste jaar ik minder heb geleerd, ik denk juist het tegenovergestelde :)
Toch valt er iets te zeggen voor het kennen van dat waar je mee werkt. Joel Spolsky (wordt hier regelmatig gequote) legt uit waarom in zijn artikel over Leaky Abstractions. Zo'n framework blijft een abstractie van het een of het ander, en op het moment dat daar iets mis mee gaat is het handig om over voldoende kennis te bezitten om dat probleem op te lossen.

Acties:
  • 0 Henk 'm!

  • sky-
  • Registratie: November 2005
  • Niet online

sky-

qn ella 👌

En om daar nog op in te springen: door bijv ZF wordt je "gelimiteerd" door wat je met ZF kan: je hoort dus een bepaalde programmeermethode aan te houden. Met een eigen Framework (waar je van KAN leren..maar je kan het ook fout aanpakken) kan je een eigen stijl aanhouden. Of je dat wilt? Ligt eraan.

Zelf heb ik een Framework ontwikkeld wat een beetje inspiraties heeft van Zend, alleen wat andere verwerkingen en het werkt uitstekend. Ach, ieder zijn ding.

don't be afraid of machines, be afraid of the people who build and train them.


Acties:
  • 0 Henk 'm!

  • nika
  • Registratie: Oktober 2003
  • Niet online
En om daar nog op in te springen: door bijv ZF wordt je "gelimiteerd" door wat je met ZF kan
Werk nu 1,5 jaar met ZF. De leercurve is stijl, maar als je er eenmaal inzit werkt het bijzonder handig en is code reusability hoog. ZF beperkt helemaal niet. Het is vrijelijk uit te breiden. Natuurlijk zit je aan een paar basis dingen vast (manier van routing bijv.) maar zelfs die zou je kunnen aanpassen. Nog altijd minder werk dan zelf een framework maken lijkt me.

Acties:
  • 0 Henk 'm!

  • sky-
  • Registratie: November 2005
  • Niet online

sky-

qn ella 👌

Ik bedoelde ook niet dat ZF beperkt, want het werkt wel. Ik doelde meer dat je een bepaalde methode aan moet houden.

don't be afraid of machines, be afraid of the people who build and train them.


Acties:
  • 0 Henk 'm!

  • compufreak88
  • Registratie: November 2001
  • Laatst online: 02-05 17:51
sky- schreef op dinsdag 03 november 2009 @ 14:11:
Ik bedoelde ook niet dat ZF beperkt, want het werkt wel. Ik doelde meer dat je een bepaalde methode aan moet houden.
Over wat voor soort methode heb je het hier dan?
Pagina: 1