Formulare in Presentern
Nette Forms erleichtern die Erstellung und Verarbeitung von Webformularen erheblich. In diesem Kapitel lernen Sie die Verwendung von Formularen innerhalb von Presentern kennen.
Wenn Sie daran interessiert sind, wie man sie völlig eigenständig ohne den Rest des Frameworks verwendet, ist die Anleitung zur eigenständigen Verwendung für Sie bestimmt.
Erstes Formular
Versuchen wir, ein einfaches Registrierungsformular zu schreiben. Sein Code wird wie folgt aussehen:
use Nette\Application\UI\Form;
$form = new Form;
$form->addText('name', 'Name:');
$form->addPassword('password', 'Passwort:');
$form->addSubmit('send', 'Registrieren');
$form->onSuccess[] = [$this, 'formSucceeded'];
und im Browser wird es so angezeigt:

Ein Formular im Presenter ist ein Objekt der Klasse Nette\Application\UI\Form
, sein Vorgänger
Nette\Forms\Form
ist für die eigenständige Verwendung bestimmt. Wir haben ihm sogenannte Elemente Name, Passwort
und eine Senden-Schaltfläche hinzugefügt. Und schließlich besagt die Zeile mit $form->onSuccess
, dass nach dem
Senden und erfolgreicher Validierung die Methode $this->formSucceeded()
aufgerufen werden soll.
Aus Sicht des Presenters ist das Formular eine gewöhnliche Komponente. Daher wird es wie eine Komponente behandelt und wir integrieren es in den Presenter mithilfe einer Factory-Methode. Das wird so aussehen:
use Nette;
use Nette\Application\UI\Form;
class HomePresenter extends Nette\Application\UI\Presenter
{
protected function createComponentRegistrationForm(): Form
{
$form = new Form;
$form->addText('name', 'Name:');
$form->addPassword('password', 'Passwort:');
$form->addSubmit('send', 'Registrieren');
$form->onSuccess[] = [$this, 'formSucceeded'];
return $form;
}
public function formSucceeded(Form $form, $data): void
{
// hier verarbeiten wir die vom Formular gesendeten Daten
// $data->name enthält den Namen
// $data->password enthält das Passwort
$this->flashMessage('Sie wurden erfolgreich registriert.');
$this->redirect('Home:');
}
}
Und im Template rendern wir das Formular mit dem Tag {control}
:
<h1>Registrierung</h1>
{control registrationForm}
Und das ist eigentlich alles :-) Wir haben ein funktionsfähiges und perfekt gesichertes Formular.
Und jetzt denken Sie wahrscheinlich, dass das zu schnell ging, und fragen sich, wie es möglich ist, dass die Methode
formSucceeded()
aufgerufen wird und was die Parameter sind, die sie erhält. Sicher, Sie haben Recht, das verdient
eine Erklärung.
Nette verwendet nämlich einen frischen Mechanismus, den wir Hollywood style nennen. Anstatt dass Sie als Entwickler ständig fragen müssen, ob etwas passiert ist („wurde das Formular gesendet?“, „wurde es gültig gesendet?“ und „wurde es nicht gefälscht?“), sagen Sie dem Framework „wenn das Formular gültig ausgefüllt ist, rufe diese Methode auf“ und überlassen ihm die weitere Arbeit. Wenn Sie in JavaScript programmieren, kennen Sie diesen Programmierstil genau. Sie schreiben Funktionen, die aufgerufen werden, wenn ein bestimmtes Ereignis eintritt. Und die Sprache übergibt ihnen die entsprechenden Argumente.
Genau so ist auch der oben genannte Presenter-Code aufgebaut. Das Array $form->onSuccess
stellt eine Liste von
PHP-Callbacks dar, die Nette aufruft, wenn das Formular gesendet und korrekt ausgefüllt wurde (d. h. es ist gültig). Im Rahmen
des Lebenszyklus des Presenters
handelt es sich um ein sogenanntes Signal, sie werden also nach der action*
-Methode und vor der
render*
-Methode aufgerufen. Und jedem Callback übergibt es als ersten Parameter das Formular selbst und als zweiten
die gesendeten Daten in Form eines ArrayHash-Objekts (oder einer
benutzerdefinierten Klasse, siehe unten). Den ersten Parameter können Sie weglassen, wenn Sie das Formularobjekt nicht
benötigen. Und der zweite Parameter kann cleverer sein, aber dazu später mehr.
Das Objekt $data
enthält die Schlüssel name
und password
mit den Daten, die der
Benutzer eingegeben hat. Normalerweise senden wir die Daten direkt zur weiteren Verarbeitung, was beispielsweise das Einfügen in
die Datenbank sein kann. Während der Verarbeitung kann jedoch ein Fehler auftreten, z. B. wenn der Benutzername bereits vergeben
ist. In diesem Fall übergeben wir den Fehler mit addError()
zurück an das Formular und lassen es erneut rendern,
auch mit der Fehlermeldung.
$form->addError('Entschuldigung, der Benutzername wird bereits verwendet.');
Neben onSuccess
gibt es noch onSubmit
: Callbacks werden immer nach dem Senden des Formulars
aufgerufen, auch wenn es nicht korrekt ausgefüllt ist. Und weiter onError
: Callbacks werden nur aufgerufen, wenn das
Senden nicht gültig ist. Sie werden sogar dann aufgerufen, wenn wir in onSuccess
oder onSubmit
das
Formular mit addError()
ungültig machen.
Nach der Verarbeitung des Formulars leiten wir auf eine andere Seite weiter. Dies verhindert das unbeabsichtigte erneute Senden des Formulars durch die Schaltflächen Aktualisieren, Zurück oder durch die Bewegung im Browserverlauf.
Versuchen Sie, auch weitere Formularelemente hinzuzufügen.
Zugriff auf Elemente
Das Formular ist eine Komponente des Presenters, in unserem Fall namens registrationForm
(nach dem Namen der
Factory-Methode createComponentRegistrationForm
), sodass Sie überall im Presenter mit Folgendem auf das Formular
zugreifen können:
$form = $this->getComponent('registrationForm');
// alternative Syntax: $form = $this['registrationForm'];
Auch die einzelnen Formularelemente sind Komponenten, daher können Sie auf die gleiche Weise darauf zugreifen:
$input = $form->getComponent('name'); // oder $input = $form['name'];
$button = $form->getComponent('send'); // oder $button = $form['send'];
Elemente werden mit unset
entfernt:
unset($form['name']);
Validierungsregeln
Das Wort gültig fiel, aber das Formular hat bisher keine Validierungsregeln. Lassen Sie uns das beheben.
Der Name wird obligatorisch sein, daher markieren wir ihn mit der Methode setRequired()
, deren Argument der Text
der Fehlermeldung ist, die angezeigt wird, wenn der Benutzer den Namen nicht ausfüllt. Wenn kein Argument angegeben wird, wird
die Standardfehlermeldung verwendet.
$form->addText('name', 'Name:')
->setRequired('Bitte geben Sie einen Namen ein');
Versuchen Sie, das Formular ohne ausgefüllten Namen abzusenden, und Sie werden sehen, dass eine Fehlermeldung angezeigt wird und der Browser oder Server es ablehnt, bis Sie das Feld ausfüllen.
Gleichzeitig können Sie das System nicht austricksen, indem Sie beispielsweise nur Leerzeichen in das Feld eingeben. Nein. Nette entfernt automatisch führende und nachfolgende Leerzeichen. Probieren Sie es aus. Das ist etwas, das Sie bei jedem einzeiligen Eingabefeld immer tun sollten, aber oft vergessen wird. Nette tut dies automatisch. (Sie können versuchen, das Formular auszutricksen und als Namen eine mehrzeilige Zeichenkette zu senden. Auch hier lässt sich Nette nicht täuschen und ändert Zeilenumbrüche in Leerzeichen.)
Das Formular wird immer serverseitig validiert, aber es wird auch eine JavaScript-Validierung generiert, die blitzschnell
abläuft, und der Benutzer erfährt sofort von dem Fehler, ohne das Formular an den Server senden zu müssen. Dafür ist das
Skript netteForms.js
verantwortlich. Fügen Sie es in das Layout-Template ein:
<script src="http://unpkg.com/nette-forms@3"></script>
Wenn Sie sich den Quellcode der Seite mit dem Formular ansehen, können Sie feststellen, dass Nette Pflichtfelder in Elemente
mit der CSS-Klasse required
einfügt. Versuchen Sie, das folgende Stylesheet zum Template hinzuzufügen, und die
Beschriftung „Name“ wird rot. So markieren wir elegant Pflichtfelder für die Benutzer:
<style>
.required label { color: maroon }
</style>
Weitere Validierungsregeln fügen wir mit der Methode addRule()
hinzu. Der erste Parameter ist die Regel, der
zweite ist wieder der Text der Fehlermeldung, und es kann noch ein Argument der Validierungsregel folgen. Was ist damit
gemeint?
Wir erweitern das Formular um ein neues optionales Feld „Alter“, das eine ganze Zahl sein muss (addInteger()
)
und außerdem in einem erlaubten Bereich ($form::Range
) liegen muss. Und hier verwenden wir genau den dritten
Parameter der Methode addRule()
, mit dem wir dem Validator den erforderlichen Bereich als Paar
[von, bis]
übergeben:
$form->addInteger('age', 'Alter:')
->addRule($form::Range, 'Das Alter muss zwischen 18 und 120 liegen', [18, 120]);
Wenn der Benutzer das Feld nicht ausfüllt, werden die Validierungsregeln nicht überprüft, da das Element optional ist.
Hier entsteht Raum für ein kleines Refactoring. In der Fehlermeldung und im dritten Parameter sind die Zahlen doppelt
aufgeführt, was nicht ideal ist. Wenn wir mehrsprachige
Formulare erstellen würden und die Meldung mit Zahlen in mehrere Sprachen übersetzt würde, würde eine spätere Änderung
der Werte erschwert. Aus diesem Grund können Platzhalter %d
verwendet werden, und Nette füllt die Werte ein:
->addRule($form::Range, 'Das Alter muss zwischen %d und %d Jahren liegen', [18, 120]);
Kehren wir zum Element password
zurück, das wir ebenfalls obligatorisch machen und noch die minimale
Passwortlänge überprüfen ($form::MinLength
), wieder unter Verwendung des Platzhalters:
$form->addPassword('password', 'Passwort:')
->setRequired('Wählen Sie ein Passwort')
->addRule($form::MinLength, 'Das Passwort muss mindestens %d Zeichen lang sein', 8);
Wir fügen dem Formular noch ein Feld passwordVerify
hinzu, in das der Benutzer das Passwort zur Kontrolle noch
einmal eingibt. Mit Validierungsregeln überprüfen wir, ob beide Passwörter übereinstimmen ($form::Equal
). Und als
Parameter geben wir einen Verweis auf das erste Passwort mithilfe von eckigen
Klammern an:
$form->addPassword('passwordVerify', 'Passwort zur Kontrolle:')
->setRequired('Bitte geben Sie das Passwort zur Kontrolle noch einmal ein')
->addRule($form::Equal, 'Die Passwörter stimmen nicht überein', $form['password'])
->setOmitted();
Mit setOmitted()
haben wir das Element markiert, dessen Wert uns eigentlich egal ist und das nur aus
Validierungsgründen existiert. Der Wert wird nicht an $data
übergeben.
Damit haben wir ein voll funktionsfähiges Formular mit Validierung in PHP und JavaScript fertiggestellt. Die Validierungsfähigkeiten von Nette sind weitaus umfangreicher, es können Bedingungen erstellt, Teile der Seite entsprechend ein- und ausgeblendet werden usw. Alles erfahren Sie im Kapitel über Formularvalidierung.
Standardwerte
Formularelementen weisen wir üblicherweise Standardwerte zu:
$form->addEmail('email', 'E-Mail')
->setDefaultValue($lastUsedEmail);
Oft ist es nützlich, Standardwerte für alle Elemente gleichzeitig festzulegen. Zum Beispiel, wenn das Formular zur Bearbeitung von Datensätzen dient. Wir lesen den Datensatz aus der Datenbank und setzen die Standardwerte:
//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);
Rufen Sie setDefaults()
erst nach der Definition der Elemente auf.
Rendering des Formulars
Standardmäßig wird das Formular als Tabelle gerendert. Die einzelnen Elemente erfüllen die grundlegende
Zugänglichkeitsregel – alle Beschriftungen sind als <label>
geschrieben und mit dem entsprechenden
Formularelement verknüpft. Beim Klicken auf die Beschriftung erscheint der Cursor automatisch im Formularfeld.
Jedem Element können wir beliebige HTML-Attribute zuweisen. Zum Beispiel einen Platzhalter hinzufügen:
$form->addInteger('age', 'Alter:')
->setHtmlAttribute('placeholder', 'Bitte geben Sie Ihr Alter an');
Es gibt wirklich viele Möglichkeiten, ein Formular zu rendern, daher ist dem ein eigenes Kapitel über Rendering gewidmet.
Mapping auf Klassen
Kehren wir zur Methode formSucceeded()
zurück, die im zweiten Parameter $data
die gesendeten Daten
als ArrayHash
-Objekt (oder stdClass
) erhält. Da es sich um eine generische Klasse handelt, fehlt uns
bei der Arbeit damit ein gewisser Komfort, wie z. B. die Autovervollständigung von Eigenschaften in Editoren oder die statische
Codeanalyse. Dies könnte gelöst werden, indem wir für jedes Formular eine spezifische Klasse hätten, deren Eigenschaften die
einzelnen Elemente repräsentieren. Z. B.:
class RegistrationFormData
{
public string $name;
public ?int $age;
public string $password;
}
Alternativ können Sie den Konstruktor verwenden (seit PHP 8.0 mit Property Promotion):
class RegistrationFormData
{
public function __construct(
public string $name,
public ?int $age,
public string $password,
) {
}
}
Die Eigenschaften der Datenklasse können auch Enums sein und werden automatisch zugeordnet.
Wie sagen wir Nette, dass es uns Daten als Objekte dieser Klasse zurückgeben soll? Einfacher als Sie denken. Es genügt, die
Klasse als Typ des Parameters $data
in der Handler-Methode anzugeben:
public function formSucceeded(Form $form, RegistrationFormData $data): void
{
// $name ist eine Instanz von RegistrationFormData
$name = $data->name;
// ...
}
Als Typ kann auch array
angegeben werden, dann werden die Daten als assoziatives Array übergeben.
Auf ähnliche Weise kann auch die Methode getValues()
verwendet werden, der wir den Klassennamen oder ein Objekt
zur Hydratisierung als Parameter übergeben:
$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;
Wenn Formulare eine mehrstufige Struktur aus Containern bilden, erstellen Sie für jeden eine separate Klasse:
$form = new Form;
$person = $form->addContainer('person');
$person->addText('firstName');
/* ... */
class PersonFormData
{
public string $firstName;
public string $lastName;
}
class RegistrationFormData
{
public PersonFormData $person;
public ?int $age;
public string $password;
}
Das Mapping erkennt dann am Typ der Eigenschaft $person
, dass der Container auf die Klasse
PersonFormData
abgebildet werden soll. Wenn die Eigenschaft ein Array von Containern enthalten würde, geben Sie den
Typ array
an und übergeben Sie die Mapping-Klasse direkt an den Container:
$person->setMappedType(PersonFormData::class);
Den Entwurf der Datenklasse des Formulars können Sie sich mit der Methode
Nette\Forms\Blueprint::dataClass($form)
generieren lassen, die ihn auf der Browserseite ausgibt. Den Code können Sie
dann einfach per Klick markieren und in Ihr Projekt kopieren.
Mehrere Schaltflächen
Wenn ein Formular mehr als eine Schaltfläche hat, müssen wir in der Regel unterscheiden, welche davon gedrückt wurde. Wir
können für jede Schaltfläche eine eigene Handler-Funktion erstellen. Wir setzen sie als Handler für das Ereignis onClick
:
$form->addSubmit('save', 'Speichern')
->onClick[] = [$this, 'saveButtonPressed'];
$form->addSubmit('delete', 'Löschen')
->onClick[] = [$this, 'deleteButtonPressed'];
Diese Handler werden nur im Falle eines gültig ausgefüllten Formulars aufgerufen, genau wie beim
onSuccess
-Ereignis. Der Unterschied besteht darin, dass als erster Parameter anstelle des Formulars die sendende
Schaltfläche übergeben werden kann, abhängig vom Typ, den Sie angeben:
public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
$form = $button->getForm();
// ...
}
Wenn das Formular mit der Enter-Taste gesendet wird, wird dies so behandelt, als ob es mit der ersten Schaltfläche gesendet wurde.
Ereignis onAnchor
Wenn wir in der Factory-Methode (wie z. B. createComponentRegistrationForm
) das Formular zusammenstellen, weiß es
noch nicht, ob es gesendet wurde oder mit welchen Daten. Es gibt jedoch Fälle, in denen wir die gesendeten Werte kennen müssen,
z. B. wenn sich die weitere Form des Formulars danach richtet oder wir sie für abhängige Select-Boxen benötigen usw.
Den Teil des Codes, der das Formular zusammenstellt, können Sie daher erst aufrufen lassen, wenn es sogenannte verankert ist,
d. h. bereits mit dem Presenter verbunden ist und seine gesendeten Daten kennt. Einen solchen Code übergeben wir an das Array
$onAnchor
:
$country = $form->addSelect('country', 'Staat:', $this->model->getCountries());
$city = $form->addSelect('city', 'Stadt:');
$form->onAnchor[] = function () use ($country, $city) {
// diese Funktion wird erst aufgerufen, wenn das Formular weiß, ob es gesendet wurde und mit welchen Daten
// es kann also die Methode getValue() verwendet werden
$val = $country->getValue();
$city->setItems($val ? $this->model->getCities($val) : []);
};
Schutz vor Schwachstellen
Das Nette Framework legt großen Wert auf Sicherheit und achtet daher sorgfältig auf die gute Absicherung von Formularen. Dies geschieht völlig transparent und erfordert keine manuelle Konfiguration.
Neben dem Schutz von Formularen vor Cross Site Scripting (XSS)- und Cross-Site Request Forgery (CSRF)-Angriffen bietet es viele kleine Sicherungen, an die Sie nicht mehr denken müssen.
So filtert es beispielsweise alle Steuerzeichen aus den Eingaben und überprüft die Gültigkeit der UTF-8-Kodierung, sodass die Daten aus dem Formular immer sauber sind. Bei Select-Boxen und Radio-Listen wird überprüft, ob die ausgewählten Elemente tatsächlich zu den angebotenen gehörten und keine Manipulation stattgefunden hat. Wir haben bereits erwähnt, dass bei einzeiligen Texteingaben Zeilenendezeichen entfernt werden, die ein Angreifer senden könnte. Bei mehrzeiligen Eingaben werden wiederum die Zeilenendezeichen normalisiert. Und so weiter.
Nette löst für Sie Sicherheitsprobleme, von denen viele Programmierer nicht einmal wissen, dass sie existieren.
Der erwähnte CSRF-Angriff besteht darin, dass ein Angreifer das Opfer auf eine Seite lockt, die unbemerkt im Browser des Opfers eine Anfrage an den Server stellt, bei dem das Opfer angemeldet ist, und der Server annimmt, dass die Anfrage vom Opfer selbst ausgeführt wurde. Daher verhindert Nette standardmäßig das Senden von POST-Formularen von einer anderen Domain. Wenn Sie aus irgendeinem Grund den Schutz deaktivieren und das Senden von Formularen von einer anderen Domain erlauben möchten, verwenden Sie:
$form->allowCrossOrigin(); // ACHTUNG! Deaktiviert den Schutz!
Dieser Schutz verwendet ein SameSite-Cookie namens _nss
. Der Schutz durch SameSite-Cookies ist möglicherweise
nicht 100 % zuverlässig, daher ist es ratsam, auch den Schutz durch ein Token zu aktivieren:
$form->addProtection();
Wir empfehlen, Formulare im Administrationsbereich der Website, die sensible Daten in der Anwendung ändern, auf diese Weise zu
schützen. Das Framework wehrt sich gegen CSRF-Angriffe, indem es ein Autorisierungs-Token generiert und überprüft, das in der
Session gespeichert wird. Daher muss vor dem Anzeigen des Formulars eine Session geöffnet sein. Im Administrationsbereich der
Website ist die Session normalerweise bereits aufgrund der Benutzeranmeldung gestartet. Andernfalls starten Sie die Session mit
der Methode Nette\Http\Session::start()
.
Gleiches Formular in mehreren Presentern
Wenn Sie ein Formular in mehreren Presentern verwenden müssen, empfehlen wir, dafür eine Factory zu erstellen, die Sie dann
an den Presenter übergeben. Ein geeigneter Speicherort für eine solche Klasse ist z. B. das Verzeichnis
app/Forms
.
Die Factory-Klasse könnte beispielsweise so aussehen:
use Nette\Application\UI\Form;
class SignInFormFactory
{
public function create(): Form
{
$form = new Form;
$form->addText('name', 'Name:');
$form->addSubmit('send', 'Anmelden');
return $form;
}
}
Wir bitten die Klasse, das Formular in der Factory-Methode für Komponenten im Presenter zu erstellen:
public function __construct(
private SignInFormFactory $formFactory,
) {
}
protected function createComponentSignInForm(): Form
{
$form = $this->formFactory->create();
// wir können das Formular ändern, hier ändern wir beispielsweise die Beschriftung auf der Schaltfläche
$form['send']->setCaption('Weiter');
$form->onSuccess[] = [$this, 'signInFormSuceeded']; // und fügen einen Handler hinzu
return $form;
}
Der Handler zur Verarbeitung des Formulars kann auch bereits von der Factory geliefert werden:
use Nette\Application\UI\Form;
class SignInFormFactory
{
public function create(): Form
{
$form = new Form;
$form->addText('name', 'Name:');
$form->addSubmit('send', 'Anmelden');
$form->onSuccess[] = function (Form $form, $data): void {
// hier führen wir die Verarbeitung des Formulars durch
};
return $form;
}
}
So, wir haben eine schnelle Einführung in Formulare in Nette hinter uns. Versuchen Sie, sich noch im Verzeichnis examples in der Distribution umzusehen, wo Sie weitere Inspiration finden.