Mehrseitige Formulare
Mehrseitige Formulare werden in vielen
komplexen Webanwendungen benötigt und werden häufig als statische
Verkettung von verschiedenen Controllern implementiert. Es gibt
allerdings eine einfache Alternative unter Nutzung von Entwurfsmustern, namentlich Zuständigkeitskette (engl. Chain of Responsibility) und Datentransferobjekt.
Mithilfe der Zuständigkeitskette komponieren wir mehrere eigenständige
Formulare zu einer Komposition von Formularen, dem mehrseitigen
Formular. Bsp.: Formular A beinhaltet Nutzerdaten, Formular B
Kontodaten und Formular C optionale Angaben. Aus diesen Formularen
lässt sich die Komposition ABC bilden, die Nutzerdaten, Kontodaten und
optionale Angaben auf drei verschiedenen Seiten abfragt. Um eine solche
Komposition zu realisieren, muss die Zuständigkeitskette folgende
Methoden anbieten:
- accept: Gibt true zurück, wenn das Formular noch nicht (korrekt) abgearbeitet wurde, d.h. es wird zur Bearbeitung akzeptiert.
- process: Arbeitet das Formular ab (Validierung der Eingaben, Schreiben der Daten ins Transfer-Objekt, ...) und gibt true zurück, wenn die Eingaben korrekt sind.
- dispatch: Arbeitet die Komposition ab, d.h. für das erste Element $form der Komposition, für das $form->accept(...) == true gilt, wird $form->process(...) aufgerufen. Die Methode gibt true zurück, wenn die gesamte Komposition korrekt abgearbeitet wurde.
- attach: (Direktes) Anhängen eines Formulars an ein anderes Formular.
Ferner ist ein hinreiches Kriterium notwendig, um festzustellen, ob
ein Formular (korrekt) abgearbeitet wurde. Hierfür wird ein
Transfer-Objekt benutzt, das die eingegeben Formulardaten speichert,
Zugriff auf jene anbietet und evtl. Zugriff auf den Workflow bietet,
wie z.B. das Bestimmen, welches Template angezeigt werden soll. Beim
Abfertigen der Komposition wird also jedem Formular das Transfer-Objekt
an die Methode accept übergeben, bis ein Formular $form gefunden wurde, für das $form->accept($transfer) == true gilt. Danach wird $form->process($transfer) und im Erfolgsfall $form->dispatch($transfer)
ausgeführt, damit das nächste Formular zum Abarbeiten angeboten wird.
Um eine Endlosschleife zu verhindern, muss muss für alle Objekte $form folgende Eigenschaft gelten: $form->process($transfer) == !$form->accept($transfer).
Wesentlich ist dabei die Reihenfolge der Auswertung (von links nach
rechts). Wenn ein Formular korrekt ausgewertet wurde, dann darf das
Formular das Transfer-Objekt nicht mehr akzeptieren. Andererseits muss
das Formular das Transfer-Objekt akzeptieren, wenn eine fehlerhafte
Eingabe gemacht wurde.
Nun zu einer Eigenschaft der Methode dispatch: dispatch($transfer) == true
dann und nur dann, wenn alle Formulare korrekt abgearbeitet wurden. Es
stellt sich heraus, dass man nur wissen muss, ob das letzte Element der
Komposition korrekt abgearbeitet wurde. Die Begründung: Um zum letzten
Formular $lastForm der Komposition zu gelangen, muss für jedes vorangegangene Formular $form die Eigenschaft $form->accept($transfer) == false gelten, womit $form->process($transfer) == true ist. Also wurde die Komposition genau dann korrekt ausgeführt, wenn $lastForm->process($transfer) == true gilt. Eine schöne Eigenschaft der dispatch-Methode ist, dass sie bei jedem dispatch-Vorgang
alle Formulare überprüft, womit man Probleme inkonsistenter Daten
vorbeugt. Beispiel: Möchte man sich ein Konto mit dem Namen JohnDoe
registrieren, dann wird beim Absenden jedes Formulars kontrolliert, ob
ein Konto mit dem Namen bereits existiert. Wurde in der Zwischenzeit
bereits ein Konto mit dem Namen registriert, wird man über einen Fehler
im assoziierten Formular informiert, während die restlichen Eingaben
unverändert bleiben.
Die zugehörige Implementierung ist minimalistisch gehalten, da die
Details von der Umgebung (z.B. Framework, Template-Engine) abhängen.
Nachfolgend die Implementierung für ein Formular:
abstract class Form
{
private $next;
abstract protected function accept ($transfer);
abstract protected function process ($transfer);
public function attach (Form $form)
{
$form->next = $this->next;
$this->next = $form;
return $this;
}
public function dispatch ($transfer)
{
if($this->accept($transfer))
{
return ($this->process($transfer) ? $this->dispatch($transfer) : false);
}
if($this->next != null)
{
return $this->next->dispatch($transfer);
}
return true;
}
}
Eine Implementierung des Transfer-Objekts werde ich nicht anfertigen, da man hier eine große Auswahl hat. Z.B. könnte man $_SESSION oder das Transfer-Objekt von Biphrost
benutzen. Wesentlich ist jedoch, dass man Daten im Transfer-Objekt
hinterlegen und lesen kann. Ob diese nur die Daten für das mehrseitige
Formular enthält oder auch Anweisungen für die Applikationslogik
(Welches Template soll angezeigt werden? Welche Fehler traten auf?)
hängt von der jeweiligen Implementierung ab. Nachfolgend noch ein
beispielhafter Probelauf:
// $a, $b und $c sind Formulare (s.o.)
$comp = $a->attach($b->attach($c));
if(!$comp->dispatch($transfer)) {
$transfer->tpl->errors = $transfer->errors;
$transfer->tpl->render();
} else {
$tpl = new Template('success.phtml');
writeToDatabase($transfer->form);
$tpl->render();
}
Mehr als zwei Klassen (und etwaige Unterklassen) benötigt man also nicht um elegante mehrseitige Formulare zu erstellen. Diese Lösung hat einige bedeutende Eigenschaften:
- Formulare lassen sich wiederverwenden.
- Die Lösung lässt sich erweitern, sodass man sich Formularkompositionen zusammenklicken kann.
- Die Formularlogik wird gekapselt.
- Formulare und Kompositionen lassen sich leichter ersetzen.
- Es macht mehr Spaß als alles statisch zu erstellen.
Und je nach Implementierung lassen sich dieser Lösung weitere Vorteile abgewinnen. Für Anregungen stehe ich gerne zur Verfügung.