Controllers, Models en Views

Dit is het vijfde deel van een serie over de bouw van een nieuwe website op basis van Zend Framework. Kijk in de zijbalk voor de linkjes naar de eerste delen.

Zoals we al eerder hebben gezegd, vormen controllers de tussenlaag tussen het model (waar de logica van de applicatie wordt uitgevoerd) en de views (waar zich de presentatie naar de buitenwereld afspeelt). Er heerst de laatste jaren een consensus in kringen van software architecten dat het het best is om te streven naar skinny controllers and fat models: houd de controllers klein en overzichtelijk, en verplaats zoveel mogelijk code naar de models.

Een nieuwe pagina maken

We wijken even af van onze elesio-applicatie om met een simpel voorbeeld aan te tonen hoe we een nieuwe pagina maken in onze website. Open application/views/scripts/index/index.phtml en plaats onder je bestaande code een linkje:

[codesyntax lang=”html4strict” title=”application/views/scripts/index/index.phtml”]

<p><a href='/index/calculate'>Voer een berekening uit</a></p>

[/codesyntax]

Klik je daar nu op, dan zul je een foutmelding te zien krijgen. Dat gaan we snel verhelpen. Open een command-prompt, navigeer naar je projectmap, en geef dit commando op:

[codesyntax lang=”bash” title=”Command Line”]

php bin/zf.php create action calculate

[/codesyntax]

Als het goed is, zul je de boodschap terugkrijgen dat er code is toegevoegd aan je indexcontroller, en dat er een view script is aangemaakt. Open nu /application/controllers/IndexController.php: daar zie je dat de methode CalculateAction is toegevoegd aan de klasse. En in de map /application/views/scripts/index is een nieuw bestand aangemaakt: calculate.phtml.

Als je nu je elesio-pagina ververst en op het linkje klikt dat je zojuist hebt aangemaakt, zal de foutmelding worden vervangen door een mededeling over het view script dat is aangemaakt. Die melding vind je terug in het bestand calculate.phtml.

Wat is er nu precies gebeurd? Als je de structuur van de link bekijkt, zie je dat er vanaf de docroot een pad wordt opgegeven ‘index/calculate’. Maar het vreemde is natuurlijk dat dit pad niet correspondeert met een werkelijk bestaande map op de webserver. En hier treedt de magie op van Zend Framework en het .htaccess bestand. Zoals eerder gemeld zorgt dat er immers voor dat elke request naar een niet werkelijk bestaand bestand of map wordt doorgesluisd naar de applicatie. Binnen ZF wordt de request ontleed in zijn bestanddelen: het eerste deel, index, verwijst naar de IndexController; het tweede deel, calculate, verwijst naar de CalculateAction.

Zo werkt het altijd. Een link /blog/view vereist in principe dat er een BlogController is met een View Action. Een link /news/new heeft een NewsController en een NewAction nodig.

Daarnaast moeten ook de bijbehorende view scripts worden ingesteld. De link blog/view zoekt naar een pad application/views/scripts/blog/view.phtml, en /news/new werkt met application/views/scripts/news/new.phtml. Het handige van Zend_Tool is dat deze component je het werk uit handen neemt om handmatig die nieuwe methoden en bestanden aan te maken. Zend_Tool doet dat voor je.

Type nog eens een niet bestaande url in je browser: dev.elesio.local/dik/dun bijvoorbeeld. De foutmelding die je te zien krijgt (dezelfde als zojuist), is nu ineens veel duidelijker. Page not found is natuurlijk logisch; Invalid controller specified (dik) is nu ook helder. Pas als je een DikController hebt gemaakt, met daarin een dunAction, en een applications/views/scripts/dun.phtml viewscript zal de foutmelding verdwijnen.

Open calculate.phtml en voer daarin deze code in:

[codesyntax lang=”html4strict” title=”application/views/scripts/index/calculate.phtml”]

<h1>Een Elesio rekenmachine</h1>

<p>Geef twee getallen in, en kies welke bewerking je wilt uitvoeren

<form method=”get”>

<fieldset legend=”rekenmachine”>

<p><label for=’x’>Getal 1: <input type=’text’ name=’x’ /></label></p>

<p><label for=’y’>Getal 1: <input type=’text’ name=’y’ /></label></p>

<p><label><input type=’radio’ name=’calc’ value=’add’ />Optellen</label></p>

<p><label><input type=’radio’ name=’calc’ value=’subtract’ />Aftrekken</label></p>

<p><label><input type=’radio’ name=’calc’ value=’multiply’ />Vermenigvuldigen</label></p>

<p><label><input type=’radio’ name=’calc’ value=’divide’ />Delen</label></p>

<p><input type=’submit’ value=’Start berekening’ /></p>

</fieldset>

</form>

</p>

[/codesyntax]

