Formulare in Presentern

Nette Forms vereinfachen das Erstellen und Verarbeiten von Webformularen erheblich. In diesem Kapitel erfahren Sie, wie Sie Formulare innerhalb von Presentern verwenden.

Wenn Sie sie völlig eigenständig ohne den Rest des Frameworks nutzen möchten, gibt es eine Anleitung zur eigenständigen Verwendung.

Das erste Formular

Versuchen wir, ein einfaches Registrierungsformular zu schreiben. Sein Code sieht so aus:

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 dargestellt:

Ein Formular im Presenter ist ein Objekt der Klasse Nette\Application\UI\Form; ihr Vorgänger Nette\Forms\Form ist für die eigenständige Verwendung gedacht. Wir haben Elemente namens name und password sowie einen Absende-Button hinzugefügt. Die Zeile $form->onSuccess sagt schließlich, dass nach dem Absenden und erfolgreicher Validierung die Methode $this->formSucceeded() aufgerufen werden soll.

Aus Sicht des Presenters ist das Formular eine gewöhnliche Komponente. Es wird deshalb wie eine Komponente behandelt und über eine Factory-Methode in den Presenter eingebunden. Das sieht so aus:

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;
	}

	private 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 haben sich erfolgreich registriert.');
		$this->redirect('Home:');
	}
}

Und im Template wird das Formular über den Tag {control} gerendert:

<h1>Registrierung</h1>

{control registrationForm}

Und das ist im Grunde alles :-) Wir haben ein funktionierendes und bestens abgesichertes Formular.

Jetzt denken Sie vermutlich, das ging zu schnell, und fragen sich, wie es möglich ist, dass die Methode formSucceeded() aufgerufen wird und welche Parameter sie bekommt. Ja, Sie haben recht, das verdient eine Erklärung.

