Verwenden des Form-Builders

Beim Erstellen von HTML-Formularen stellt man oft fest, dass in Views immer wieder ähnlicher Code auftaucht, den man aber nur schwer in anderen Projekten wiederverwenden kann. Für jedes Eingabefeld muss zum Beispiel immer ein Label generiert und die aufgetretenen Fehler angezeigt werden. Zwecks besserer Wiederverwendbarkeit kann man dafür den Form-Builder (engl. sinngem. "Formularersteller") einsetzen.

1. Grundprinzip

Der Yii Form-Builder verwendet ein CForm-Objekt, in dem sämtliche Angaben über ein HTML-Formular abgelegt sind, also u.a., welche Datenmodels mit dem Formular verknüpft sind, welche Art von Eingabefeldern verwendet werden und wie das ganze Formular gerendert werden soll. Als Entwickler braucht man somit im Wesentlichen nur noch dieses CForm-Objekt konfigurieren und kann dann dessen Render-Methode aufrufen, um das Formular anzuzeigen.

Die Angaben zu den Eingabefeldern sind als hierarchische Struktur von Formularelementen angelegt. Den Wurzelknoten dieser Baumstruktur bildet das CForm-Objekt selbst. Dieses Element hat zwei Gruppen von Kindelementen: CForm::buttons und CForm::elements. CForm::buttons enthält alle Buttons des Formulars (z.B. Submit- oder Resetbuttons), CForm::elements sämtliche Eingabeelemente, statischen Texte und Subformulare. Ein Subformular ist einfach ein weiteres CForm-Objekt innerhalb der CForm::elements-Liste eines anderen Formulars. Es kann sein eigenes Datenmodel, eigene CForm::buttons und CForm::elements enthalten.

Beim Absenden eines Formulars werden alle Daten in den Eingabefeldern der Formularhierarchie übergeben, inklusive der Daten in den Subformular-Feldern. CForm bietet einige komfortable Methoden, um die gesendeten Daten automatisch den entsprechenden Models zuzuweisen und die Validierung durchzuführen.

2. Erstellen eines einfachen Formulars

Unten sehen wir ein Beispiel, wie man mit dem Form-Builder ein Anmeldeformular erstellen kann.

Zunächst wird dazu eine Controlleraction für die Anmeldung angelegt:

public function actionLogin()
{
    $model = new LoginForm;
    $form = new CForm('application.views.site.loginForm', $model);
    if($form->submitted('login') && $form->validate())
        $this->redirect(array('site/index'));
    else
        $this->render('login', array('form'=>$form));
}

Hier wird ein CForm-Objekt mit den Angaben aus application.views.site.loginForm erzeugt (der Dateipfad wird als Pfadalias angegeben). Dieses CForm-Objekt ist hier als Beispeil mit dem LoginForm-Model verknüpft, das wir schon im Kapitel Erstellen des Models verwendet haben.

Der Code ist relativ einfach zu verstehen: Falls das Formular abgeschickt wurde ($form->submitted('login')) und alle Eingabefelder fehlerfrei sind ($form->validate()) wird auf die Seite site/index umgeleitet. Andernfalls soll der login-View mit diesem Formular gerendert werden.

Der Pfadalias application.views.site.loginForm verweist auf die PHP-Datei protected/views/site/loginForm.php. Diese Datei muss ein Array mit der CForm-Konfiguration zurückliefern:

return array(
    'title'=>'Bitte geben Sie Ihre Anmeldedaten ein',
 
    'elements'=>array(
        'username'=>array(
            'type'=>'text',
            'maxlength'=>32,
        ),
        'password'=>array(
            'type'=>'password',
            'maxlength'=>32,
        ),
        'rememberMe'=>array(
            'type'=>'checkbox',
        )
    ),
 
    'buttons'=>array(
        'login'=>array(
            'type'=>'submit',
            'label'=>'Anmelden',
        ),
    ),
);

Wie üblich entsprechen die Schlüsselnamen dieses Arrays den Namen der Klassenvariablen von CForm, die mit den entsprechenden Werten belegt werden sollen. Die wichtigsten Einträge sind hierbei CForm::elements und CForm::buttons. Jede dieser Eigenschaften besteht aus einem weiteren Array, das die Formularelemente definiert. Darauf gehen wir später noch genauer ein.

