Formulieren en gegevensverwerking

Dit is het zevende 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.

Een van de meer gecompliceerde componenten van het Zend Framework is Zend_Form. Het duurt even voordat je doorhebt hoe de verschillende onderdelen van Zend_Form met elkaar samenwerken om het de developer uiteindelijk gemakkelijk te maken. Maar als je eenmaal die steile leercurve hebt doorlopen, zul  je merken dat je inspanningen niet voor niets zijn geweest.

Wat Zend_Form gecompliceerd maakt, is de veelheid van verschillende taken die eraan worden toegekend:

  • Het moet op eenduidige wijze formulieren weergeven in de pagina’s
  • Het moet gebruikersinput filteren en valideren
  • Het moet kunnen omgaan met de verschillende formulierelementen die in de verschillende HTML-specificaties zijn opgenomen.

Voor de weergave van formulieren worden decorators gebruikt; voor filtering en validatie filters en validators; en voor de verschillende elementen een deelcomponent die Zend_Form_Element wordt genoemd.

Zend_Form nader bekeken

Het belangrijkste dat je moet weten van Zend_Form is dat je deze component nooit rechtstreeks benadert. Je ontwerpt je eigen formulierklassen die een child vormen van Zend_Form. Binnen je formulierklasse hoef je geen aparte constructor te maken; sterker, het is aan te bevelen om dat niet te doen, omdat je dan een aantal standaardinstellingen van Zend_Form kunt overschrijven.

Zend_Form levert een hook, een lege methode die vanuit de constructor wordt aangeroepen, die je kunt gebruiken om je formulier te initialiseren. Die methode wordt, heel toepasselijk, ‘init’ genoemd. Je standaard formulier zal dus meestal zo’n opbouw kennen:

[codesyntax lang=”php” title=”Default formulier code”]

<?php
class Form_Registration extends Zend_Form
{
	public function init()
	{
		// hierin de code om je formulier op te bouwen
	}
}

[/codesyntax]

Het eerste wat opvalt, is dat de naamgeving van onze klasse voldoet aan de conventies zoals we die inmiddels uit ZF kennen. De naam Form_Registration impliceert dat we een nieuwe folder hebben gemaakt in onze applicatiemap, en die folder noemen we forms. Zoals de naam al zegt wordt dit een registratieformulier voor onze nieuwe gebruikers.

Waarom geen Form? Dat heeft te maken met een ZF-eigen conventie. Misschien is het je opgevallen dat onze Model_DbTable_User ook niet in een folder Model staat, maar in de folder models – let op: het is belangrijk dat je de kapitalisering in orde hebt, want een hoofdletter waar een kleine letter hoort te staan, brengt je applicatie in de war; ten minste, als ze op een Linux server draait. Op een Windows systeem maakt het niets uit.
De autoloader implementatie van Zend, zoals die in onze bootstrap.php gedefinieerd staat, mapt klassen die met Form beginnen op de folder ‘forms’, en de klassen die met Model beginnen op ‘models’. In de map plugins komen klassen die beginnen met ‘Plugin’, in services klassen die beginnen met ‘Service’. Hierover later meer.

We hebben een vooralsnog lege methode init() gedefinieerd die we zo gaan vullen. Deze methode wordt aangeroepen door de constructor van de moederklasse, Zend_Form. Init() wordt gebruikt om de elementen aan ons formulier toe te voegen en hun eigenschappen te bepalen. Dit is onze initiële code:

[codesyntax lang=”php” lines=”normal” title=”applications/forms/Registration.php”]

<?php
class Form_Registration extends Zend_Form {

