3. Een generieke controller
3.1. Inleiding
In de vorige methode was het de bedoeling dat we de controller met de naam main.php zouden schrijven. Met wat ervaring realiseert men zich dat deze controller vaak dezelfde taken uitvoert en is het dan verleidelijk om een generieke controller te schrijven die in de meeste webapplicaties kan worden gebruikt. De code van deze controller zou als volgt kunnen zijn:
<?php
// generieke controller
// configuratie lezen
include 'config.php';
// bibliotheken opnemen
for($i=0;$i<count($dConfig['includes']);$i++){
include($dConfig['includes'][$i]);
}//for
// de sessie starten of hervatten
session_start();
$dSession=$_SESSION["session"];
if($dSession) $dSession=unserialize($dSession);
// de uit te voeren actie ophalen
$sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';
$sAction=strtolower($_SERVER['REQUEST_METHOD']).":$sAction";
// is de volgorde van de acties normaal?
if( ! enchainementOK($dConfig,$dSession,$sAction)){
// afwijkende volgorde
$sAction='enchainementinvalide';
}//if
// de actie wordt verwerkt
$scriptAction=$dConfig['actions'][$sAction] ?
$dConfig['actions'][$sAction]['url'] :
$dConfig['actions']['actionInvalide']['url'];
include $scriptAction;
// het antwoord (weergave) naar de klant verzenden
$sEtat=$dSession['etat']['principal'];
$scriptVue=$dConfig['etats'][$sEtat]['vue'];
include $scriptVue;
// einde van het script – dit zou niet mogen gebeuren, tenzij er een bug is
trace ("Erreur de configuration.");
trace("Action=[$sAction]");
trace("scriptAction=[$scriptAction]");
trace("Etat=[$sEtat]");
trace("scriptVue=[$scriptVue]");
trace ("Vérifiez que les script existent et que le script [$scriptVue] se termine par l'appel à finSession.");
exit(0);
// ---------------------------------------------------------------
function finSession(&$dConfig,&$dReponse,&$dSession){
// $dConfig: configuratiedictionary
// $dSession: woordenboek met sessiegegevens
// $dReponse: het woordenboek met de argumenten van de responspagina
// sessie-record
if(isset($dSession)){
//: de parameters van het verzoek worden in de sessie opgeslagen
$dSession['requete']=strtolower($_SERVER['REQUEST_METHOD'])=='get' ? $_GET :
strtolower($_SERVER['REQUEST_METHOD'])=='post' ? $_POST : array();
$_SESSION['session']=serialize($dSession);
session_write_close();
}else{
// geen sessie
session_destroy();
}
// het antwoord wordt weergegeven
include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];
// einde van het script
exit(0);
}//sessie beëindigen
//--------------------------------------------------------------------
function enchainementOK(&$dConfig,&$dSession,$sAction){
// controleert of de huidige actie is toegestaan gezien de vorige status
$etat=$dSession['etat']['principal'];
if(! isset($etat)) $etat='sansetat';
// actiecontrole
$actionsautorisees=$dConfig['etats'][$etat]['actionsautorisees'];
$autorise= ! isset($actionsautorisees) || in_array($sAction,$actionsautorisees);
return $autorise;
}
//--------------------------------------------------------------------
function dump($dInfos){
// geeft een informatieoverzicht weer
while(list($clé,$valeur)=each($dInfos)){
echo "[$clé,$valeur]<br>\n";
}//while
}//opvolging
//--------------------------------------------------------------------
function trace($msg){
echo $msg."<br>\n";
}//opvolging
?>
3.2. Het configuratiebestand van de applicatie
De applicatie wordt geconfigureerd in een script dat verplicht de naam config.php moet dragen. De parameters van de applicatie worden opgeslagen in een woordenboek met de naam $dConfig, dat zowel door de controller als door de actiescripts, de modellen en de elementaire weergaven wordt gebruikt.
3.3. De bibliotheken die in de controller moeten worden opgenomen
De bibliotheken die in de code van de controller moeten worden opgenomen, staan in de tabel $dConfig['includes']. De controller neemt ze op met de volgende codereeks:
<?php
...
// configuratie lezen
include "config.php";
// bibliotheken opnemen
for($i=0;$i<count($dConfig['includes']);$i++){
include($dConfig['includes'][$i]);
}//for
3.4. Sessiebeheer
De generieke controller beheert automatisch een sessie. Hij slaat de inhoud van een sessie op en haalt deze op via het woordenboek $dSession. Dit woordenboek kan objecten bevatten die moeten worden geserialiseerd om ze later correct te kunnen ophalen. De sleutel die aan dit woordenboek is gekoppeld, is 'session'. Het ophalen van een sessie gebeurt dus met de volgende code:
<?php
…
// sessie starten of hervatten
session_start();
$dSession=$_SESSION["session"];
if($dSession) $dSession=unserialize($dSession);
Als een actie informatie in de sessie wil opslaan, voegt deze sleutels en waarden toe aan het woordenboek $dSession. Aangezien alle acties dezelfde sessie delen, bestaat er een risico op conflicten met sessiesleutels als de applicatie door meerdere personen onafhankelijk van elkaar wordt ontwikkeld. Dit vormt een uitdaging. Er moet een repository worden ontwikkeld waarin de sessiesleutels worden opgesomd, een repository die door iedereen wordt gedeeld. We zullen zien dat elke actie eindigt met het aanroepen van de volgende functie finSession:
<?php
...
// ---------------------------------------------------------------
function finSession(&$dConfig,&$dReponse,&$dSession){
// $dConfig: configuratiedictionary
// $dSession: woordenboek met sessie-informatie
// $dReponse: het woordenboek met de argumenten van de responspagina
// sessie-record
if(isset($dSession)){
//: de parameters van het verzoek worden in de sessie opgeslagen
$dSession['requete']=strtolower($_SERVER['REQUEST_METHOD'])=='get' ? $_GET :
strtolower($_SERVER['REQUEST_METHOD'])=='post' ? $_POST : array();
$_SESSION['session']=serialize($dSession);
session_write_close();
}else{
// geen sessie
session_destroy();
}
// het antwoord wordt weergegeven
include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];
// einde van het script
exit(0);
}//einde sessie
Een actie kan besluiten een sessie niet voort te zetten. Hiervoor hoeft er geen waarde te worden doorgegeven aan de parameter $dSession van de functie finSession; in dat geval wordt de sessie verwijderd (session_destroy). Als het woordenboek $dSession bestaat, wordt het in de sessie opgeslagen en wordt de sessie vervolgens vastgelegd (session_write_close). De huidige actie kan dus elementen in de sessie opslaan door elementen toe te voegen aan het woordenboek $dSession. Merk op dat de controller automatisch de parameters van de huidige aanvraag in de sessie opslaat. Hierdoor kunnen deze indien nodig worden opgehaald om de volgende aanvraag te verwerken.
3.5. Het verzenden van het antwoord naar de klant
De functie finSession heeft als uiteindelijk doel een antwoord naar de gebruiker te verzenden. We hebben gezegd dat een antwoord verschillende paginasjablonen kan hebben. Deze worden via de configuratie in $dConfig['vuesReponse'] geplaatst. In een applicatie met twee sjablonen zou men dus het volgende kunnen hebben:
<?php
…
$dConfig['vuesReponse']['modele1']=array('url'=>'m-modele1.php');
$dConfig['vuesReponse']['modele2']=array('url'=>'m-modele2.php');
De lopende actie specificeert in $dReponse['vuereponse'] het gewenste model. Dit wordt door de controller weergegeven met de instructie:
<?php
…
// het antwoord wordt weergegeven
include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];
Zodra dit antwoord naar de client is verzonden, stopt de controller (exit).
3.6. De uitvoering van de acties
De controller verwacht verzoeken met de parameter action=XX. Als deze parameter niet in het verzoek voorkomt en het verzoek de vorm GET heeft, krijgt de actie de waarde 'init'. Dit is het geval bij het allereerste verzoek aan de controller, dat de vorm http://machine:port/chemin/main.php heeft.
<?php
…..
// de uit te voeren actie wordt opgehaald
$sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';
Standaard is aan elke actie een script gekoppeld dat deze actie verwerkt. Bijvoorbeeld:
<?php
...
// configuratie van de acties van de applicatie
$dConfig['actions']['get:init']=array('url'=>'a-init.php');
$dConfig['actions']['post:calculerimpot']=array('url'=>'a-calculimpot.php');
$dConfig['actions']['get:retourformulaire']=array('url'=>'a-retourformulaire.php');
$dConfig['actions']['post:effacerformulaire']=array('url'=>'a-init.php');
$dConfig['actions']['enchainementinvalide']=array('url'=>'a-enchainementinvalide.php');
$dConfig['actions']['actionInvalide']=array('url'=>'a-actioninvalide.php');
Er zijn twee acties vooraf gedefinieerd:
in het geval dat de huidige actie niet kan volgen op de vorige actie | |
in het geval dat de gevraagde actie niet voorkomt in het actieregister |
De acties die specifiek zijn voor de applicatie worden genoteerd in de vorm methode:actie, waarbij methode de get- of post-methode van het verzoek is en actie de gevraagde actie, in dit geval: init, calculerimpot, retourformulaire, effacerformulaire. Merk op dat de actie wordt opgehaald, ongeacht de methode GET of POST voor het verzenden van de parameters, via de reeks:
<?php
…
// de uit te voeren actie wordt opgehaald
$sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';
Want zelfs als een formulier is verzonden, kun je nog steeds schrijven:
De formulierelementen worden verzonden (method='post'). De opgevraagde URL zal echter main.php?action=calculerimpot zijn. De parameters van deze URL worden opgehaald uit het woordenboek $_GET, terwijl de overige formulierelementen worden opgehaald uit het woordenboek $_POST.
Met behulp van het actiedictionary voert de controller de gevraagde actie als volgt uit:
<?php
...
// de actie wordt verwerkt
$scriptAction=$dConfig['actions'][$sAction] ?
$dConfig['actions'][$sAction]['url'] :
$dConfig['actions']['actionInvalide']['url'];
include $scriptAction;
Als de gevraagde actie niet in het actieregister voorkomt, wordt het script uitgevoerd dat bij een ongeldige actie hoort. Zodra het actiescript in de controller is geladen, wordt het uitgevoerd. Merk op dat het toegang heeft tot de variabelen van de controller ($dConfig, $dSession) en tot de superglobale woordenboeken van PHP ($_GET, $_POST, $_SERVER, $_ENV, $_SESSION). Het script bevat zowel applicatielogica als aanroepen van businessklassen. In alle gevallen moet de actie
- het woordenboek $dSession invullen als er elementen in de huidige sessie moeten worden opgeslagen
- in $dReponse['vuereponse'] de naam van het weer te geven antwoordmodel aangeven
- afsluiten met de aanroep van finSession($dConfig, $dReponse, $dSession). Als de sessie moet worden beëindigd, eindigt de actie eenvoudigweg met de aanroep van finSession($dConfig, $dReponse).
Omwille van de consistentie kan de actie alle informatie die nodig is voor de weergaven in het woordenboek $dReponse opslaan. Dit is echter niet verplicht. Alleen de waarde $dReponse['vuereponse'] is onmisbaar. Merk op dat elk actiescript eindigt met een aanroep van de functie finSession, die op haar beurt eindigt met een exit-bewerking. Men keert dus niet terug vanuit een actiescript.
3.7. De opeenvolging van acties
Een webapplicatie kan worden gezien als een automaat met eindige toestanden. De verschillende toestanden van de applicatie zijn gekoppeld aan de weergaven die aan de gebruiker worden getoond. De gebruiker gaat via een link of een knop naar een andere weergave. De webapplicatie is van toestand veranderd. We hebben gezien dat een actie wordt geïnitieerd door een verzoek van het type http://machine:port/chemin/main.php?action=XX. Deze URL moet afkomstig zijn van een link in de weergave die aan de gebruiker wordt getoond. We willen namelijk voorkomen dat een gebruiker de URL http://machine:port/chemin/main.php?action=XX rechtstreeks intypt en zo het traject omzeilt dat de applicatie voor hem heeft uitgestippeld. Dit geldt ook als de klant een programma is.
Een navigatieketen is correct als de opgevraagde URL een URL is die kan worden opgevraagd vanuit de laatste weergave die aan de gebruiker is getoond. De lijst hiervan is eenvoudig vast te stellen. Deze bestaat
- uit de URL-pagina’s die in de weergave zijn opgenomen, hetzij in de vorm van links, hetzij in de vorm van actiedoelen van het type ‘submit’
- de URL die een gebruiker rechtstreeks in zijn browser mag invoeren wanneer de weergave aan hem wordt getoond.
De lijst met applicatiestaten valt niet noodzakelijkerwijs samen met die van de weergaven. Laten we bijvoorbeeld eens kijken naar de volgende elementaire weergave erreurs.php:
Les erreurs suivantes se sont produites :
<ul>
<?php
for($i=0;$i<count($dReponse["erreurs"]);$i++){
echo "<li class='erreur'>".$dReponse["erreurs"][$i]."</li>\n";
}//for
?>
</ul>
<div class="info"><?php echo $dReponse["info"] ?></div>
<a href="<?php echo $dReponse["href"] ?>"><?php echo $dReponse["lien"] ?></a>
Deze elementaire weergave wordt geïntegreerd in een samenstelling van elementaire weergaven die samen het antwoord vormen. Op deze weergave bevindt zich een link die dynamisch kan worden gepositioneerd. De weergave erreurs.php kan dan, afhankelijk van de omstandigheden, met n verschillende links worden weergegeven. Dit leidt tot n verschillende statussen voor de applicatie. In status nr. i wordt de weergave erreurs.php weergegeven met de link lieni. In deze status is alleen het gebruik van lieni toegestaan.
De lijst met statussen van een applicatie en de mogelijke acties in elke status worden vastgelegd in het woordenboek $dConfig['etats']:
<?php
...
// configuratie van de statussen van de applicatie
$dConfig['etats']['e-formulaire']=array(
'actionsautorisees'=>array('post:calculerimpot','get:init','post:effacerformulaire'),
'vue'=>'e-formulaire2.php');
$dConfig['etats']['e-erreurs']=array(
'actionsautorisees'=>array('get:retourformulaire','get:init'),
'vue'=>'e-erreurs2.php');
$dConfig['etats']['sansetat']=array('actionsautorisees'=>array('get:init'));
De bovenstaande applicatie heeft twee statussen met de namen: e-formulier en e-fouten. We voegen een status toe met de naam 'zonder status', die overeenkomt met de eerste start van de applicatie toen deze nog geen status had. In een E-status staat de lijst met toegestane acties in de tabel $dConfig['etats'][E]['actionsautorisees']. Daarin worden de voor de actie toegestane methode (GET/POST) en de naam van de actie gespecificeerd. In het bovenstaande voorbeeld zijn er vier mogelijke acties: get:init, post:alculerimpot, get:retourformulaire en post:effacerformulaire.
Aan de hand van het woordenboek $dConfig['etats'] kan de controller bepalen of de huidige actie $sAction al dan niet is toegestaan in de huidige status van de applicatie. Deze wordt bij elke actie opgebouwd en in de sessie opgeslagen in $dSession['etat']. De code van de controller om te controleren of de huidige actie is toegestaan of niet, is als volgt:
<?php
.....
// is de volgorde van de acties normaal?
if( ! enchainementOK($dConfig,$dSession,$sAction)){
// abnormale volgorde
$sAction='enchainementinvalide';
}//if
// verwerking van de actie
$scriptAction=$dConfig['actions'][$sAction] ?
$dConfig['actions'][$sAction]['url'] :
$dConfig['actions']['actionInvalide']['url'];
include $scriptAction;
..........
//--------------------------------------------------------------------
function enchainementOK(&$dConfig,&$dSession,$sAction){
// controleert of de huidige actie is toegestaan gezien de vorige status
$etat=$dSession['etat']['principal'];
if(! isset($etat)) $etat='sansetat';
// actiecontrole
$actionsautorisees=$dConfig['etats'][$etat]['actionsautorisees'];
$autorise= ! isset($actionsautorisees) || in_array($sAction,$actionsautorisees);
return $autorise;
}
De logica is als volgt: een actie $sAction is toegestaan als deze voorkomt in de lijst $dConfig['etats'][$etat]['actionsautorisees'] of als deze lijst niet bestaat waardoor elke actie is toegestaan. $etat is de status van de applicatie aan het einde van de vorige cyclus van clientverzoek/serverantwoord. Deze status is opgeslagen in de sessie en wordt daar teruggevonden. Als blijkt dat de gevraagde actie ongeldig is, wordt het script $dConfig['actions']['enchainementInvalide']['url'] uitgevoerd. Dit script zorgt ervoor dat er een passend antwoord naar de klant wordt verzonden.
Tijdens de ontwikkelingsfase hoeft het woordenboek $dConfig['etats'] niet te worden ingevuld. In dat geval is elke actie toegestaan in elke status. Het woordenboek kan worden afgestemd zodra de applicatie volledig is gedebugd. Het beschermt de applicatie tegen ongeoorloofde acties.
3.8. Debuggen
De controller biedt twee debugfuncties:
- met de trace-functie kan een bericht worden weergegeven in de stream HTML
- met de dump-functie kan de inhoud van een woordenboek in dezelfde stream worden weergegeven
Elk actiescript kan deze twee functies gebruiken. Aangezien de code van het actiescript is opgenomen (include) in de code van de controller, zijn de functies trace en dump zichtbaar voor de scripts.
3.9. Conclusie
De generieke controller is bedoeld om de ontwikkelaar in staat te stellen zich te concentreren op de acties en weergaven van zijn applicatie. De controller zorgt voor:
- het beheer van de sessie (herstel, opslag)
- controle op de geldigheid van de aangevraagde acties
- de uitvoering van het aan de actie gekoppelde script
- het verzenden van het juiste antwoord naar de client, afhankelijk van de uitkomst van de actie