Schließlich wird noch das login-Viewscript benötigt, das im einfachsten Fall so aussehen kann:

<h1>Anmeldung</h1>
 
<div class="form">
<?php echo $form; ?>
</div>

Tipp: echo $form ist äquivalent zu echo $form->render();, da CForm nämlich die magische PHP-Methode __toString enthält. Darin wird render() aufgerufen und dessen Ausgabe, also das fertige HTML-Formular, zurückgeliefert.

3. Angeben der Formularelemente

Verwendet man den Form-Builder, verlagert sich der Tätigkeitsschwerpunkt weg vom Erstellen des Views, hin zur Definition der Formularelemente. Im folgenden beschreiben wir, wie diese Angaben für CForm::elements aussehen müssen. Für CForm::buttons gilt analog das selbe, weshalb wir darauf nicht weiter eingehen.

CForm::elements erwartet ein Array als Wert, wobei jedes Element einem Formularelement entspricht. Dabei kann es sich um ein Eingabelement, statischen Text oder ein Subformular handeln.

Definieren eines Eingabeelements

Ein Eingabeelement besteht im Wesentlichen aus einem Label, einem Eingabefeld, einem Hilfstext und einer Fehleranzeige. Es muss außerdem mit einem Modelattribut verknüpft sein. Die Angaben für ein Element werden in Form einer CFormInputElement-Instanz festgelegt. Folgender Beispielcode aus einem CForm::elements-Array definiert ein solches Element:

'username'=>array(
    'type'=>'text',
    'maxlength'=>32,
),

Damit wird festgelegt, dass das entsprechende Modelattribut username heißt und das Eingabefeld vom Typ text mit einem maxlength-Attribut von 32 sein soll.

So kann jede beschreibbare Eigenschaft eines CFormInputElements konfiguriert werden. Mit der hint-Option kann man so zum Beispiel einen Hilfstext angeben oder mit items die Elemente in einer DropDown-, CheckBox- oder RadioButton-List bestimmen (entsprechend den Methoden in CHtml). Handelt es sich bei einer Option nicht um den Namen einer CFormInputElement-Eigenschaft, wird sie als Attribut des entsprechenden HTML-Elements verwendet. Im obigen Beispiel wird daher z.B. das maxlength-Attribut als HTML-Attribut des entsprechenden Eingabefeldes gerendert.

Sehen wir uns die type-Option näher an. Mit ihr wird der Typ des Eingabefelds festgelegt. Der Typ text steht zum Beispiel für ein normales Textfeld, password für ein Passwortfeld. Folgende Typen werden "von Haus aus" von CFormInputElement erkannt:

  • text
  • hidden
  • password
  • textarea
  • file
  • radio
  • checkbox
  • listbox
  • dropdownlist
  • checkboxlist
  • radiolist

Von allen diesen eingebauten Typen wollen wir näher auf die Verwendung der "Listen"-Typen eingehen, also dropdownlist, checkboxlist und radiolist. Diese Typen erfordern, dass die items-Eigenschaft des entsprechenden Eingabeelements wie folgt angegeben wird:

'gender'=>array(
    'type'=>'dropdownlist',
    'items'=>User::model()->getGeschlechtOptions(),
    'prompt'=>'Bitte wählen:',
),
 
...
 
class User extends CActiveRecord
{
    public function getGeschlechtOptions()
    {
        return array(
            0 => 'Männlich',
            1 => 'Weiblich',
        );
    }
}

Dieser Code erzeugt eine Dropdownliste mit dem Aufforderungstext "Bitte wählen:". Die Liste enthält die Optionen "Männlich" und "Weiblich", so wie sie von der getGeschlechtOptions-Methode in der User-Klasse geliefert werden.

Daneben kann die type-Option auch den Namen einer Widgetklasse oder deren Pfadalias enthalten. Die Widgetklasse muss lediglich CInputWidget oder CJuiInputWidget erweitern. Beim Rendern des Elements wird eine Instanz der angegebenen Widgetklasse mit den angegebenen Elementparametern erzeugt und gerendert.

Verwenden von statischem Text

In vielen Fällen enthält ein Formular zusätzlichen, rein "dekorativen" HTML-Code, wie zum Beispiel eine horizontale Linie um Formularabschnitte voneinander zu trennen. An anderen Stellen wird eventuell ein Bild zur Auflockerung des Formulars verwendet. Solche statischen Elemente werden einfach als String an der entsprechenden Stelle des CForm::elements-Arrays angeben.