Een simpel voorbeeldje van een rekenmachine. De gebruiker wordt gevraagd om twee getallen in te vullen in een formulier, een bewerking uit te kiezen, en vervolgens de berekening te laten uitvoeren. Als je het formulier nu invult, gebeurt er niets. Daar gaan we nu verandering in aanbrengen.

De controller aanpassen

De calculateAction in de IndexController is nu nog leeg. Daarom doet een druk op de knop ook niets: het formulier post naar zichzelf, maar het enige wat er gebeurt is dat de pagina wordt ververst; met de input van het formulier wordt niets gedaan. Hoe kunnen we dat nu gaan aanpassen?

Zet deze code in je calculateAction:

[codesyntax lang=”php” title=”application/controllers/indexController.php”]

<?php

$xParam = (float) $this->_request->getParam(‘x’, 0);

$yParam = (float) $this->_request->getParam(‘y’, 0);

$action = $this->_request->getParam(‘calc’, ”);

$result = ”;

if (in_array($action, array(‘add’, ‘subtract’, ‘multiply’, ‘divide’))) {

$calc = new Model_Sb_Calc($xParam, $yParam);

try {

$result = $calc->{$action}();

} catch (Exception $e) {

$result = $e->getMessage();

}

}

if (! empty($result)) {

$this->view->message = “Het resultaat van je berekening is:”;

$this->view->result = $xParam . $action . $yParam . ‘ = ‘ . $result;

} else {

$this->view->message = “Geef twee getallen in, en kies welke bewerking je wilt uitvoeren”;

}

[/codesyntax]

In de eerste drie regels wordt het request onderzocht op de aanwezigheid van parameters die mogelijk met het versturen van het formulier zijn meegekomen. In elke controller heb je toegang tot het request object als een lokale variabele via $this->_request (of $this->getRequest()). Het request biedt een flink aantal methoden om zijn aard te onderzoeken. $this->_request->isPost() bepaalt bijvoorbeeld of het request een post is.

Wat we in de voorliggende code doen, is het request onderzoeken op de aanwezigheid van een x-, een y- en een calc-parameter. Als die aanwezig zijn, mogen we concluderen dat het formulier is ingevuld en willen we iets doen met de ingegeven waarden. Als die niet aanwezig zijn, dan hoeven we simpelweg het formulier te tonen.

De parameters worden met $this->_request->getParam naar een lokale variabele omgezet. Deze methode biedt het voordeel dat, mocht de parameter niet aanwezig zijn, er een default waarde kan worden toegekend. Dat is het tweede argument in de methode getParam. Zo krijgen we binnen de calculateAction methode drie lokale variabelen. Indien die empty zijn, hoeven we niets te doen. Zijn ze gezet, dan moeten we controleren of de calc-parameter een toegestane actie bevat.

Deze functionaliteit steken we in het grote if-statement, halverwege de calculateAction. Let op: dit is dus typische controller-functionaliteit: het request onderzoeken, analyseren of er geen kwaadwillende input van de gebruiker in voorkomt, en pas daarna actie ondernemen om de juiste view te genereren.

Laten we ervan uitgaan dat de parameters ingevuld zijn, en dat de gevraagde bewerking toegestaan is. Je ziet dat er in de volgende regel een nieuw object $calc wordt aangemaakt. In de constructor van $calc worden twee argumenten meegegeven, het eerste en het tweede door de gebruiker ingegeven getal.

Een model maken

Dit object $calc is onze kleine rekenmachine. Dit is de code:

[codesyntax lang=”php” title=”application/models/sb/Calc.php”]

<?php

class Model_Sb_Calc

{

private $_x;

private $_y;

public function __construct($x = 0, $y = 0){

$this->setX($x);

$this->setY($y);

}

public function setX($x)

{

$this->_x = $x;

}

public function setY($y)

{

$this->_y = $y;

}

public function add()

{

return $this->_x + $this->_y;

}

public function subtract()

{

return $this->_x - $this->_y;

}

public function multiply()

{

return $this->_x * $this->_y;

}

public function divide()

{

if ($this->_y == 0) throw new Exception("Delen door nul is onmogelijk");

return $this->_x / $this->_y;

}

}

[/codesyntax]

Zoals je ziet is het een simpele klasse, met twee private variabelen die ofwel in de constructor gezet kunnen worden, ofwel met twee set-methoden. Verder zijn er vier publieke bewerkingen, waarvan alleen de divide-methode enigszins interessant is: hier wordt gecheckt of de y-property van het object niet 0 is. Indien wel, dan wordt er een exceptie gegooid.

Het eigenaardige zit ‘m in de naam van het model: Model_Sb_Calc. Herinner je je hoe ik in een vorig hoofdstuk sprak over de namespaces die Zend toepast? We willen graag dat onze applicatie al onze bestanden automatisch kan vinden, zonder dat we met include of require statements moeten werken. Door deze naam te gebruiken, kan ZF onze zelf gebouwde klassen probleemloos vinden. Daarvoor hanteren we de conventie dat de naam van de klasse overeenkomt met het pad ernaar toe. Dit betekent dat we de klasse moeten aanmaken in een nieuwe submap sb in de map models.