	public function init()
	{
	$this->addElement('text', 'userName', array(
		'label'=>'Gebruikersnaam',
		'required'=>true,
		'description'=>'Een unieke gebruikersnaam tussen de 4 en de 30 lettertekens, mag alleen letters en cijfers bevatten',
		'validators'=>array(
		array('StringLength',false,array(4,30)),
		array('Alnum', false))
	));

	$this->addElement('text','email', array(
		'label'=>'Emailadres',
		'required'=>true,
		'validators'=>array(
		array('EmailAddress',false))
	));

	$this->addElement('password', 'password', array(
		'label'=>'Wachtwoord',
		'required'=>true,
		'description'=>'Uw wachtwoord moet tussen de 5 en 10 lettertekens bevatten',
		'validators'=>array(
		array('StringLength',false,array(5,10)))
		));

	$this->addElement('password', 'password2', array(
		'label'=>'Herhaal uw wachtwoord',
		'required'=>true,
		'description'=>'Uw wachtwoord nog een keer invoeren om te vermijden dat u per ongeluk maakt',
		'validators'=>array(
		array('StringLength',false,array(5,10)))
		));

	$this->addElement('text','firstName', array(
		'label'=>'Voornaam',
		));
	$this->addElement('text','lastNamePrefix', array(
		'label'=>'Tussenvoegsel',
		));
	$this->addElement('text', 'lastName', array(
		'label'=>'Achternaam',
		));
	$this->addElement('file', 'profilePic', array(
		'label'=>'Uw profielfoto',
		'destination'=>Znd_Registry::get('uploadDir'),
		'validators'=>array(
		array('Extension', false, array('jpg','png','gif')),
		array('Size', false, 500000)
	)));

	$this->addElement('submit','submit', array(
		'label'=>'Registreren',
		'ignore'=>true
		));

	$this->setMethod('post')
		->setEnctype(Zend_Form::ENCTYPE_MULTIPART)
		->setAction($this->getView()->url(array('controller'=>'user','action'=>'index')))

		->setAttrib('class', 'feForm');

	$this->setElementFilters(array('StripTags','StringTrim'));
	}

[/codesyntax]

We voegen elementen toe aan het formulier met de methode addElement, een protected methode van Zend_Form. AddElement accepteert drie argumenten:

  • Het eerste argument is het type van het element, bijvoorbeeld text, textarea, select, checkbox of radio.
  • Het tweede argument is de naam van het element. Hierin heb je een vrije keuze: de naam komt in de weergave van het formulier terug als zowel het attribuut name als het attribuut id, dus zorg er wel voor dat je namen voldoen aan de eisen die W3C stelt. Verder is het ook praktisch om de namen te laten overeenkomen met de kolommen van je database-tabellen; dat scheelt later werk als je de input van de gebruiker in de tabel wilt plaatsen.
  • Het derde argument is een associatieve array van opties. Meestal definieer je in deze array in ieder geval het label, maar ook de filters en validators die je wilt toepassen, de weergave van het element in het formulier, en eventuele andere attributen. Een belangrijke optie hier is ook ‘required’: stel je deze optie in op true, dan is een waarde vereist en wordt het formulier afgekeurd als de gebruiker het veld leeg laat.

Een van de opties die je kunt meegeven zijn de validators die Zend_Form moet toepassen op het moment dat de gebruiker op de ‘verzend’ knop klikt. Van het element ‘userName’ zeggen we bijvoorbeeld dat deze uit ten minste 4 en ten hoogste 30 tekens moet bestaan. Bovendien worden alleen alfanumerieke tekens geaccepteerd. Bij het element ‘email’ willen we met de validator controleren of de gebruiker daadwerkelijk een emailadres heeft ingevoerd. De profielfoto moet een extensie hebben van jpg, png of gif, en mag niet zwaarder zijn dan een halve megabyte.

Het file-element (gebruikt voor het uploaden van een profielfoto) heeft een extra optie die andere elementen niet hebben: de destination bepaalt waar het geuploade bestand terecht komt. Hiervoor doen we een beroep op het centrale register dat in onze bootstrap.php wordt gedefinieerd. Daarin bepalen we de upload-map met deze code:

[codesyntax lang=”php” lines=”normal” title=”application/bootstrap.php”]

<?php
/**
* Prepares the folders where we store user uploads
*/

protected function _initUserUploads() {
	$uploadURI = '/uploads/' . date("Y") . '/' . date('m');
	$uploadDir = BASE_PATH . '/public' . $uploadURI;
	if (!file_exists($uploadDir)) {
		mkdir($baseDir, 0777, true);
	}
	Zend_Registry::set('uploadDir', $uploadDir);
	Zend_Registry::set('uploadURI', $uploadURI);
}

[/codesyntax]

Zoals inmiddels bekend worden methoden die met _init beginnen in onze bootstrap automatisch aangeroepen tijdens het opstartproces van onze applicatie. De methode _initUpload bepaalt in welke map onze gebruikersbestanden moeten terechtkomen. We willen dat liever niet statisch definiëren om te vermijden dat er na enkele jaren gebruik een map met vele duizenden bestanden ontstaat. Linux kan veel hebben, maar het filesysteem is efficiënter met kleinere mappen. Wat we hier doen is:

  • We bepalen de uri aan de hand van het huidige jaar en de lopende maand; dit garandeert dat er elke maand een nieuwe map wordt aangemaakt volgens een logische structuur.
  • Als we de uri hebben, kunnen we daar gemakkelijk het filepath voor plakken, nodig voor het verwerken van de uploads
  • Als dit filepath niet bestaat, wordt het aangemaakt met maximale permissies voor uploaden
  • En ten slotte worden zowel uploadmap als uri in het register geplaatst.

In het submit element wordt een optie ‘ignore’ op true gezet: dit betekent dat Zend_Form bij de verwerking van de gebruikersgegevens dit element mag overslaan.

Als alle elementen zijn toegevoegd, geven we het formulier zelf nog enkele eigenschappen mee. We bepalen zijn method (post in dit geval) en zijn enctype (multipart omdat we ook gebruik willen maken van de upload-functionaliteit!) en geven bovendien een klasse-attribuut mee. Het action-attribuut bepalen we ten slotte met behulp van de view, die in Zend_Form beschikbaar is. Wat we hier eigenlijk zeggen is dat we nog niet helemaal zeker weten wat exact de url gaat zijn waarnaar dit formulier wordt gepost, maar wel dat de post afgehandeld moet worden door de indexAction van de UserController. Dat biedt ons de mogelijkheid om ons nog niet definitief vast te leggen op een url-structuur.

Ten slotte willen we alle elementen voorzien van hetzelfde setje filters: we willen dat de user input wordt ‘getrimd’, dat wil zeggen van witruimte aan het begin en het eind wordt ontdaan, en dat alle tags gestript worden. Dat verhoogt de veiligheid van het formulier.

Ons formulier tonen en verwerken

Nu we weten hoe we ons formulier opbouwen, kunnen we het gaan integreren in onze controllers en in onze view. Het lijkt me handig dat we alle handelingen die met het gebruikersbeheer van doen hebben, in een aparte controller stoppen, de UserController. De indexAction wordt als volgt gevuld:

[codesyntax lang=”php” lines=”normal” title=”application/controllers/UserController.php”]

<?php
public function indexAction()
{
	$this->view->form = $form = new Form_Registration();
	if ($this->_request->isPost()) {
		if ($form->isValid($this->_request->getPost())) {
			$data = $form->getValues();
			$tblUser = new Model_DbTable_User();
			$tblUser->insert($data);
		}
	}
}

[/codesyntax]

In de eerste regel wordt een nieuwe instantie van ons formulierobject aangemaakt en in de variabele $form gestopt. We wijzen deze instantie tegelijkertijd toe aan een formproperty van ons view-object.

Vervolgens bekijken we of de request voor de pagina het gevolg is van een post. Als dat zo is, dan willen we de gebruikersinput (die reeds gefilterd is met de addElementFilters methode van Zend_Form) valideren. Dat doen we in het if-statement: de isValid-method van Zend_Form accepteert een array van waarden, die we hier rechtstreeks uit het request halen.

De gevalideerde en gefilterde waarden stoppen we vervolgens in een nieuwe array $data. De logische volgende stap is om deze array op te slaan in onze database; en omdat er een één-op-één relatie bestaat tussen de namen van de formuliervelden en de kolommen in de usertabel, volstaan twee regels code om dit in orde te brengen: we maken een nieuwe instantie van onze Usertabel aan, en met een simpele insert-methode stoppen we onze schone data in de gebruikerstabel. Een kind kan de was doen!

Mocht de gebruiker nu ongeldige gegevens in het formulier gestopt hebben, bijvoorbeeld een e-mailadres dat niet klopt of een gebruikersnaam die te veel of te weinig tekens bevat, wordt het formulier opnieuw getoond met de bijbehorende foutmeldingen. We hoeven daarvoor verder niets te doen;  Zend_Form regelt het allemaal! De foutmeldingen verschijnen nu nog in het Engels, maar in een later stadium leren we hoe we die meldingen kunnen aanpassen en vertalen.

In het corresponderende view-bestand (application/views/scripts/user/index.phtml) volstaat het om wat inleidende html te tikken en vervolgens met

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

<?php
echo $this->form;
?>

[/codesyntax]

het formulier te tonen op de pagina.

Opmaak aanpassen met decorators

Als je naar de broncode kijkt van onze formulier pagina, dan zie je dat Zend_Form standaard een definition list (‘dl’) gebruikt om de formuliervelden weer te geven. De labels van de formulierelementen worden getoond in een ‘dt’ tag, de elementen zelf met ‘dd’. Dat is een prima default instelling die met stylesheets vrijwel volledig naar je wens is aan te passen, maar het komt natuurlijk vaak voor dat je andere tags wilt gebruiken. Gelukkig biedt Zend_Form de mogelijkheid om met behulp van decorators de code naar believen aan te passen.

Decorators vormen een ontwerppatroon dat vaak wordt gebruikt om een standaardobject uit te breiden (te decoreren) met extra functionaliteit. Een vaak gebruikt voorbeeld is die van de koffie die je in het café bestelt. Het standaardobject is hier de simpele kop koffie. Maar als je koffie met melk wilt, moet er een nieuw object gemaakt worden, koffie met melk genaamd. Wat er dan gebeurt is dat je het standaardobject ‘koffie-simpel’ als argument meegeeft aan de ‘melk’-constructor. Dan heb je koffie met melk. Wil je er slagroom en suiker en een scheut whiskey bij, dan pas je dit principe nog enkele keren toe. Bij elke toepassing wordt de ‘koffie-simpel’ ‘gedecoreerd’ met een extra object.

Zo werkt ook Zend_Form_Decorator. De simpelste decorator heet ViewHelper en doet niets meer dan het gevraagde element weergeven: <input> of <select>. Er worden geen tags omheen gezet, maar wel worden de benodigde attributen in het element geplaatst: het type (type=’text’ bijvoorbeeld), de id, de name, enzovoorts.

Wil ik een tag daaromheen plaatsen, dan voeg ik een decorator toe. De meest gebruikte decorator is HtmlTag en die doet precies wat de naam belooft. Hij plaatst een tag om het element heen. Andere veel gebruikte tags zijn Label, Errors en Description.

Ik streef in mijn formulieren naar een standaard opmaak, die uiteindelijk deze broncode zou moeten opleveren. Geen definition list meer dus!

[codesyntax lang=”php” lines=”fancy” title=”Zo moet het worden”]

<form enctype="multipart/form-data" accept-charset="utf-8" method="post" action="/user" class="feForm">
<div class="gjbform">
<p class="gjbformelement"><label for="userName" class="required">Gebruikersnaam<span class="alert">*</span></label>
<input name="userName" id="userName" value="" type="text">
</p><p class="hint">Een unieke gebruikersnaam tussen de 4 en de 30 lettertekens, mag alleen letters en cijfers bevatten</p>
<p class="gjbformelement"><label for="email" class="required">Emailadres<span class="alert">*</span></label>
<input name="email" id="email" value="" type="text"></p>
<p class="gjbformelement"><label for="password" class="required">Wachtwoord<span class="alert">*</span></label>
<input name="password" id="password" value="" type="password">
</p><p class="hint">Uw wachtwoord moet tussen de 5 en 10 lettertekens bevatten</p>
<p class="gjbformelement"><label for="password2" class="required">Herhaal uw wachtwoord<span class="alert">*</span></label>
<input name="password2" id="password2" value="" type="password">
</p><p class="hint">Uw wachtwoord nog een keer invoeren om te vermijden dat u per ongeluk maakt</p>
<p class="gjbformelement"><label for="firstName" class="optional">Voornaam</label>
<input name="firstName" id="firstName" value="" type="text"></p>
<p class="gjbformelement"><label for="lastNamePrefix" class="optional">Tussenvoegsel</label>
<input name="lastNamePrefix" id="lastNamePrefix" value="" type="text"></p>
<p class="gjbformelement"><label for="lastName" class="optional">Achternaam</label>
<input name="lastName" id="lastName" value="" type="text"></p>
<p class="gjbformelement"><label for="profilePic" class="optional">Uw profielfoto</label>
<input name="MAX_FILE_SIZE" value="2097152" id="MAX_FILE_SIZE" type="hidden">
<input name="profilePic" id="profilePic" type="file"></p>
<p class="gjbbuttonformelement">
<input name="submit" id="submit" value="Registreren" type="submit"></p></div>
</form>

[/codesyntax]

Hier worden zowel form_decorators als element_decorators gebruikt. De belangrijkste form_decorator is ‘div’. Alle elementen worden binnen een div geplaatst die een specifieke klasse heeft meegekregen.

Elk formulierelement wordt omgeven door ‘p’-tags, elk van de klasse ‘gjbformelement’. Labels krijgen een klasse ‘optional’ of ‘required’; zo kunnen we in ons stylesheet de verplichte elementen een andere opmaak meegeven. Als we bij een element een nadere uitleg nodig hebben, voegen we een extra p-tag toe met de klasse ‘hint’. Wat hier niet wordt weergegeven, is wat er gebeurt als fouten moeten worden getoond na validatie van het formulier. Fouten worden gepresenteerd als een unordered list (ul en li-tags).

Hoe kunnen we dit bereiken? En vooral? Hoe kunnen we dit het beste aanpakken zonder dat we elk nieuw formulier op onze site moeten voorzien van onze aangepaste decorators? De simpelste manier is om natuurlijk een eigen klasse Form_Standard te maken. Onze formulieren zijn dan niet langer children van Zend_Form, maar van een klasse GJB_Form_Standard.

Dit vergt natuurlijk wat aanpassingen in onze code. Op de eerste plaats moeten we ons afvragen of die standaard formulierklasse die we gaan maken, hergebruikt moet worden in andere applicaties. Dat lijkt me voor de hand te liggen; de meeste ontwikkelaars hebben een eigen patroon ontwikkeld voor het weergeven van standaardelementen zoals formulieren; laten we nu ons standaardformulier zo abstract maken, dat het gemakkelijk in andere applicaties in te zetten is.

Zoals we weten plaatsen we applicatie-overstijgende code in de map library, waar ook het Zend Framework zelf staat. Verder staat er nog niets. Het eerste wat we doen is een nieuwe folder aanmaken, GJB getiteld – naar believen aan te passen aan je eigen initialen. Wat we hiermee feitelijk doen, is een nieuwe namespace creëren. Dat wil zeggen dat onze klassenamen die binnen GJB worden aangemaakt, allemaal beginnen met de letters GJB. Vervolgens kunnen ze, analoog aan de gewoonte binnen Zend Framework, de folderstructuur kopiëren.

Binnen de nieuwe map GJB maken we een map Form, en daarbinnen een bestand Standard.php. In dit bestand gaan we onze nieuwe klasse definiëren, getiteld GJB_Form_Standard. De code van deze klasse is:

[codesyntax lang=”php” lines=”normal” title=”library/GJB/Form/Standard.php”]

<?php
class GJB_Form_Standard extends Zend_Form
{
protected $_elmDecorator = array(
array(
'ViewHelper',
array(
'class' => 'gjbformelement')),
array(
'Label',
array(
'requiredSuffix' => '<span class='alert'>*</span>',
'escape' => false)),
'Errors',
array(
'Description',
array(
'escape' => false)),
array(
'HtmlTag',
array(
'tag' => 'p',
'class' => 'gjbformelement')));
protected $_fileDecorator = array(

array(

'File',

'ViewHelper',

array(

'tag' => 'span',

'class' => 'gjbformelement')),

array(

'Label',

array(

'requiredSuffix' => '<span class='alert'>*</span>',

'escape' => false)),

array(

'Errors'),

array(

'Description',

array(

'escape' => false)),

array(

'HtmlTag',

array(

'tag' => 'p',

'class' => 'gjbformelement')));

protected $_radioDecorator = array(

array(

'ViewHelper',

array(

'tag' => 'span',

'class' => 'gjbformelement')),

array(

'Label',

array(

'requiredSuffix' => '<span class='alert'>*</span>',

'escape' => false,

'tag' => 'span',

'placement'=>'APPEND',

'class' => 'gjbradioformelement')),

'Errors',

array(

'Description',

array(

'escape' => false)),

array(

'HtmlTag',

array(

'tag' => 'p',

'class' => 'gjbformelement radio')));

protected $_buttonDecorator = array(

'ViewHelper',

array(

'HtmlTag',

array(

'tag' => 'p',

'class' => 'gjbbuttonformelement')));

protected $_hiddenDecorator = array(

'ViewHelper');

protected $_formDecorator = array(

'FormElements',

array(

'HtmlTag',

array(

'tag' => 'div',

'class' => 'gjbform')),

'Form');

public function __construct ($options = null)

{

$this->setDisableLoadDefaultDecorators(true);

$this->setDecorators($this->_formDecorator);

$this->setAttrib('accept-charset', 'utf-8');

parent::__construct($options);

}

}

?>

[/codesyntax]

Wat we feitelijk doen in deze klasse, is het definiëren van een aantal standaard decorators die de plaats innemen van de Zend_Decorators. Dit zijn de protected arrays die we voorin de klassedefinitie zien.

Laten we als voorbeeld de $_elmDecorator nemen. Elke decorator is een array. Als we die toepassen op een element, zien we dat als eerste de ViewHelper decorator wordt aangeroepen: het element zelf. De ViewHelper decorator krijgt een css-klasse van gjbformelement. Het element moet natuurlijk ‘gedecoreerd’ worden met een label decorator. Deze array krijgt twee belangrijke opties mee: requiredSuffix is een standaardoptie die het je toestaat om de label van de verplichte velden te voorzien van extra informatie; in de regel wordt een rood sterretje gebruikt om deze verplichting aan te duiden. De tweede optie is de escape-optie; die zetten we op ‘false’. We zeggen daarmee tegen deze decorator dat hij html die in de label wordt gebruikt niet hoeft te escapen; deze html mag geïnterpreteerd en gebruikt worden.

De derde decorator die elk element meekrijgt is Errors – als we die zouden weglaten, zouden we geen foutmeldingen  kunnen tonen. Aan Errors worden geen aanpassingen verricht; we nemen dus de standaard Zend_Form_Decorator_Errors over. De vierde decorator, Description, nemen we eveneens over met slechts een kleine aanpassing: ook in deze decorator willen we graag html toestaan, dus we zetten ‘escape’ op ‘false’.

Het geheel (dus het element plus label plus errors plus description) wordt vervolgens voorzien van een HtmlTag decorator, met de opties tag (p) en css-klasse (gjbformelement).

Zo, daarmee hebben we onze eerste decorator nu gedefinieerd. Die kunnen we toepassen op elk willekeurig element. Maar waarschijnlijk hebben we voor verschillende elementen net iets andere decorators nodig; een radiobutton of een file-uploadveld willen we misschien op iets andere manier vormgeven. Daarom hebben we nog een aantal extra decorators geformuleerd, waarvan een opvallende $_hiddenDecorator is. Die gebruiken we voor verborgen formuliervelden. Die hebben geen tag nodig, geen label; het enige dat we wel nodig hebben is de ViewHelper. Een andere opmerkelijke is  de $_buttonDecorator, zonder label, errors of description. En ten slotte de $_formDecorator. Onze intentie is om deze decorator niet op afzonderlijke elementen te plaatsen, maar op het formulier als geheel.

Dat doen we in de constructor van deze klasse.  In de eerste regel vertellen we Zend_Form dat we, nee dank u, geen gebruik willen maken van de default decorators. In regel twee voegen we onze net gedefinieerde $_formDecorator toe aan het formulier. En in regel drie zetten we een standaardattribuut voor elk formulier dat we willen gebruiken, de charset.  Omdat we een eigen constructor definiëren, moeten we niet vergeten de parent (Zend_Form::__construct()) aan te roepen: dat doen we in de laatste regel van de constructor.

Daarmee hebben we onze eigen formulierdefinitie aangemaakt die we vanaf nu willen gebruiken. Voordat het echter zover is, moeten we onze applicatie eerst nog vertellen dat-ie vanaf nu een nieuwe namespace moet onderzoeken bij zijn autoloading-proces. Daartoe volstaat een enkele extra regel in application/configs/application.ini:

[codesyntax lang=”bash”]

autoloaderNamespaces.GJB[] = "GJB_"

[/codesyntax]

Nu kunnen we ons registratieformulier gaan aanpassen.  In plaats van een extensie van Zend _Form, is het nu een child van GJB_Form_Standard. Daarmee komen onze eigen decorators beschikbaar, en wordt automatisch in ieder geval al de charset gezet. Als we willen kunnen we nu elk element langslopen en in de array options aangeven welke decorator we willen:

[codesyntax lang=”php”]

<?php
'decorators'=>$this->_elmDecorator,

[/codesyntax]

maar er is een veel handiger manier beschikbaar.

Plaats aan het einde van onze init()-methode deze code:

[codesyntax lang=”php” title=”application/forms/Registration.php”]

<?php
$this->setElementDecorators($this->_elmDecorator);
$this->setElementDecorators($this->_fileDecorator, array('profilePic'));
$this->setElementDecorators($this->_buttonDecorator, array('submit'));

[/codesyntax]

In de eerste regel kennen we onze standaard elementdecorator toe aan alle elementen. In de daarop volgende twee regels definiëren we een paar uitzonderingen. Het element dat we gebruiken om een profielplaatje te uploaden, voorzien we van onze _fileDecorator, en de verstuurknop krijgt de _buttonDecorator.

Als je nu je registratiepagina ververst, zou je nu de nieuwe broncode met je eigen decorators moeten zien.

6.4 Je eigen validators schrijven

Zend biedt de nodige validators aan die in veel gevallen zullen volstaan om je functionele eisen in te willigen. Maar natuurlijk zijn er omstandigheden denkbaar waarin je je eigen validators wil maken. In ons registratieformulier  zijn er twee velden die we aan een extra controle willen onderwerpen. Onze userName en ons emailadres moeten uniek zijn; die mogen maar een keer voorkomen in de database. En we vragen de gebruiker om zijn ingevoerde wachtwoord te herhalen; aangezien hij in het wachtwoordveld niet ziet wat hij typt, helpt dit om invoerfouten te vermijden. Dat vraagt om twee specifieke, nieuwe validators waarin Zend Framework niet voorziet.

Plaats in de validators array van userName en emailadres deze validator code:

[codesyntax lang=”php”]

array('Unique', true, array('field'=>'userName'))

[/codesyntax]

en

[codesyntax lang=”php”]

array('Unique', false, array('field'=>'email'))

[/codesyntax]

Plaats in de validators array van het tweede wachtwoord veld deze  code:

[codesyntax lang=”php”]

array('CheckSame', false)

[/codesyntax]

Opnieuw zullen we beginnen met de bepaling van de locatie van onze validators. Aangezien het klassen zijn die we wellicht ook in andere applicaties willen gebruiken, is een logische plek onze nieuw aangemaakt library/GJB folder. En omdat het specifieke functionaliteit betreft die we voor formulieren nodig hebben, maken we een nieuwe submap Validator aan  in GJB/Form.

De eerste validator noemen we GJB_Form_Validator_Unique. Het pad van het bestand is dus library/GJB/Form/Validator/Unique.php. Zijn functie gaat zijn om ingevoerde waarden te checken op hun bestaan in de database; als gebruikersnaam en/of emailadres reeds bestaan, willen we een foutmelding genereren.

Elke validator is een child-klasse van Zend_Validate_Abstract, die al veel basisfunctionaliteit biedt. Dit is de code van onze klasse:

[codesyntax lang=”php” lines=”normal” title=”library/GJB/Form/Validator/Unique.php”]

<?php
class GJB_Form_Validate_Unique extends Zend_Validate_Abstract
{
	const NOT_UNIQUE_USERNAME = "notUniqueUsername";
	const NOT_UNIQUE_EMAIL = "notUniqueEmail";
	protected $_messageTemplates = array(
	self::NOT_UNIQUE_USERNAME => "De gebruikersnaam '%value%' is reeds in gebruik.",
	self::NOT_UNIQUE_EMAIL => "Het emailadres '%value%' is reeds in gebruik.");
	protected $_fieldNames;