Hier ein Beispiel:

return array(
    'elements'=>array(
        ......
        'password'=>array(
            'type'=>'password',
            'maxlength'=>32,
        ),
 
        '<hr />',
 
        'rememberMe'=>array(
            'type'=>'checkbox',
        )
    ),
    ......
);

Zwischen die Elemente für password und rememberMe wird so eine horizontale Linie eingefügt.

Statischer Text eignet sich am besten für unregelmäßig verteilte Inhalte. Sollen hingegen alle Elemente mit ähnlicher "Dekoration" versehen werden, ist es günstiger, eine eigene Rendermethode zu verwenden. Darauf werden wir weiter unten noch eingehen.

Verwenden von Subformularen

Subformulare eignen sich dazu, sehr lange Formulare in mehrere logisch zusammenhängende Blöcke zu gliedern. Ein langes Registrierungsformular könnte man so z.B. in Anmelde- und Profildaten unterteilen. Ein Subformular kann - muss aber nicht - mit einem eigenen Datenmodel verknüpft sein. Wenn beim erwähnten Registrierungsformular die Anmelde- und Profildaten in zwei unterschiedlichen Datebanktabellen (und damit zwei Datenmodels) gespeichert werden, würde man jedes Subformular mit dem entsprechenden Datenmodel verknüpfen. Speichert man alles in einer einzelnen Tabelle braucht keines der Subformulare ein Model, da sie dann das Model des Wurzelformulars verwenden.

Ein Subformular ist ebenfalls ein CForm-Objekt. Ein Subformular wird dem CForm::elements-Array als Element vom Typ form hinzugefügt:

return array(
    'elements'=>array(
        ......
        'user'=>array(
            'type'=>'form',
            'title'=>'Anmeldedaten',
            'elements'=>array(
                'username'=>array(
                    'type'=>'text',
                ),
                'password'=>array(
                    'type'=>'password',
                ),
                'email'=>array(
                    'type'=>'text',
                ),
            ),
        ),
 
        'profile'=>array(
            'type'=>'form',
            ......
        ),
        ......
    ),
    ......
);

Auch das Subformular besteht im Wesentlichen wieder aus Einträgen im CForm::elements-Array. Soll das Subformular mit einem eigenen Model verknüpft werden, kann dies über die CForm::model angegeben werden.

Manchmal kann es nötig sein, eine andere Formklasse statt CForm zu verwenden. Wie wir in Kürze sehen werden, kann man z.B. eine eigene Klasse von CForm ableiten, um die Renderlogik anzupassen. Sämtliche Subformulare verwenden standardmäßig die selbe Klasse wie deren Elternelement. Soll ein Subformular eine andere Klasse verwenden, kann der Typ auf XyzForm gesetzt werden (also einen String, der auf Form endet). Das Subformular wird dann als XyzForm-Objekt erstellt.

4. Zugriff auf Formularelemente

Auf Formularelemente kann man wie auf Arrayelemente zugreifen. Liest man die Eigenschaft CForm::elements aus, erhält man ein Objekt vom Typ CFormElementCollection zurück, das wiederum von CMap abgeleitet wurde. Dadurch kann es wie ein normales Array angesprochen werden. Das Element username des weiter oben definierten Loginformulars erhält man zum Beispiel mit:

$username = $form->elements['username'];

Und auf das email-Element des Registrierungsformulars kann man so zugreifen:

$email = $form->elements['user']->elements['email'];

CForm implementiert außerdem das ArrayAccess-Interface so, dass man damit direkt auf CForm::elements zugreifen kann. Statt den obigen Zeilen kann man daher noch einfacher schreiben:

$username = $form['username'];
$email = $form['user']['email'];

5. Erstellen eines verschachtelten Formulars

Formulare, die, wie eben beschrieben, Subformulare enthalten, nennen wir verschachtelte Formulare (engl.: nested forms). Anhand des erwähnten Registrierungsformulars zeigen wir hier, wie man ein verschachteltes Formular erstellt, das mit mehreren Datenmodels verknüpft ist. Dabei seien die Anmeldeinformation im Model User und die Profildaten im Model Profile gespeichert.

Zunächst benötigen wir eine register-Action:

public function actionRegister()
{
    $form = new CForm('application.views.user.registerForm');
    $form['user']->model = new User;
    $form['profile']->model = new Profile;
    if($form->submitted('register') && $form->validate())
    {
        $user = $form['user']->model;
        $profile = $form['profile']->model;
        if($user->save(false))
        {
            $profile->userID = $user->id;
            $profile->save(false);
            $this->redirect(array('site/index'));
        }
    }
 
    $this->render('register', array('form'=>$form));
}

Die Formularkonfiguration wird hier in application.views.user.registerForm abgelegt. Wurde das Formular abgeschickt und die Daten erfolgreich geprüft, wird versucht, die Models User und Profile zu speichern. Diese Models können über die model-Eigenschaft des jeweiligen Subformulars bezogen werden. Da die Validierung bereits durchgeführt wurde, wird $user->save(false) aufgerufen, um eine nochmalige Prüfung zu verhindern. Mit dem Profile-Model wird ebenso verfahren.

Sehen wir uns als nächstes die Formularkonfiguration in protected/views/user/registerForm.php an:

return array(
    'elements'=>array(
        'user'=>array(
            'type'=>'form',
            'title'=>'Anmeldedaten',
            'elements'=>array(
                'username'=>array(
                    'type'=>'text',
                ),
                'password'=>array(
                    'type'=>'password',
                ),
                'email'=>array(
                    'type'=>'text',
                )
            ),
        ),
 
        'profile'=>array(
            'type'=>'form',
            'title'=>'Profildaten',
            'elements'=>array(
                'firstName'=>array(
                    'type'=>'text',
                ),
                'lastName'=>array(
                    'type'=>'text',
                ),
            ),
        ),
    ),
 
    'buttons'=>array(
        'register'=>array(
            'type'=>'submit',
            'label'=>'Registrieren',
        ),
    ),
);

Bei jedem Subformular wird hier auch eine CForm::title-Eigenschaft definiert. Standardmäßig sorgt die Renderlogik dafür, dass jedes Subformular in ein eigenes fieldset mit dieser Eigenschaft als Titel eingebettet wird.

Nun fehlt nur noch das Viewscript für register:

<h1>Registrierung</h1>
 
<div class="form">
<?php echo $form; ?>
</div>

6. Anpassen der Formularausgabe

Der eigentliche Nutzen des Form-Builders liegt in der Trennung von Logik (Formularkonfiguration in einer eigenen Datei) und Präsentation (CForm::render-Methode). Dadurch kann die Anzeige des Formulars angepasst werden. Entweder, indem man CForm::render überschreibt oder indem man einen Teilview zum Rendern des Formulars angibt. Beide Ansätze sind unabhängig von der Formularkonfiguration und lassen sich so einfach wiederverwenden.

Überschreibt man CForm::render, so müssen dort eigentlich nur CForm::elements und CForm::buttons in einer Schleife durchlaufen und die CFormElement::render-Methode jedes Elements aufgerufen werden:

class MyForm extends CForm
{
    public function render()
    {
        $output = $this->renderBegin();
 
        foreach($this->getElements() as $element)
            $output .= $element->render();
 
        $output .= $this->renderEnd();
 
        return $output;
    }
}

Zum Rendern des Formular kann man auch ein Viewscript _form verwenden:

<?php
echo $form->renderBegin();
 
foreach($form->getElements() as $element)
    echo $element->render();
 
echo $form->renderEnd();

Dieses Script kann so aufgerufen werden:

<div class="form">
$this->renderPartial('_form', array('form'=>$form));
</div>

Falls ein Formular nicht mit diesem allgemeinen Renderansatz dargestellt werden kann (z.B. weil einige Elemente vollkommen anders aussehen müssen), kann man im Viewscript auch so verfahren:

Einige komplexe UI-Elemente hier
 
<?php echo $form['username']; ?>
 
Einige komplexe UI-Elemente hier
 
<?php echo $form['password']; ?>
 
einige komplexe UI-Elemente hier

Bei dieser Methode scheint der Form-Builder nicht viel zu nützen, da man immer noch fast genauso viel Code wie bisher braucht. Trotzdem kann sich der Einsatz lohnen, da das Formular in einer separaten Datei definiert wird und sich der Entwickler so besser auf die Logik konzentrieren kann.

$Id: form.builder.txt 2890 2011-01-18 15:58:34Z qiang.xue $

Be the first person to leave a comment

Please to leave your comment.