Nette bringt einen erfrischenden Mechanismus mit, den Hollywood-Stil. Statt dass Sie als Entwickler ständig fragen müssen, ob etwas passiert ist (“wurde das Formular abgesendet?”, “wurde es gültig abgesendet?”, “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, ist Ihnen dieser Programmierstil bestens vertraut. Sie schreiben Funktionen, die aufgerufen werden, wenn ein bestimmtes Event eintritt. Und die Sprache übergibt ihnen die passenden Argumente.

Genau so ist der obige Code des Presenters aufgebaut. Das Array $form->onSuccess stellt eine Liste von PHP-Callbacks dar, die Nette in dem Moment aufruft, in dem das Formular abgesendet und richtig ausgefüllt ist (also gültig). Innerhalb des Lebenszyklus des Presenters handelt es sich um ein sogenanntes Signal, sie werden also nach der Methode action* und vor der Methode render* aufgerufen. Und jedem Callback übergibt es als ersten Parameter das Formular selbst und als zweiten die gesendeten Daten als Objekt ArrayHash (oder stdClass oder eine eigene Klasse). Den ersten Parameter können Sie weglassen, wenn Sie das Objekt des Formulars nicht brauchen. Der zweite Parameter kann klüger sein, dazu aber später mehr.

Das Objekt $data enthält die Properties name und password mit den Daten, die der Benutzer eingegeben hat. Üblicherweise geben wir die Daten direkt zur weiteren Verarbeitung weiter, etwa zum Einfügen in eine Datenbank. Bei der Verarbeitung kann jedoch ein Fehler auftreten, zum Beispiel ist der Benutzername schon vergeben. In einem solchen Fall geben wir den Fehler über addError() an das Formular zurück und lassen es samt Fehlermeldung erneut rendern.

$form->addError('Der Benutzername ist leider bereits vergeben.');

Neben onSuccess gibt es auch onSubmit: Die Callbacks werden immer aufgerufen, wenn das Formular abgesendet wird, auch wenn es nicht richtig ausgefüllt ist. Und außerdem onError: Die Callbacks werden nur dann aufgerufen, wenn das Absenden nicht gültig ist. Sie werden sogar dann aufgerufen, wenn wir das Formular in onSuccess über addError() für ungültig erklären.

Nach dem Verarbeiten des Formulars leiten wir auf eine andere Seite weiter. Das verhindert, dass das Formular ungewollt erneut abgesendet wird, wenn der Benutzer Aktualisieren oder Zurück drückt oder in der Browser-Historie navigiert.

Wird das Formular über AJAX abgesendet, zeichnen Sie statt einer Weiterleitung üblicherweise ein Snippet mit dem neu gerenderten Formular neu.

Versuchen Sie, weitere Formularelemente hinzuzufügen.

Zugriff auf die Elemente

Das Formular ist eine Komponente des Presenters, in unserem Fall namens registrationForm (nach dem Namen der Factory-Methode createComponentRegistrationForm), Sie können also überall im Presenter so auf das Formular zugreifen:

$form = $this->getComponent('registrationForm');
// alternative Schreibweise: $form = $this['registrationForm'];

Auch die einzelnen Formularelemente sind Komponenten, Sie greifen also genauso auf sie zu:

$input = $form->getComponent('name'); // oder $input = $form['name'];
$button = $form->getComponent('send'); // oder $button = $form['send'];

Entfernt werden Elemente über unset:

unset($form['name']);

Validierungsregeln

Das Wort gültig ist gefallen, aber das Formular hat noch keine Validierungsregeln. Bringen wir das in Ordnung.

Der Name wird zum Pflichtfeld, wir kennzeichnen ihn also über die Methode setRequired(). Ihr Argument ist der Text der Fehlermeldung, die angezeigt wird, wenn der Benutzer den Namen nicht ausfüllt. Wird das Argument weggelassen, wird die Standard-Fehlermeldung verwendet.

$form->addText('name', 'Name:')
	->setRequired('Bitte geben Sie Ihren Namen ein.');

Versuchen Sie, das Formular ohne ausgefüllten Namen abzusenden, und Sie sehen die Fehlermeldung; der Browser oder der Server weist es ab, bis Sie das Feld ausfüllen.

Zugleich können Sie das System nicht austricksen, indem Sie zum Beispiel nur Leerzeichen ins Feld schreiben. Auf keinen Fall. Nette schneidet Leerraum am Anfang und Ende automatisch ab. Probieren Sie es aus. Das sollten Sie bei jeder einzeiligen Eingabe immer tun, es wird aber oft vergessen. Nette macht es automatisch. (Sie können versuchen, das Formular hereinzulegen und als Namen einen mehrzeiligen String zu senden. Auch hier lässt sich Nette nicht täuschen, die Zeilenumbrüche werden in Leerzeichen umgewandelt.)

Das Formular wird immer auf der Serverseite validiert, es wird aber auch eine JavaScript-Validierung erzeugt, die sofort läuft und den Benutzer unmittelbar über den Fehler informiert, ohne dass das Formular an den Server gesendet werden muss. Dafür sorgt das Skript netteForms.js. Binden Sie es in Ihr Layout-Template ein:

<script src="https://unpkg.com/nette-forms@3"></script>

Wenn Sie sich den Quellcode der Seite mit dem Formular ansehen, fällt Ihnen vielleicht auf, dass Nette die Pflichtelemente in Elemente mit der CSS-Klasse required packt. Fügen Sie Ihrem Template das folgende Stylesheet hinzu, und das Label “Name” wird rot. So heben Sie Pflichtfelder für die Benutzer elegant hervor:

<style>
.required label { color: maroon }
</style>

Weitere Validierungsregeln fügen wir über die Methode addRule() hinzu. Der erste Parameter ist die Regel, der zweite wiederum der Text der Fehlermeldung, und danach kann ein Argument für die Validierungsregel folgen. Was bedeutet das?

Erweitern wir das Formular um ein neues optionales Feld “Alter”, das eine ganze Zahl sein muss (addInteger()) und außerdem in einem erlaubten Bereich liegen muss ($form::Range). Hier verwenden wir den dritten Parameter der Methode addRule(), um dem Validator den verlangten Bereich als Paar [min, max] zu übergeben:

$form->addInteger('age', 'Alter:')
	->addRule($form::Range, 'Das Alter muss zwischen 18 und 120 liegen.', [18, 120]);

Füllt der Benutzer das Feld nicht aus, werden die Validierungsregeln nicht geprüft, denn das Element ist optional.

Damit entsteht Raum für ein kleines Refactoring. In der Fehlermeldung und im dritten Parameter stehen die Zahlen doppelt, was nicht ideal ist. Würden wir mehrsprachige Formulare bauen und die Meldung mit den Zahlen in mehrere Sprachen übersetzen, wäre das Ändern der Werte mühsam. Aus diesem Grund lassen sich die Platzhalter %d verwenden, und Nette setzt die Werte ein:

	->addRule($form::Range, 'Das Alter muss zwischen %d und %d Jahren liegen.', [18, 120]);

Kehren wir zum Element password zurück, machen es ebenfalls zum Pflichtfeld und prüfen außerdem die Mindestlänge des Passworts ($form::MinLength), wieder mit einem Platzhalter in der Meldung:

$form->addPassword('password', 'Passwort:')
	->setRequired('Wählen Sie ein Passwort')
	->addRule($form::MinLength, 'Ihr Passwort muss mindestens %d Zeichen lang sein.', 8);

Fügen wir dem Formular noch ein Feld passwordVerify hinzu, in dem der Benutzer das Passwort zur Bestätigung erneut eingibt. Über Validierungsregeln prüfen wir, ob beide Passwörter gleich sind ($form::Equal). Als Argument geben wir über eckige Klammern einen Verweis auf das erste Passwort an:

$form->addPassword('passwordVerify', 'Passwort erneut:')
	->setRequired('Geben Sie das Passwort zur Kontrolle erneut ein')
	->addRule($form::Equal, 'Die Passwörter stimmen nicht überein.', $form['password'])
	->setOmitted();

Mit setOmitted() haben wir ein Element gekennzeichnet, dessen Wert uns eigentlich nicht interessiert und das nur zur Validierung da ist. Sein Wert wird nicht an $data übergeben.

Damit haben wir ein voll funktionsfähiges Formular mit Validierung in PHP und in JavaScript. Die Fähigkeiten von Nette zur Validierung reichen viel weiter; es lassen sich Bedingungen erstellen, anhand derer sich Teile der Seite ein- und ausblenden lassen, und so weiter. Alles erfahren Sie im Kapitel über die Validierung von Formularen.

Standardwerte

Für Formularelemente setzen wir üblicherweise Standardwerte:

$form->addEmail('email', 'E-Mail')
	->setDefaultValue($lastUsedEmail);

Oft ist es nützlich, die Standardwerte für alle Elemente auf einmal zu setzen. Zum Beispiel, wenn das Formular zum Bearbeiten 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() nach dem Definieren der Elemente auf.

Bei einem bereits abgesendeten Formular hat setDefaults() keine Wirkung – es überschreibt nicht, was der Benutzer ausgefüllt hat, es ist also gefahrlos, es in der Factory des Formulars ohne Bedingung aufzurufen. Wenn Sie die Werte auch nach dem Absenden erzwingen müssen, verwenden Sie stattdessen setValues().

Rendering des Formulars

Standardmäßig wird das Formular als Tabelle gerendert. Die einzelnen Elemente halten die grundlegenden Regeln der Barrierefreiheit ein – alle Labels sind als <label>-Elemente geschrieben und dem jeweiligen Formularelement zugeordnet. Ein Klick auf das Label setzt den Cursor automatisch in das Formularfeld.

Für jedes Element können wir beliebige HTML-Attribute setzen. Fügen wir zum Beispiel einen Platzhalter hinzu:

$form->addInteger('age', 'Alter:')
	->setHtmlAttribute('placeholder', 'Bitte geben Sie das Alter an');

Es gibt wirklich viele Wege, ein Formular zu rendern, deshalb ist ihm ein eigenes Kapitel über das Rendering gewidmet.

Mapping auf Klassen

Kehren wir zur Methode formSucceeded() zurück, die die gesendeten Daten im zweiten Parameter $data als Objekt ArrayHash (oder stdClass) bekommt. Weil es eine generische Klasse ist, ähnlich stdClass, fehlt uns bei der Arbeit damit einiger Komfort, etwa die Vervollständigung der Properties im Editor oder die statische Analyse des Codes. Lösen ließe sich das, indem es für jedes Formular eine eigene Klasse gibt, deren Properties die einzelnen Elemente darstellen. Zum Beispiel:

class RegistrationFormData
{
	public string $name;
	public ?int $age;
	public string $password;
}

Alternativ können Sie einen Konstruktor verwenden:

class RegistrationFormData
{
	public function __construct(
		public string $name,
		public ?int $age,
		public string $password,
	) {
	}
}

Die Properties der Datenklasse können auch Enums sein, sie werden automatisch gemappt.

Wie sagen wir Nette, dass es die Daten als Objekte dieser Klasse zurückgeben soll? Einfacher, als Sie denken. Geben Sie die Klasse einfach als Typ des Parameters $data in der Methode des Handlers an:

public function formSucceeded(Form $form, RegistrationFormData $data): void
{
	// $data ist eine Instanz von RegistrationFormData
	$name = $data->name;
	// ...
}

Als Typ lässt sich auch array angeben, dann werden die Daten als Array übergeben.

Ebenso lässt sich die Methode getValues() verwenden, der Sie den Namen der Klasse oder ein zu füllendes Objekt als Parameter übergeben:

$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;

Wenn Sie die Werte lesen müssen, bevor das Formular validiert ist – typischerweise in einem onValidate-Handler -, verwenden Sie stattdessen die Methode getUntrustedValues(). Sie nimmt dieselben Parameter entgegen wie getValues(), gibt die gesendeten Werte aber ohne die Garantie zurück, dass sie die Validierung bestanden haben.

Haben die Formulare eine mehrstufige Struktur aus Containern, legen Sie für jeden eine eigene Klasse an:

$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 leitet dann aus dem Typ der Property $person ab, dass es den Container auf die Klasse PersonFormData mappen soll. Würde die Property ein Array von Containern enthalten, geben Sie den Typ array an und übergeben die zu mappende Klasse direkt dem Container:

$person->setMappedType(PersonFormData::class);

Einen Vorschlag für die Datenklasse des Formulars können Sie sich über die Methode Nette\Forms\Blueprint::dataClass($form) erzeugen lassen, die ihn auf der Seite im Browser ausgibt. Dann markieren Sie den Code einfach mit einem Klick und kopieren ihn in Ihr Projekt.

Mehrere Absende-Buttons

Hat ein Formular mehr als einen Button, müssen wir üblicherweise unterscheiden, welcher davon gedrückt wurde. Für jeden Button können wir eine eigene Handler-Funktion schreiben. Setzen Sie sie als Handler für das Event onClick:

$form->addSubmit('save', 'Speichern')
	->onClick[] = $this->saveButtonPressed(...);

$form->addSubmit('delete', 'Löschen')
	->onClick[] = $this->deleteButtonPressed(...);

Ein Handler lässt sich dem Button auch direkt als drittes Argument der Methode addSubmit() übergeben.

Diese Handler werden ebenso wie das Event onSuccess nur dann aufgerufen, wenn das Formular gültig ausgefüllt ist (sofern die Validierung für den Button nicht abgeschaltet ist). Der Unterschied ist, dass als erster Parameter statt des Formulars das Objekt des Absende-Buttons übergeben werden kann, je nachdem, welche Typdeklaration Sie angeben:

private function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
	$form = $button->getForm();
	// ...
}