Sb is een naam die ik zelf heb bedacht (het staat voor sandbox, zandbak – beschouw het maar als onze speeltuin waarin we nieuwe dingetjes kunnen uitproberen.

Voordat Zend onze nieuwe klasse kan vinden, moeten we nog een kleine kunstgreep uithalen: we moeten ZF vertellen dat we gebruik willen maken van de automatische namespacing. Dat doen we in bootstrap.php. Voeg aan dat bestand deze nieuwe methode toe:

[codesyntax lang=”php” lines=”fancy” tab_width=”5″ title=”application/bootstrap.php”]

<?php

protected function _initAutoload()

{

$autoloader = new Zend_Application_Module_Autoloader(array(

'namespace'=>'',

'basePath'=>dirname(__FILE__)));

return $autoloader;

}

[/codesyntax]

Zoals eerder gemeld, zal onze applicatie automatisch alle protected en public methods uitvoeren binnen bootstrap.php die beginnen met _init. Deze methode vertelt onze applicatie dat we een lege namespace willen toevoegen, waarin automatisch alle bestanden worden geladen die zich ergens onder de application directory (dirname(__FILE__)) bevinden.

Zoals je ziet, zit de logica van de applicatie in het model, en niet in de controller.

Terug naar de controller, en dan de view

Nu we een rekenmachine hebben gemaakt, rest ons nog de gevraagde bewerking te laten uitvoeren door ons $calc-object. De gevraagde bewerking zit in onze variabele $action, en omdat we erop hebben gelet dat de namen van de bewerkingen overeenkomen met de namen van de methoden in $calc, kunnen we de syntax $calc->{$action} gebruiken. Let wel op dat je dit alleen maar doet als je hebt gecontroleerd dat er geen illegale bewerkingsnaam wordt doorgevoerd. De controle vindt plaats via een soort whitelist, die in ons if-statement plaatsvindt in de expressie in_array().

De aanroep van onze methode heb ik omgeven met een try-catch blok. Als er wordt gevraagd om een deling uit te voeren, terwijl de y-parameter 0 is, gooit onze rekenmachine immers een exceptie; en die moet wel worden opgevangen.

In elke controller is het view-object beschikbaar als een property van het controller-object. Daarom kun je het benaderen met $this->view. Aan deze view kun je vervolgens willekeurige properties toekennen. Vaak zullen dat strings zijn, zoals in dit geval. Als er een resultaat komt uit een bewerking van onze rekenmachine (dat mag ook een foutboodschap zijn bij delen door 0), wordt dit resultaat opgeslagen in een property $this->view->result. Ook wordt er een message property gezet: $this->view message. Als er geen bewerking heeft plaatsgevonden, dan volgt een uitnodiging om een paar getallen in te voeren; anders wordt het resultaat weergegeven.

Het enige wat ons nu nog rest, is het aanpassen van ons viewscript. Alles staat er nu nog hard gecodeerd in; er moet ruimte worden gemaakt voor onze result- en message properties.

Pas de code in views/scripts/index/calculate.phtml als volgt aan:

[codesyntax lang=”php” lines=”normal” title=”application/views/scripts/index/calculate.phtml”]

<?php
?>
<h1>Een Elesio rekenmachine</h1>

<p><?php echo $this->message;

if (isset($this->result)) {

echo "<p>" . $this->result . "</p>";

} else {

?>

<form method="get">

<fieldset legend="rekenmachine">

<p><label for='x'>Getal 1: <input type='text' name='x' /></label></p>

<p><label for='y'>Getal 1: <input type='text' name='y' /></label></p>

<p><label><input type='radio' name='calc' value='add' />Optellen</label></p>

<p><label><input type='radio' name='calc' value='subtract' />Aftrekken</label></p>

<p><label><input type='radio' name='calc' value='multiply' />Vermenigvuldigen</label></p>

<p><label><input type='radio' name='calc' value='divide' />Delen</label></p>

<p><input type='submit' value='Start berekening' /></p>

</fieldset>

</form>

<?php

}

?>

</p>

[/codesyntax]

Dit is simpel te lezen. Als er een result is, wordt dit weergegeven; zo niet, dan moet het formulier worden getoond. Hoe dan ook beginnen we met de boodschap.

Tot besluit

In dit hoofdstuk hebben we gezien hoe controller, model en view met elkaar samenwerken. In de controller pak je het request op, controleert het, en geeft de benodigde data door aan het model. Het model bewerkt die data of haalt nieuwe data op, en geeft die terug aan de controller. De controller geeft deze gegevens vervolgens door naar de view.

In het volgende hoofdstuk gaan we dieper in op de modellen die we willen gebruiken, en zullen we Zend’s database functionaliteit nader gaan onderzoeken.