Form-Generator Konzept [1]
Christian Seiler
- php
Hallo Forum,
Juhuuu - ich bin über das Posting-Limit gekommen. ;-)
Nachdem ich heute nicht ganz unschuldig an der aufgeheizten Stimmung und an sinnlosen Diskussionen war, bringe ich mal was sinnvolles. (Hilfe, ich starte einen Thread, waahh, ich befürchte, ich werde bald die Marke 10 erreichen ;))
In </archiv/2002/10/27058/> (Blöd, dass der Archiv-Viewer nicht geht) hatte ich vor, eine Klasse zu schreiben, wie man Formulare bequem validieren kann. Ich bekam jedoch einige Antworten (speziell von Thomas (Schmieder) und Mathias) die gesagt haben, es sei sinnvoller, einen Formulargenerator zu erstellen, um somit sehr einfach Formulare erstellen zu können. Damals war ich von der Idee noch nicht so begeistert, inzwischen denke ich anders. :)
Ich hab' mir ewig lang Gedanken über ein mögliches Konzept für so etwas gemacht (und deswegen bin ich dann auch auf den Formulargenerator zurückgekommen) Ich will hier nun mal kurz mein jetztiges Konzept vorstellen (das meiste, was ich hier vorstelle, habe ich auch schon implementiert):
Zuerst einmal habe ich eine Datei RegexUtility.inc.php, die aus nichts anderem als zwei Funktionen besteht, um Zeichen, die für reguläre Ausdrücke eine besondere Bedeutung haben, zu maskieren. Diese habe ich _regex_replace_normal und _regex_replace_cclass genannt. Beide Versionen haben zwei Parameter: der erste für den zu ersetzenden String, der andere für den Delimiter des Ausdrucks. Der Defaultwert für den Delimiter ist /. Warum brauche ich diese Funktionen? Wenn ich zum Validieren von Formulareingaben möglichst Flexibel sein will, dann können sich bestimmte Zeichen (z.B. Dezimaltrennzeichen) je nach Einstellungen verändern. Da diese u.U. eine besondere Bedeutung in regulären Ausdrücken haben können, maskiere ich sie.
Ich habe eine weitere Datei BasicTranslation.inc.php, die dazu dient, Standardfehlermeldungen zurückzuliefern. Es gibt dort eine Funktion __form_validator_tr, die jedoch nur verwendet werden sollte, wenn man sich sonst keine Gedanken zur Mehrsprachigkeit gemacht hat. Die verwendete Übersetzungsfunktion kann beliebib ausgetauscht werden.
Desweiteren habe ich eine Datei FormFieldValidator.class.php. In dieser Datei habe ich eine Basisklasse FormFieldValidator, die zur Validierung dient. Der Konstruktor erwartet 4 Paramter: Das Locale [1], die Mindestlänge, die Maximallänge und die Übersetzungsfunktion. Die Klasse hat noch eine weitere Methode, validate, die einen Parameter (den Wert) besitzt und im Moment nur die Länge prüft. Die Funktion validate besitzt einen Parameter, den zu prüfenden Wert, und sie gibt entweder false oder den Wert zurück, der zu validieren war. [2] Von dieser Klasse leiten sich alle weiteren Validierungsklassen ab, da werde ich noch ein paar schreiben, im Moment habe ich nur für Integer-Zahlen und beliebige Zahlen. (in nicht-wissenschaftlicher Notation) Man kann sich also einen Validierer konstruieren:
$null_locale = null;
$validator = new FormFieldValidator ($null_locale, 3, 8);
$value = $validator->validate ($_POST["value"]);
if ($value === false) {
...
}
Das Spielchen mit dem null-Locale muss leider sein, da ich die Variable per Referenz erwarte. (Wenn das Locale erst mal richtig groß geworden ist (da viele Mitglieder), dann kann das bei vielen Formularfeldern ziemlich viel Zeit fressen, das ständig zu kopieren.)
Danach habe ich eine Klasse Form, die in der Datei Form.class.php definiert ist. Diese Klasse besitzt drei Parameter: Den Namen (der Inhalt des id-Feldes), den Titel (wird im Formular als Text angezeigt) und das Ausgabeobjekt. [3] Die Klasse besitzt noch weitere Methoden:
- wasProcessed: gibt zurück, ob dieses Script zur Verarbeitung des Formulars aufgerufen wurde. (also das Formular an dieses Script 'abgeschickt' worden ist) Rückgabewert: true/false.
- hiddenField: setzt oder verändert ein hidden-Field, erwartet 2 Parameter, den Namen und den Wert. Rückgabewert: keiner.
- removeHiddenField: löscht ein hidden-Field, erwarten 1 Paramter: den Namen. Rückgabewert: keiner.
- setAction: setzt die Aktion des Formulars auf etwas anderes als $_SERVER['PHP_SELF'], erwartet einen Parameter, die neue Aktion. Rückgabewert: keiner.
- addError: fügt dem Fehler-Array des Formulars eine weitere Fehlermeldung hinzu, erwartet einen Paramter, die Fehlermeldung. Rückgabewert: keiner.
- validate: Lässt alle Formularfelder validieren, erwartet keinen Paramter. Gibt 0 zurück, wenn alles OK ist, -1 zurück, wenn mindestens ein Fehler aufgetreten ist und -2 zurück, wenn mindestens eine Warnung, aber kein Fehler aufgetreten ist.
- fetch: Gibt das Formular als HTML-Code zurück, entweder über eine Standardmethode oder über das Ausgabeobjekt. (Im Falle des Ausgabeobjekts wird nur die entsprechende Funktion aufgerufen) Erwartet keine Parameter.
- display: da steht im Moment echo $this->fetch(); drinnen, eine Erklärung dürfte sich erübrigen.
[-->]
-------------------------------------
[1] Ein Objekt einer Klasse Locale, über das ich mir noch keine großen Gedanken gemacht habe. Im Moment müssen laut meiner Implementation 2 Eigenschaften vorhanden sein: thousands_sep und decimal_point. Später wird das sicherlich noch mehr werden. Ich möchte nicht die OS-Unterstützung für Locales verwenden, weil man da sehr auf das System angewiesen ist und das ganze nicht sehr portabel ist.
[2] Ich mache das absichtlich so, damit die Methode den Wert auch gleich umwandeln kann, z.B. eine Deutsche Zahl "3,5" in eine für PHP verständliche Zahl "3.5", oder ein Datum in einen Timestamp, somit sind die Möglichkeiten sehr groß.
[3] Das Ausgabeobjekt muss ein Objekt einer Klasse sein, das eine Methode process besitzt, die als einzigen Parameter ein Objekt der Klasse Form erwartet. Man kann auch null übergeben, dann wird eine Standardfunktion verwendet, die jedoch nicht immer sinnvoll ist.
[<---]
So, jetzt fragen sich alle, die hier noch nicht abgeschaltet haben, sicherlich, wie ich jetzt stinknormale Formularfelder hinzufüge. Dazu habe ich die Klasse FormField erstellt, die in der Datei FormField.class.php (was war anderes zu erwarten *g*) vorhanden ist. Diese Besitzt einen Konstruktor, der 7 Parameter erwartet: Das Formular, den Namen des Feldes (Inhalt des Name-Attributs), den Text/Titel des Feldes (was halt im Label steht), ein Objekt des Typs FormFieldValidator, das Vaterobjekt (kommt später noch, im Moment ist nur wichtig, das das auch null sein darf, aber wie das Locale per Referenz übergeben werden *muss*), die Größe (size-Attribut) und die maximale Länge (maxlength-Attribut). Der Konstruktor "registriert" das Feld automatisch in einer internen Liste des Formulars. (erster Parameter) Hierbei kommt ein kleineres technisches Problem auf. [4] Das Formularfeld besitzt noch einige öffentliche Eigenschaften:
- field_required: true: *Muss* das Feld validieren (wenn nicht => Fehler) oder false: *sollte* das Feld validieren (wenn nicht => Warnung und der Defaultwert wird verwendet) Standard: false.
- value: der Wert des Feldes zum späteren Zugriff.
- default_value: der Standardwert des Feldes. (funktioniert nur in Verbindung mit field_required = false)
- password_field: ist es ein Passwortfeld oder ein normales Textfeld.
Daraufhin besitzt die Klasse noch Methoden:
- validate: Validiert dieses Formularfeld. Daraufhin wird die öffentliche Variable value gesetzt. Wird vom Formular aufgerufen.
- fetch_default: Wird von der Funktion fetch des Formulars aufgerufen, falls kein Ausgabeobjekt angegeben wurde.
Jetzt gibt es noch eine Klasse FormFieldSet, und bei der kommt dann das Vaterobjekt zu tragen. Wenn man nämlich ein Objekt der Klasse FormFieldSet als Vaterobjekt eines FormFields angibt, dann wird das Formularfeld im Vaterobjekt und nicht im Formular registriert. Der Konstruktor der Klasse erwartet drei Parameter: Das Formular, den Titel und das Vaterobjekt. (man kann beliebig verschachteln) Es gibt wieder die zwei Funktionen validate (die ihrerseits alle Unterelemente aufruft) und fetch_default (die mit <fieldset> und <legend> die Ausgabe erzeugt und dann alle Kindelemente aufruft)
Tja, jetzt ist das ganze natürlich sehr 'trocken', ohne dass man jetzt mal ein Beispiel für die Verwendung sieht, daher hier der Inhalt meines Testformulars (das ganze gehört natürlich noch in eine HTML-Datei eingebettet):
------------------------------------------------------------------------------------------
require_once "Form.class.php";
require_once "FormField.class.php";
require_once "FormFieldSet.class.php";
require_once "FormFieldValidator.class.php";
// create a null locale
$mylocale = null;
// create null parent object
$npo = null;
// create the validators
$username_validator =& new FormFieldValidator ($mylocale, null, 32);
$password_validator =& new FormFieldValidator ($mylocale, null, 32);
// create the form
$login_form =& new Form ("loginform", "Login", $npo);
// create a sample fieldset
$login_fieldset =& new FormFieldSet ($login_form, "Login", $npo);
$u_fieldset =& new FormFieldSet ($login_form, "Username", $login_fieldset);
$p_fieldset =& new FormFieldSet ($login_form, "Password", $login_fieldset);
// create username field
$login_field =& new FormField (&$login_form, "username", "Username", $username_validator, $u_fieldset, 20, 32);
$login_field->field_required = true;
// create password field
$password_field =& new FormField (&$login_form, "password", "Password", $password_validator, $p_fieldset, 15, 32);
$password_field->field_required = false;
$password_field->password_field = true;
// was the form processed?
if ($login_form->wasProcessed ()) {
// validate it
switch ($login_form->validate ()) {
case 0: // did it validate?
// check username
if ($login_field->value != 'christian' || $password_field->value != 'test') {
// oooops - auth failure and redisplay
$login_form->addError ('Authentication failure.');
$login_form->display ();
break;
} else {
// display data
echo "USERNAME: " . $login_field->value . "<br />";
echo "PASSWORD: " . $password_field->value . "<br />";
break;
}
break;
case -1: // errors
case -2: // or warnings
default: // or something strange occured
// redisplay the form
$login_form->display ();
break;
}
return;
} else {
// ok, first call, display the form
$login_form->display ();
}
------------------------------------------------------------------------------------------
(ich habe hier die FieldSets sehr gekünselt verwendet, prinzipiell würde ich die warscheinlich nicht so einsetzen) Ach ja, ein Formularelement kann auch direkt 'Unterelement' eines Formulars sein, ein FieldSet ist nicht notwendig, hier hab' ich es bloß so gemacht.
Ach ja, der Code, der standardmäßig Produziert wird, ist XHTML-Kompatibel (auch Version 1.1) und jedes Formularfeld bekommt einen eigenen Absatz.
So, jetzt würde ich gerne Eure Meinung dazu hören, wenn ihr wollt, dann kann ich auch mal den Code schon mal vorveröffentlichen [5] - ist aber noch lange nicht fertig. Was ich noch gerne realisieren würde, wären
a) Tabellen, bei denen jede Zeile die gleichen Eingabefelder hat - da habe ich auch schon etwas versucht, aber mir fehlt irgendwie das Konzept... :(
b) Andere Eingabefelder als nur normale Inputs - ich könnte mir ein FormSelectField oder auch ein FormDateSelectField vorstellen...
Noch etwas: Die Klassen verwenden $_POST, daher funktioniert das nicht mit GET-Formularen (könnte ich später vielleicht auch noch einbauen) und außerdem erst ab PHP 4.1.
Grüße,
Christian
-------------------------------------
[4] Das Problem wird in http://www.php3.de/manual/de/language.oop.newref.php beschrieben.
[5] Und dann wäre da noch das Problem mit der Lizenz, ich würde ja eigentlich gerne die GPL nehmen, vor allem wg. der Restriktionen zu proprietärer Software, aber a) weiß ich nicht, inwiefern es sinnvoll ist, proprietäre Software überhaupt einzuschränken (ich möchte nicht zu sehr ideologisch denken, das ist auch nicht gut, wenn man verblendet ist) und b) Christoph Zurnieden mich darauf aufmerksam gemacht hat, das die GPL nicht sinnvoll ist, da sie inkompatibel mit der PHP-Lizenz ist. GPL werde ich auf keinen Fall verwenden (wg. Punkt b), aber zum Punkt a würde ich gerne ein paar Meinungen hören.
Hallo nochmal,
So, jetzt würde ich gerne Eure Meinung dazu hören, wenn ihr wollt, dann kann ich auch mal den Code schon mal vorveröffentlichen [5] - ist aber noch lange nicht fertig.
Ich muss mich korrigieren: Das, was ich *insgesamt* vorhatte, ist noch lange nicht fertig, das, was ich hier beschrieben habe (oder zumindest das, was in der 'Demo' verwendet wird), ist aber fertig. Sorry für eventuelle Mißverständnisse.
Grüße,
Christian