Wird das Formular durch Drücken der Taste Enter abgesendet, wird das behandelt, als wäre es über den ersten Absende-Button abgesendet worden.

Event onAnchor

Wenn Sie ein Formular in einer Factory-Methode bauen (etwa createComponentRegistrationForm), weiß es noch nicht, ob es abgesendet wurde und mit welchen Daten. Es gibt aber Fälle, in denen wir die gesendeten Werte kennen müssen, weil vielleicht das Aussehen des Formulars von ihnen abhängt oder sie für voneinander abhängige Select-Boxen gebraucht werden.

Sie können den Code, der das Formular baut, deshalb erst dann aufrufen lassen, wenn es “verankert” ist, also bereits mit dem Presenter verbunden ist und seine gesendeten Daten kennt. Legen Sie solchen Code in das Array $onAnchor:

$country = $form->addSelect('country', 'Land:', $this->model->getCountries());
$city = $form->addSelect('city', 'Stadt:');

$form->onAnchor[] = function () use ($country, $city) {
	// diese Funktion wird aufgerufen, wenn das Formular die Daten kennt, mit denen es abgesendet wurde
	// Sie können also die Methode getValue() verwenden
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};

Schutz vor Sicherheitslücken

Das Nette Framework legt großen Wert auf Sicherheit und achtet deshalb gewissenhaft auf die Sicherheit von Formularen. Es tut das vollkommen transparent und verlangt keine manuelle Einrichtung.