	public function __construct(array $fieldNames) {
		$this->_fieldNames = $fieldNames;
	}

	public function isValid($value, $context = null)
	{
		$tblUser = new Model_DbTable_Users();
		$this->_setValue($value);
		$isValid = true;
		$error = false;
		switch ($this->_fieldNames['field']) {
			case 'userName':
			if ((bool) $tblUser->fetchRow("userName='$value'")) {
				$this->_error(self::NOT_UNIQUE_USERNAME);
				return false;
			}
			break;
		case 'email':
			if ((bool) $tblUser->fetchRow("email='$value'")) {
				$this->_error(self::NOT_UNIQUE_EMAIL);
				return false;
			}
		}
		return true;
	}
}
?>

[/codesyntax]

We beginnen met de definitie van een paar constanten. Die gebruiken we later om de juiste foutboodschap te kunnen weergeven. Die foutmeldingen worden gekoppeld in de array $_messageTemplates. Je ziet dat we daarin een voor php tamelijk gebruikelijk string-replacement formaat gebruiken: de waarde die door de gebruiker wordt ingevoerd, wordt weergegeven door ‘%value%’; bij het presenteren van de fout wordt dan de werkelijk ingevoerde waarde gepresenteerd.

De constructor krijgt een parameter mee. De waarde van deze parameter is een associatieve array, en is feitelijk het derde argument dat je meegeeft tijdens het aanroepen van de Validator. Vergelijk anders nog even de standaard StringLength Validator die we in ons formulier gebruiken: het eerste lid van de array is de naam van de validator (StringLength in dit geval), het tweede lid is false of true; zetten we dit op true, dan houdt het formulier vanaf dit moment op met het checken op nieuwe fouten. In het geval userName willen we geen nieuwe checks uitvoeren totdat dit veld wordt goedgekeurd; dat bespaart ons een onnodige toch naar de database om het wachtwoord nog te checken. Het derde lid is de naam van het veld dat we willen checken: deze naam moet gelijk zijn aan de betreffende kolom in de database tabel. Het is dit derde lid dat we ook als argument in de constructor aantreffen.

Het eigenlijke werk gebeurt in de methode isValid(). Deze methode krijgt standaard twee argumenten mee: het eerste argument is de ingevoerde waarde, het tweede argument is de context; deze context bestaat uit een array van alle andere door de gebruiker ingevoerde waarden.

In deze methode maken we eerst een nieuwe instantie van onze usertabel aan; daarin vinden we immers de informatie of de gebruikte userName en emailadres al vaker voorkomen. We gaan ervan uit dat de ingevoerde waarden ok zijn; daarom zetten we standaard isValid op true, en error op false. Het switch statement dat volgt bepaalt of we de gebruikersnaam of emailadres willen testen in de database. Als er een rij wordt gevonden met de gebruikte userName of emailadres, roepen we de methode _error aan (overgeërfd van Zend_Validate_Abstract) met de correcte foutmelding, en retourneren we false. Wordt er geen rij gevonden, dan doorlopen we het switch statement zonder gevolgen en retourneren we true.

De CheckSame validator is nog iets simpeler, omdat we geen argumenten in de constructor nodig hebben en ook de database niet hoeven aan te roepen:

[codesyntax lang=”php” lines=”normal” title=”library/GJB/Form/Validate/CheckSame.php”]

<?php
class GJB_Form_Validate_CheckSame extends Zend_Validate_Abstract
{
	const NOT_MATCH = 'notMatch';
	protected $_messageTemplates = array(self::NOT_MATCH => 'De wachtwoorden zijn niet hetzelfde');
	public function isValid($value, $context = null)
	{
		$value = (string)$value;
		$this->_setValue($value);
		if (is_array($context)) {
			if (isset($context['password']) && $value==$context['password']) {
				return true;
		}
		} elseif (is_string($context) && $context == $value) {
		return true;
		}
		$this->_error(self::NOT_MATCH);
		return false;
	}
}

[/codesyntax]

Wat hier vooral van belang is, is de regel waarin $context[‘password’] wordt vergeleken met de waarde die is meegekomen in de isValid methode; zijn deze gelijk aan elkaar, dan is password2 gelijk aan password, en is de gebruikersinput valide.

Voordat we ons formulier met de nieuwe validators kunnen testen, moeten we nog een kleine aanpassing doen aan ons moederformulier, GJB_Form_Standard; dat moeten we namelijk nog vertellen dat we onze eigen validators willen gebruiken. Dat doen we door deze regel toe te voegen aan de constructor:

[codesyntax lang=”php” title=”library/GJB/Form/Standard.php”]

$this->addElementPrefixPath('GJB_Form_Validate', 'GJB/Form/Validator', 'validate');

[/codesyntax]

Feitelijk zeggen we hier tegen Zend Framework: als je een validator tegenkomt die je nog niet kent, zoek dan in het includepath (dus de library) naar het pad ‘GJB/Form/Validate naar een validator die begint met het prefix ‘GJB_Form_Validate’. En daarmee hebben we onze eigen validators toegevoegd.

6.5 Je eigen foutmeldingen aanmaken

Ons formulier is bijna af. Wat we nog zouden willen doen, is onze eigen foutmeldingen weergeven in goed Nederlands in plaats van het standaard Zend Engels dat wordt gebruikt. Daarvoor gebruiken we de component Zend_Translate.  Als we in onze bootstrap Zend_Translate initialiseren en in het applicatieregister plaatsen, zal Zend_Form automatisch gebruik maken van onze aangepaste foutmeldingen.

Voeg daarom een nieuwe methode _initLocale() toe aan onze bootstrap:

[codesyntax lang=”php” title=’application/bootstrap.php’]

/**
* Sets up the translation files and the locale
*/
protected function _initLocale() {
	setlocale(LC_ALL, "nl_nl");
	$translationFile = BASE_PATH . '/library/GJB/Form/nl_translation.php';
	$translate = new Zend_Translate('array', $translationFile, 'nl');
	Zend_Registry::set('Zend_Translate', $translate);
}

[/codesyntax]

In de eerste regel vertellen we onze applicatie de locale, Nederlands in dit geval. In de tweede regel plaatsen we het pad naar ons bestand met foutmeldingen in de variabele $translationFile; en in de derde regel geven we dit bestand als argument mee aan de constructor van een Zend_Translate. Ook vertellen we Zend_Translate dat dit bestand de vorm heeft van een array, en een Nederlandse vertaling betreft. Ten slotte plaatsen we de instantie van Zend_Translate in het applicatieregister.

Het vertaalbestand (nl_translation.php) is feitelijk niet meer dan een gigantische array, die meteen geretourneerd wordt. De eerste drie regels luiden als volgt:

[codesyntax lang=”php” title=”library/GJB/Form/nl_translation.php”]

<?php
return array(
'notAlnum'                      => "'%value%' bestaat niet uitsluitend uit alfanumerieke tekens",
'notAlpha'                      => "'%value%' bestaat niet uitsluitend uit alfabetische tekens",
'stringEmpty'                   => "'%value%' is leeg"
);

[/codesyntax]

Elke foutboodschap in Zend_Validator wordt gedefinieerd in zo’n associatieve array, waarvan de key de constante is van het type foutmelding, en de value de daadwerkelijke foutboodschap. Zo kun je naar believen je eigen applicatie voorzien van Nederlandstalige foutmeldingen. Meer hoef je niet te doen.

6.6 Tot besluit

Dit was een forse klus, maar het was de moeite waard. Zend_Form is een door zijn omvang moeilijke, maar zeer bruikbaar onderdeel van het Framework. In het volgende hoofdstuk zullen we onze applicatie verder gaan inrichten: we maken alle benodigde controllers en actions, zullen css gaan toepassen, en gaan een loginsysteem implementeren.

One Comment

Comments are closed.