Neben dem Schutz von Formularen vor Angriffen wie Cross-Site Scripting (XSS) und Cross-Site Request Forgery (CSRF) trifft es viele kleine Sicherheitsmaßnahmen, an die Sie nicht mehr denken müssen.

So filtert es zum Beispiel alle Steuerzeichen aus den Eingaben und prüft die Gültigkeit der UTF-8-Kodierung, sodass die Daten aus dem Formular immer sauber sind. Bei Select-Boxen und Radio-Listen prüft es, dass die ausgewählten Elemente tatsächlich zu den angebotenen gehörten und keine Fälschung stattgefunden hat. Wir haben bereits erwähnt, dass es bei einzeiligen Textfeldern die Zeilenumbruchzeichen, die ein Angreifer senden könnte, durch Leerzeichen ersetzt. Bei mehrzeiligen Eingaben vereinheitlicht es die Zeilenumbruchzeichen. Und so weiter.

Nette kümmert sich für Sie um Sicherheitsrisiken, von deren Existenz viele Programmierer nicht einmal wissen.

Der erwähnte CSRF-Angriff besteht darin, dass ein Angreifer ein Opfer auf eine Seite lockt, die im Browser des Opfers unbemerkt einen Request an den Server ausführt, auf dem das Opfer angemeldet ist. Der Server glaubt dann, das Opfer habe den Request freiwillig ausgelöst. Nette weist deshalb POST-Formulare ab, die von einem fremden Origin abgesendet wurden; auch eine andere Subdomain derselben Website gilt als fremd. Wenn Sie das Absenden von einem anderen Origin erlauben müssen, schalten Sie den Schutz ab mit:

$form->allowCrossOrigin(); // ACHTUNG! Schaltet den Schutz vollständig ab!

Damit ist der Schutz allerdings für jeden Origin abgeschaltet. Um nur bestimmte Origins zu erlauben, schalten Sie den Schutz ab und prüfen den Header Origin selbst gegen eine eigene Allowlist.

Der Schutz stützt sich auf den Header Sec-Fetch-Site des Browsers (Fetch Metadata), den der Browser automatisch sendet und der sich selbst mit einer XSS-Lücke nicht fälschen lässt. Für ältere Browser ohne Unterstützung dafür greift ersatzweise ein SameSite-Cookie, das eine Nette-Anwendung automatisch setzt. Der Artikel The browser finally solves CSRF beschreibt das ausführlich.

Der frühere Schutz über ein Autorisierungstoken in der Session, der über $form->addProtection() aktiviert wurde, ist nicht mehr nötig und seit Version 3.3 veraltet.

Ein Formular in mehreren Presentern verwenden

Wenn Sie dasselbe Formular in mehreren Presentern brauchen, empfehlen wir, dafür eine Factory zu schreiben, die Sie dann in die Presenter injizieren. Ein passender Ort für eine solche Klasse ist zum Beispiel das Verzeichnis app/Forms.

Die Klasse der Factory könnte 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;
	}
}

In der Factory-Methode der Komponente im Presenter lassen wir uns von der Klasse das Formular erzeugen:

public function __construct(
	private SignInFormFactory $formFactory,
) {
}

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	// wir können das Formular ändern, hier ändern wir zum Beispiel das Label des Buttons
	$form['send']->setCaption('Weiter');
	$form->onSuccess[] = $this->signInFormSuceeded(...); // und einen Handler ergänzen
	return $form;
}

Den Handler zum Verarbeiten des Formulars kann auch die Factory selbst bereitstellen:

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 verarbeiten wir unser abgesendetes Formular
		};
		return $form;
	}
}

Damit haben wir eine kurze Einführung in Formulare in Nette hinter uns. Schauen Sie sich für weitere Anregungen das Verzeichnis examples in der Distribution an.

Version: 4.x