Nette Documentation Preview

syntax
Formulare in Moderatoren
************************

.[perex]
Nette Forms erleichtert die Erstellung und Bearbeitung von Webformularen erheblich. In diesem Kapitel lernen Sie, wie Sie Formulare in Presentern verwenden können.

Wenn Sie daran interessiert sind, sie völlig eigenständig ohne den Rest des Frameworks zu verwenden, gibt es eine Anleitung für [eigenständige Formulare |standalone].


Erstes Formular .[#toc-first-form]
==================================

Wir werden versuchen, ein einfaches Registrierungsformular zu schreiben. Sein Code wird wie folgt aussehen:

```php
use Nette\Application\UI\Form;

$form = new Form;
$form->addText('name', 'Name:');
$form->addPassword('password', 'Password:');
$form->addSubmit('send', 'Sign up');
$form->onSuccess[] = [$this, 'formSucceeded'];
```

und im Browser sollte das Ergebnis wie folgt aussehen:

[* form-en.webp *]

Das Formular im Presenter ist ein Objekt der Klasse `Nette\Application\UI\Form`, sein Vorgänger `Nette\Forms\Form` ist für den eigenständigen Gebrauch gedacht. Wir haben ihm die Felder Name, Passwort und Sendebutton hinzugefügt. Schließlich besagt die Zeile mit `$form->onSuccess`, dass nach dem Absenden und der erfolgreichen Validierung die Methode `$this->formSucceeded()` aufgerufen werden soll.

Aus der Sicht des Präsentators ist das Formular eine gemeinsame Komponente. Daher wird es als Komponente behandelt und mit der [Factory-Methode |application:components#Factory Methods] in den Presenter eingebunden. Das sieht dann wie folgt aus:

```php .{file:app/UI/Home/HomePresenter.php}
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', 'Password:');
		$form->addSubmit('send', 'Sign up');
		$form->onSuccess[] = [$this, 'formSucceeded'];
		return $form;
	}

	public function formSucceeded(Form $form, $data): void
	{
		// hier werden die vom Formular gesendeten Daten verarbeitet
		// $data->name enthält Name
		// $data->password enthält das Passwort
		$this->flashMessage('Sie haben sich erfolgreich angemeldet.');
		$this->redirect('Home:');
	}
}
```

Und das Rendern in der Vorlage erfolgt mit dem Tag `{control}`:

```latte .{file:app/UI/Home/default.latte}
<h1>Registration</h1>

{control registrationForm}
```

Und das ist alles :-) Wir haben ein funktionierendes und perfekt [gesichertes |#Vulnerability Protection] Formular.

Jetzt denken Sie wahrscheinlich, dass es zu schnell ging und fragen sich, wie es möglich ist, dass die Methode `formSucceeded()` aufgerufen wird und welche Parameter sie erhält. Sicher, Sie haben Recht, das verdient eine Erklärung.

Nette hat sich einen coolen Mechanismus einfallen lassen, den wir [Hollywood-Stil |application:components#Hollywood style] nennen. Anstatt ständig nachzufragen, ob etwas passiert ist ("wurde das Formular abgeschickt?", "wurde es gültig abgeschickt?" oder "wurde es nicht gefälscht?"), sagt man dem Framework "wenn das Formular gültig ausgefüllt ist, rufe diese Methode auf" und lässt es weiterarbeiten. Wenn Sie in JavaScript programmieren, sind Sie mit dieser Art der Programmierung vertraut. Sie schreiben Funktionen, die aufgerufen werden, wenn ein bestimmtes [Ereignis |nette:glossary#Events] eintritt. Und die Sprache übergibt die entsprechenden Argumente an sie.

So ist der obige Presenter-Code aufgebaut. Das Array `$form->onSuccess` stellt die Liste der PHP-Rückrufe dar, die Nette aufruft, wenn das Formular eingereicht und korrekt ausgefüllt wurde.
Im [Lebenszyklus des Presenters |application:presenters#Life Cycle of Presenter] handelt es sich um ein so genanntes Signal, das heißt, sie werden nach der Methode `action*` und vor der Methode `render*` aufgerufen.
Und es übergibt jedem Callback im ersten Parameter das Formular selbst und im zweiten Parameter die gesendeten Daten als Objekt [ArrayHash |utils:arrays#ArrayHash]. Sie können den ersten Parameter weglassen, wenn Sie das Formularobjekt nicht benötigen. Der zweite Parameter kann sogar noch praktischer sein, aber dazu [später |#Mapping to Classes] mehr.

Das Objekt `$data` enthält die Eigenschaften `name` und `password` mit den vom Benutzer eingegebenen Daten. Normalerweise senden wir die Daten direkt zur weiteren Verarbeitung, z. B. zum Einfügen in die Datenbank. Es kann jedoch ein Fehler bei der Verarbeitung auftreten, z. B. wenn der Benutzername bereits vergeben ist. In diesem Fall geben wir den Fehler mit `addError()` an das Formular zurück und lassen es neu zeichnen, mit einer Fehlermeldung:

```php
$form->addError('Sorry, username is already in use.');
```

Neben `onSuccess` gibt es auch `onSubmit`: Callbacks werden immer nach dem Absenden des Formulars aufgerufen, auch wenn es nicht korrekt ausgefüllt ist. Und schließlich `onError`: Rückrufe werden nur aufgerufen, wenn die Übermittlung nicht gültig ist. Sie werden sogar aufgerufen, wenn wir das Formular in `onSuccess` oder `onSubmit` mit `addError()` ungültig machen.

Nach der Bearbeitung des Formulars werden wir zur nächsten Seite weiterleiten. Dadurch wird verhindert, dass das Formular unbeabsichtigt durch Anklicken des *Refresh*- oder *Back*-Buttons oder durch Verschieben des Browserverlaufs erneut abgeschickt wird.

Versuchen Sie, weitere [Formularsteuerelemente |controls] hinzuzufügen.


Zugang zu Steuerelementen .[#toc-access-to-controls]
====================================================

Das Formular ist eine Komponente des Presenters, in unserem Fall mit dem Namen `registrationForm` (nach dem Namen der Factory-Methode `createComponentRegistrationForm`), so dass Sie von jeder Stelle des Presenters aus auf das Formular zugreifen können:

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

Auch die einzelnen Steuerelemente des Formulars sind Komponenten, so dass Sie auf sie auf die gleiche Weise zugreifen können:

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

Steuerelemente werden mit unset entfernt:

```php
unset($form['name']);
```


Überprüfungsregeln .[#toc-validation-rules]
===========================================

Das Wort *valid* wurde mehrere Male verwendet, aber das Formular hat noch keine Validierungsregeln. Das müssen wir ändern.

Der Name wird obligatorisch sein, also werden wir ihn mit der Methode `setRequired()` markieren, deren Argument der Text der Fehlermeldung ist, die angezeigt wird, wenn der Benutzer sie nicht ausfüllt. Wenn kein Argument angegeben wird, wird die Standard-Fehlermeldung verwendet.

```php
$form->addText('name', 'Name:')
	->setRequired('Please fill your name.');
```

Versuchen Sie, das Formular abzuschicken, ohne den Namen auszufüllen, und Sie werden sehen, dass eine Fehlermeldung angezeigt wird und der Browser oder Server das Formular ablehnt, bis Sie es ausgefüllt haben.

Gleichzeitig können Sie das System nicht austricksen, indem Sie z. B. nur Leerzeichen in die Eingabe eintippen. Das geht nicht. Nette schneidet automatisch linke und rechte Leerzeichen ab. Probieren Sie es aus. Das sollten Sie bei jeder einzeiligen Eingabe immer tun, aber das wird oft vergessen. Nette macht es automatisch. (Sie können versuchen, die Formulare zu täuschen und eine mehrzeilige Zeichenkette als Namen zu senden. Auch hier lässt sich Nette nicht täuschen und die Zeilenumbrüche werden in Leerzeichen umgewandelt.)

Das Formular wird immer serverseitig validiert, aber es wird auch eine JavaScript-Validierung generiert, die schnell ist und dem Benutzer den Fehler sofort anzeigt, ohne dass das Formular an den Server gesendet werden muss. Dies wird durch das Skript `netteForms.js` erledigt.
Fügen Sie es in die Layout-Vorlage ein:

```latte
<script src="https://unpkg.com/nette-forms@3/src/assets/netteForms.js"></script>
```

Wenn Sie in den Quellcode der Seite mit dem Formular schauen, werden Sie feststellen, dass Nette die erforderlichen Felder in Elemente mit einer CSS-Klasse `required` einfügt. Versuchen Sie, den folgenden Stil in die Vorlage einzufügen, und die Bezeichnung "Name" wird rot sein. Auf elegante Weise markieren wir die erforderlichen Felder für die Benutzer:

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

Zusätzliche Validierungsregeln werden mit der Methode `addRule()` hinzugefügt. Der erste Parameter ist rule, der zweite ist wieder der Text der Fehlermeldung, und das optionale Argument validation rule kann folgen. Was bedeutet das?

Das Formular erhält eine weitere optionale Eingabe *Alter* mit der Bedingung, dass es eine Zahl sein muss (`addInteger()`) und in bestimmten Grenzen (`$form::Range`). Und hier werden wir das dritte Argument von `addRule()` verwenden, den Bereich selbst:

```php
$form->addInteger('age', 'Age:')
	->addRule($form::Range, 'You must be older 18 years and be under 120.', [18, 120]);
```

.[tip]
Wenn der Benutzer das Feld nicht ausfüllt, werden die Validierungsregeln nicht überprüft, da das Feld optional ist.

Offensichtlich ist Raum für ein kleines Refactoring vorhanden. In der Fehlermeldung und im dritten Parameter sind die Zahlen doppelt aufgeführt, was nicht ideal ist. Wenn wir ein [mehrsprachiges Formular |rendering#translating] erstellen würden und die Nachricht mit den Zahlen in mehrere Sprachen übersetzt werden müsste, würde dies die Änderung der Werte erschweren. Aus diesem Grund können die Ersatzzeichen `%d` verwendet werden:

```php
	->addRule($form::Range, 'You must be older %d years and be under %d.', [18, 120]);
```

Kehren wir zum Feld *Kennwort* zurück, machen es *erforderlich* und überprüfen die Mindestlänge des Kennworts (`$form::MinLength`), wiederum unter Verwendung der Ersatzzeichen in der Nachricht:

```php
$form->addPassword('password', 'Password:')
	->setRequired('Pick a password')
	->addRule($form::MinLength, 'Your password has to be at least %d long', 8);
```

Wir fügen dem Formular ein Feld `passwordVerify` hinzu, in das der Benutzer das Passwort erneut eingibt, um es zu überprüfen. Mit Hilfe von Validierungsregeln überprüfen wir, ob beide Passwörter gleich sind (`$form::Equal`). Und als Argument geben wir in [eckigen Klammern |#Access to Controls] einen Verweis auf das erste Kennwort an:

```php
$form->addPassword('passwordVerify', 'Password again:')
	->setRequired('Fill your password again to check for typo')
	->addRule($form::Equal, 'Password mismatch', $form['password'])
	->setOmitted();
```

Mit `setOmitted()` markieren wir ein Element, dessen Wert uns nicht wirklich interessiert und das nur zur Validierung existiert. Sein Wert wird nicht an `$data` übergeben.

Wir haben ein voll funktionsfähiges Formular mit Validierung in PHP und JavaScript. Die Validierungsmöglichkeiten von Nette sind viel umfassender, Sie können Bedingungen erstellen, Teile einer Seite entsprechend anzeigen und ausblenden usw. Sie können alles im Kapitel über [Formularvalidierung |validation] nachlesen.


Standardwerte .[#toc-default-values]
====================================

Wir setzen oft Standardwerte für Formularsteuerelemente:

```php
$form->addEmail('email', 'Email')
	->setDefaultValue($lastUsedEmail);
```

Oft ist es sinnvoll, Standardwerte für alle Steuerelemente gleichzeitig festzulegen. Zum Beispiel, wenn das Formular verwendet wird, um Datensätze zu bearbeiten. Wir lesen den Datensatz aus der Datenbank und legen ihn als Standardwerte fest:

```php
//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);
```

Rufen Sie `setDefaults()` auf, nachdem Sie die Steuerelemente definiert haben.


Rendering des Formulars .[#toc-rendering-the-form]
==================================================

Standardmäßig wird das Formular als Tabelle gerendert. Die einzelnen Steuerelemente entsprechen den grundlegenden Richtlinien für die Barrierefreiheit im Web. Alle Beschriftungen werden als `<label>` Elemente generiert und sind mit ihren Eingaben verknüpft; ein Klick auf die Beschriftung bewegt den Cursor auf die Eingabe.

Wir können für jedes Element beliebige HTML-Attribute festlegen. Fügen Sie zum Beispiel einen Platzhalter hinzu:

```php
$form->addInteger('age', 'Age:')
	->setHtmlAttribute('placeholder', 'Please fill in the age');
```

Es gibt wirklich viele Möglichkeiten, ein Formular zu rendern, daher ist dieses Kapitel dem [Rendering |rendering] gewidmet.


Mapping auf Klassen .[#toc-mapping-to-classes]
==============================================

Kehren wir zur Methode `formSucceeded()` zurück, die im zweiten Parameter `$data` die gesendeten Daten als `ArrayHash` Objekt erhält. Da es sich hierbei um eine generische Klasse handelt, ähnlich wie `stdClass`, fehlen uns einige Annehmlichkeiten bei der Arbeit mit ihr, wie z. B. die Codevervollständigung für Eigenschaften in Editoren oder die statische Codeanalyse. Dies könnte durch eine spezifische Klasse für jedes Formular gelöst werden, deren Eigenschaften die einzelnen Steuerelemente darstellen. Z.B.:

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

Ab PHP 8.0 können Sie diese elegante Notation verwenden, die einen Konstruktor benutzt:

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

Wie sagt man Nette, dass es Daten als Objekte dieser Klasse zurückgeben soll? Einfacher als Sie denken. Alles, was Sie tun müssen, ist, die Klasse als Typ des `$data` Parameters im Handler anzugeben:

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

Sie können auch `array` als Typ angeben, dann werden die Daten als Array übergeben.

In ähnlicher Weise können Sie die Methode `getValues()` verwenden, die wir als Klassenname oder Objekt an hydrate als Parameter übergeben:

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

Wenn die Formulare aus einer mehrstufigen Struktur bestehen, die sich aus Containern zusammensetzt, erstellen Sie für jeden Container eine eigene Klasse:

```php
$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 weiß dann anhand des Eigenschaftstyps `$person`, dass es den Container auf die Klasse `PersonFormData` abbilden soll. Wenn die Eigenschaft ein Array von Containern enthalten würde, geben Sie den Typ `array` an und übergeben Sie die Klasse, die direkt auf den Container abgebildet werden soll:

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

Mit der Methode `Nette\Forms\Blueprint::dataClass($form)` können Sie einen Vorschlag für die Datenklasse eines Formulars erzeugen, der auf der Browserseite ausgedruckt wird. Sie können dann einfach auf den Code klicken, um ihn auszuwählen und in Ihr Projekt zu kopieren. .{data-version:3.1.15}


Mehrere Submit-Buttons .[#toc-multiple-submit-buttons]
======================================================

Wenn das Formular mehr als eine Schaltfläche hat, müssen wir normalerweise unterscheiden, welche Schaltfläche gedrückt wurde. Wir können für jede Schaltfläche eine eigene Funktion erstellen. Legen Sie sie als Handler für das [Ereignis |nette:glossary#Events] `onClick` fest:

```php
$form->addSubmit('save', 'Save')
	->onClick[] = [$this, 'saveButtonPressed'];

$form->addSubmit('delete', 'Delete')
	->onClick[] = [$this, 'deleteButtonPressed'];
```

Diese Handler werden auch nur dann aufgerufen, wenn das Formular gültig ist, wie im Fall des Ereignisses `onSuccess`. Der Unterschied besteht darin, dass der erste Parameter das Submit-Button-Objekt anstelle des Formulars sein kann, je nachdem, welchen Typ Sie angeben:

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

Wenn ein Formular mit der <kbd>Eingabetaste</kbd> abgeschickt wird, wird es so behandelt, als ob es mit der ersten Schaltfläche abgeschickt worden wäre.


Ereignis onAnchor .[#toc-event-onanchor]
========================================

Wenn Sie ein Formular in einer Factory-Methode (wie `createComponentRegistrationForm`) erstellen, weiß es noch nicht, ob es abgeschickt wurde oder mit welchen Daten es abgeschickt wurde. Aber es gibt Fälle, in denen wir die übermittelten Werte kennen müssen, vielleicht hängt es von ihnen ab, wie das Formular aussehen wird, oder sie werden für abhängige Auswahlfelder verwendet usw.

Daher kann der Code, der das Formular erstellt, aufgerufen werden, wenn es verankert ist, d.h. wenn es bereits mit dem Präsentator verknüpft ist und die übermittelten Daten kennt. Wir werden diesen Code in das Array `$onAnchor` einfügen:

```php
$country = $form->addSelect('country', 'Country:', $this->model->getCountries());
$city = $form->addSelect('city', 'City:');

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


Schutz vor Schwachstellen .[#toc-vulnerability-protection]
==========================================================

Nette Framework gibt sich große Mühe, sicher zu sein, und da Formulare die häufigste Benutzereingabe sind, sind Nette-Formulare so gut wie unangreifbar. Alles wird dynamisch und transparent verwaltet, nichts muss manuell eingestellt werden.

Zusätzlich zum Schutz der Formulare vor Angriffen auf bekannte Schwachstellen wie [Cross-Site Scripting (XSS) |nette:glossary#cross-site-scripting-xss] und [Cross-Site Request Forgery (CSRF |nette:glossary#cross-site-request-forgery-csrf]) übernimmt es viele kleine Sicherheitsaufgaben, an die Sie nicht mehr denken müssen.

So filtert es zum Beispiel alle Steuerzeichen aus den Eingaben heraus und überprüft die Gültigkeit der UTF-8-Kodierung, so dass die Daten aus dem Formular immer sauber sind. Bei Auswahlfeldern und Auswahllisten wird überprüft, ob die ausgewählten Elemente tatsächlich aus den angebotenen ausgewählt wurden und keine Fälschung vorliegt. Wie bereits erwähnt, werden bei einzeiligen Texteingaben Zeichen am Zeilenende entfernt, die ein Angreifer dort einfügen könnte. Bei mehrzeiligen Eingaben werden die Zeichen am Zeilenende normalisiert. Und so weiter.

Nette behebt für Sie Sicherheitslücken, von deren Existenz die meisten Programmierer keine Ahnung haben.

Der erwähnte CSRF-Angriff besteht darin, dass ein Angreifer das Opfer dazu verleitet, eine Seite zu besuchen, die im Browser des Opfers unbemerkt eine Anfrage an den Server ausführt, bei dem das Opfer gerade angemeldet ist, und der Server glaubt, dass die Anfrage vom Opfer absichtlich gestellt wurde. Daher verhindert Nette, dass das Formular per POST von einer anderen Domäne übermittelt wird. Wenn Sie aus irgendeinem Grund den Schutz ausschalten und die Übermittlung des Formulars von einer anderen Domäne aus zulassen möchten, verwenden Sie:

```php
$form->allowCrossOrigin(); // ACHTUNG! Schaltet den Schutz aus!
```

Dieser Schutz verwendet ein SameSite-Cookie namens `_nss`. Der SameSite-Cookie-Schutz ist möglicherweise nicht zu 100 % zuverlässig, weshalb es ratsam ist, den Token-Schutz zu aktivieren:

```php
$form->addProtection();
```

Es wird dringend empfohlen, diesen Schutz auf die Formulare in einem administrativen Teil Ihrer Anwendung anzuwenden, in dem sensible Daten geändert werden. Das Framework schützt vor einem CSRF-Angriff, indem es ein Authentifizierungs-Token generiert und validiert, das in einer Session gespeichert wird (das Argument ist die Fehlermeldung, die angezeigt wird, wenn das Token abgelaufen ist). Aus diesem Grund muss vor der Anzeige des Formulars eine Session gestartet werden. Im Verwaltungsteil der Website ist die Session in der Regel bereits durch die Anmeldung des Benutzers gestartet.
Ansonsten starten Sie die Session mit der Methode `Nette\Http\Session::start()`.


Ein Formular in mehreren Presentern verwenden .[#toc-using-one-form-in-multiple-presenters]
===========================================================================================

Wenn Sie ein Formular in mehreren Presentern verwenden müssen, empfehlen wir Ihnen, eine Factory dafür zu erstellen, die Sie dann an den Presenter weitergeben. Ein geeigneter Ort für eine solche Klasse ist z. B. das Verzeichnis `app/Forms`.

Die Fabrikklasse könnte wie folgt aussehen:

```php
use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Name:');
		$form->addSubmit('send', 'Log in');
		return $form;
	}
}
```

Wir bitten die Klasse, das Formular in der Factory-Methode für Komponenten im Presenter zu erstellen:

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

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	// können wir das Formular ändern, hier zum Beispiel ändern wir die Beschriftung der Schaltfläche
	$form['login']->setCaption('Continue');
	$form->onSuccess[] = [$this, 'signInFormSubmitted']; // und fügen den Handler
	return $form;
}
```

Der Handler für die Formularverarbeitung kann auch von der Fabrik geliefert werden:

```php
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 {
			// wir bearbeiten unser eingereichtes Formular hier
		};
		return $form;
	}
}
```

So, das war eine kurze Einführung in Formulare in Nette. Schauen Sie im Verzeichnis [examples |https://github.com/nette/forms/tree/master/examples] in der Distribution nach, um weitere Anregungen zu erhalten.

Formulare in Moderatoren

Nette Forms erleichtert die Erstellung und Bearbeitung von Webformularen erheblich. In diesem Kapitel lernen Sie, wie Sie Formulare in Presentern verwenden können.

Wenn Sie daran interessiert sind, sie völlig eigenständig ohne den Rest des Frameworks zu verwenden, gibt es eine Anleitung für eigenständige Formulare.

Erstes Formular

Wir werden versuchen, 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', 'Password:');
$form->addSubmit('send', 'Sign up');
$form->onSuccess[] = [$this, 'formSucceeded'];

und im Browser sollte das Ergebnis wie folgt aussehen:

Das Formular im Presenter ist ein Objekt der Klasse Nette\Application\UI\Form, sein Vorgänger Nette\Forms\Form ist für den eigenständigen Gebrauch gedacht. Wir haben ihm die Felder Name, Passwort und Sendebutton hinzugefügt. Schließlich besagt die Zeile mit $form->onSuccess, dass nach dem Absenden und der erfolgreichen Validierung die Methode $this->formSucceeded() aufgerufen werden soll.

Aus der Sicht des Präsentators ist das Formular eine gemeinsame Komponente. Daher wird es als Komponente behandelt und mit der Factory-Methode in den Presenter eingebunden. Das sieht dann wie folgt 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', 'Password:');
		$form->addSubmit('send', 'Sign up');
		$form->onSuccess[] = [$this, 'formSucceeded'];
		return $form;
	}

	public function formSucceeded(Form $form, $data): void
	{
		// hier werden die vom Formular gesendeten Daten verarbeitet
		// $data->name enthält Name
		// $data->password enthält das Passwort
		$this->flashMessage('Sie haben sich erfolgreich angemeldet.');
		$this->redirect('Home:');
	}
}

Und das Rendern in der Vorlage erfolgt mit dem Tag {control}:

<h1>Registration</h1>

{control registrationForm}

Und das ist alles :-) Wir haben ein funktionierendes und perfekt gesichertes Formular.

Jetzt denken Sie wahrscheinlich, dass es zu schnell ging und fragen sich, wie es möglich ist, dass die Methode formSucceeded() aufgerufen wird und welche Parameter sie erhält. Sicher, Sie haben Recht, das verdient eine Erklärung.

Nette hat sich einen coolen Mechanismus einfallen lassen, den wir Hollywood-Stil nennen. Anstatt ständig nachzufragen, ob etwas passiert ist („wurde das Formular abgeschickt?“, „wurde es gültig abgeschickt?“ oder „wurde es nicht gefälscht?“), sagt man dem Framework „wenn das Formular gültig ausgefüllt ist, rufe diese Methode auf“ und lässt es weiterarbeiten. Wenn Sie in JavaScript programmieren, sind Sie mit dieser Art der Programmierung vertraut. Sie schreiben Funktionen, die aufgerufen werden, wenn ein bestimmtes Ereignis eintritt. Und die Sprache übergibt die entsprechenden Argumente an sie.

So ist der obige Presenter-Code aufgebaut. Das Array $form->onSuccess stellt die Liste der PHP-Rückrufe dar, die Nette aufruft, wenn das Formular eingereicht und korrekt ausgefüllt wurde. Im Lebenszyklus des Presenters handelt es sich um ein so genanntes Signal, das heißt, sie werden nach der Methode action* und vor der Methode render* aufgerufen. Und es übergibt jedem Callback im ersten Parameter das Formular selbst und im zweiten Parameter die gesendeten Daten als Objekt ArrayHash. Sie können den ersten Parameter weglassen, wenn Sie das Formularobjekt nicht benötigen. Der zweite Parameter kann sogar noch praktischer sein, aber dazu später mehr.

Das Objekt $data enthält die Eigenschaften name und password mit den vom Benutzer eingegebenen Daten. Normalerweise senden wir die Daten direkt zur weiteren Verarbeitung, z. B. zum Einfügen in die Datenbank. Es kann jedoch ein Fehler bei der Verarbeitung auftreten, z. B. wenn der Benutzername bereits vergeben ist. In diesem Fall geben wir den Fehler mit addError() an das Formular zurück und lassen es neu zeichnen, mit einer Fehlermeldung:

$form->addError('Sorry, username is already in use.');

Neben onSuccess gibt es auch onSubmit: Callbacks werden immer nach dem Absenden des Formulars aufgerufen, auch wenn es nicht korrekt ausgefüllt ist. Und schließlich onError: Rückrufe werden nur aufgerufen, wenn die Übermittlung nicht gültig ist. Sie werden sogar aufgerufen, wenn wir das Formular in onSuccess oder onSubmit mit addError() ungültig machen.

Nach der Bearbeitung des Formulars werden wir zur nächsten Seite weiterleiten. Dadurch wird verhindert, dass das Formular unbeabsichtigt durch Anklicken des Refresh- oder Back-Buttons oder durch Verschieben des Browserverlaufs erneut abgeschickt wird.

Versuchen Sie, weitere Formularsteuerelemente hinzuzufügen.

Zugang zu Steuerelementen

Das Formular ist eine Komponente des Presenters, in unserem Fall mit dem Namen registrationForm (nach dem Namen der Factory-Methode createComponentRegistrationForm), so dass Sie von jeder Stelle des Presenters aus auf das Formular zugreifen können:

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

Auch die einzelnen Steuerelemente des Formulars sind Komponenten, so dass Sie auf sie auf die gleiche Weise zugreifen können:

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

Steuerelemente werden mit unset entfernt:

unset($form['name']);

Überprüfungsregeln

Das Wort valid wurde mehrere Male verwendet, aber das Formular hat noch keine Validierungsregeln. Das müssen wir ändern.

Der Name wird obligatorisch sein, also werden wir ihn mit der Methode setRequired() markieren, deren Argument der Text der Fehlermeldung ist, die angezeigt wird, wenn der Benutzer sie nicht ausfüllt. Wenn kein Argument angegeben wird, wird die Standard-Fehlermeldung verwendet.

$form->addText('name', 'Name:')
	->setRequired('Please fill your name.');

Versuchen Sie, das Formular abzuschicken, ohne den Namen auszufüllen, und Sie werden sehen, dass eine Fehlermeldung angezeigt wird und der Browser oder Server das Formular ablehnt, bis Sie es ausgefüllt haben.

Gleichzeitig können Sie das System nicht austricksen, indem Sie z. B. nur Leerzeichen in die Eingabe eintippen. Das geht nicht. Nette schneidet automatisch linke und rechte Leerzeichen ab. Probieren Sie es aus. Das sollten Sie bei jeder einzeiligen Eingabe immer tun, aber das wird oft vergessen. Nette macht es automatisch. (Sie können versuchen, die Formulare zu täuschen und eine mehrzeilige Zeichenkette als Namen zu senden. Auch hier lässt sich Nette nicht täuschen und die Zeilenumbrüche werden in Leerzeichen umgewandelt.)

Das Formular wird immer serverseitig validiert, aber es wird auch eine JavaScript-Validierung generiert, die schnell ist und dem Benutzer den Fehler sofort anzeigt, ohne dass das Formular an den Server gesendet werden muss. Dies wird durch das Skript netteForms.js erledigt. Fügen Sie es in die Layout-Vorlage ein:

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

Wenn Sie in den Quellcode der Seite mit dem Formular schauen, werden Sie feststellen, dass Nette die erforderlichen Felder in Elemente mit einer CSS-Klasse required einfügt. Versuchen Sie, den folgenden Stil in die Vorlage einzufügen, und die Bezeichnung „Name“ wird rot sein. Auf elegante Weise markieren wir die erforderlichen Felder für die Benutzer:

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

Zusätzliche Validierungsregeln werden mit der Methode addRule() hinzugefügt. Der erste Parameter ist rule, der zweite ist wieder der Text der Fehlermeldung, und das optionale Argument validation rule kann folgen. Was bedeutet das?

Das Formular erhält eine weitere optionale Eingabe Alter mit der Bedingung, dass es eine Zahl sein muss (addInteger()) und in bestimmten Grenzen ($form::Range). Und hier werden wir das dritte Argument von addRule() verwenden, den Bereich selbst:

$form->addInteger('age', 'Age:')
	->addRule($form::Range, 'You must be older 18 years and be under 120.', [18, 120]);

Wenn der Benutzer das Feld nicht ausfüllt, werden die Validierungsregeln nicht überprüft, da das Feld optional ist.

Offensichtlich ist Raum für ein kleines Refactoring vorhanden. In der Fehlermeldung und im dritten Parameter sind die Zahlen doppelt aufgeführt, was nicht ideal ist. Wenn wir ein mehrsprachiges Formular erstellen würden und die Nachricht mit den Zahlen in mehrere Sprachen übersetzt werden müsste, würde dies die Änderung der Werte erschweren. Aus diesem Grund können die Ersatzzeichen %d verwendet werden:

	->addRule($form::Range, 'You must be older %d years and be under %d.', [18, 120]);

Kehren wir zum Feld Kennwort zurück, machen es erforderlich und überprüfen die Mindestlänge des Kennworts ($form::MinLength), wiederum unter Verwendung der Ersatzzeichen in der Nachricht:

$form->addPassword('password', 'Password:')
	->setRequired('Pick a password')
	->addRule($form::MinLength, 'Your password has to be at least %d long', 8);

Wir fügen dem Formular ein Feld passwordVerify hinzu, in das der Benutzer das Passwort erneut eingibt, um es zu überprüfen. Mit Hilfe von Validierungsregeln überprüfen wir, ob beide Passwörter gleich sind ($form::Equal). Und als Argument geben wir in eckigen Klammern einen Verweis auf das erste Kennwort an:

$form->addPassword('passwordVerify', 'Password again:')
	->setRequired('Fill your password again to check for typo')
	->addRule($form::Equal, 'Password mismatch', $form['password'])
	->setOmitted();

Mit setOmitted() markieren wir ein Element, dessen Wert uns nicht wirklich interessiert und das nur zur Validierung existiert. Sein Wert wird nicht an $data übergeben.

Wir haben ein voll funktionsfähiges Formular mit Validierung in PHP und JavaScript. Die Validierungsmöglichkeiten von Nette sind viel umfassender, Sie können Bedingungen erstellen, Teile einer Seite entsprechend anzeigen und ausblenden usw. Sie können alles im Kapitel über Formularvalidierung nachlesen.

Standardwerte

Wir setzen oft Standardwerte für Formularsteuerelemente:

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

Oft ist es sinnvoll, Standardwerte für alle Steuerelemente gleichzeitig festzulegen. Zum Beispiel, wenn das Formular verwendet wird, um Datensätze zu bearbeiten. Wir lesen den Datensatz aus der Datenbank und legen ihn als Standardwerte fest:

//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);

Rufen Sie setDefaults() auf, nachdem Sie die Steuerelemente definiert haben.

Rendering des Formulars

Standardmäßig wird das Formular als Tabelle gerendert. Die einzelnen Steuerelemente entsprechen den grundlegenden Richtlinien für die Barrierefreiheit im Web. Alle Beschriftungen werden als <label> Elemente generiert und sind mit ihren Eingaben verknüpft; ein Klick auf die Beschriftung bewegt den Cursor auf die Eingabe.

Wir können für jedes Element beliebige HTML-Attribute festlegen. Fügen Sie zum Beispiel einen Platzhalter hinzu:

$form->addInteger('age', 'Age:')
	->setHtmlAttribute('placeholder', 'Please fill in the age');

Es gibt wirklich viele Möglichkeiten, ein Formular zu rendern, daher ist dieses Kapitel dem Rendering gewidmet.

Mapping auf Klassen

Kehren wir zur Methode formSucceeded() zurück, die im zweiten Parameter $data die gesendeten Daten als ArrayHash Objekt erhält. Da es sich hierbei um eine generische Klasse handelt, ähnlich wie stdClass, fehlen uns einige Annehmlichkeiten bei der Arbeit mit ihr, wie z. B. die Codevervollständigung für Eigenschaften in Editoren oder die statische Codeanalyse. Dies könnte durch eine spezifische Klasse für jedes Formular gelöst werden, deren Eigenschaften die einzelnen Steuerelemente darstellen. Z.B.:

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

Ab PHP 8.0 können Sie diese elegante Notation verwenden, die einen Konstruktor benutzt:

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

Wie sagt man Nette, dass es Daten als Objekte dieser Klasse zurückgeben soll? Einfacher als Sie denken. Alles, was Sie tun müssen, ist, die Klasse als Typ des $data Parameters im Handler anzugeben:

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

Sie können auch array als Typ angeben, dann werden die Daten als Array übergeben.

In ähnlicher Weise können Sie die Methode getValues() verwenden, die wir als Klassenname oder Objekt an hydrate als Parameter übergeben:

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

Wenn die Formulare aus einer mehrstufigen Struktur bestehen, die sich aus Containern zusammensetzt, erstellen Sie für jeden Container eine eigene 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 weiß dann anhand des Eigenschaftstyps $person, dass es den Container auf die Klasse PersonFormData abbilden soll. Wenn die Eigenschaft ein Array von Containern enthalten würde, geben Sie den Typ array an und übergeben Sie die Klasse, die direkt auf den Container abgebildet werden soll:

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

Mit der Methode Nette\Forms\Blueprint::dataClass($form) können Sie einen Vorschlag für die Datenklasse eines Formulars erzeugen, der auf der Browserseite ausgedruckt wird. Sie können dann einfach auf den Code klicken, um ihn auszuwählen und in Ihr Projekt zu kopieren.

Mehrere Submit-Buttons

Wenn das Formular mehr als eine Schaltfläche hat, müssen wir normalerweise unterscheiden, welche Schaltfläche gedrückt wurde. Wir können für jede Schaltfläche eine eigene Funktion erstellen. Legen Sie sie als Handler für das Ereignis onClick fest:

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

$form->addSubmit('delete', 'Delete')
	->onClick[] = [$this, 'deleteButtonPressed'];

Diese Handler werden auch nur dann aufgerufen, wenn das Formular gültig ist, wie im Fall des Ereignisses onSuccess. Der Unterschied besteht darin, dass der erste Parameter das Submit-Button-Objekt anstelle des Formulars sein kann, je nachdem, welchen Typ Sie angeben:

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

Wenn ein Formular mit der Eingabetaste abgeschickt wird, wird es so behandelt, als ob es mit der ersten Schaltfläche abgeschickt worden wäre.

Ereignis onAnchor

Wenn Sie ein Formular in einer Factory-Methode (wie createComponentRegistrationForm) erstellen, weiß es noch nicht, ob es abgeschickt wurde oder mit welchen Daten es abgeschickt wurde. Aber es gibt Fälle, in denen wir die übermittelten Werte kennen müssen, vielleicht hängt es von ihnen ab, wie das Formular aussehen wird, oder sie werden für abhängige Auswahlfelder verwendet usw.

Daher kann der Code, der das Formular erstellt, aufgerufen werden, wenn es verankert ist, d.h. wenn es bereits mit dem Präsentator verknüpft ist und die übermittelten Daten kennt. Wir werden diesen Code in das Array $onAnchor einfügen:

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

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

Schutz vor Schwachstellen

Nette Framework gibt sich große Mühe, sicher zu sein, und da Formulare die häufigste Benutzereingabe sind, sind Nette-Formulare so gut wie unangreifbar. Alles wird dynamisch und transparent verwaltet, nichts muss manuell eingestellt werden.

Zusätzlich zum Schutz der Formulare vor Angriffen auf bekannte Schwachstellen wie Cross-Site Scripting (XSS) und Cross-Site Request Forgery (CSRF) übernimmt es viele kleine Sicherheitsaufgaben, an die Sie nicht mehr denken müssen.

So filtert es zum Beispiel alle Steuerzeichen aus den Eingaben heraus und überprüft die Gültigkeit der UTF-8-Kodierung, so dass die Daten aus dem Formular immer sauber sind. Bei Auswahlfeldern und Auswahllisten wird überprüft, ob die ausgewählten Elemente tatsächlich aus den angebotenen ausgewählt wurden und keine Fälschung vorliegt. Wie bereits erwähnt, werden bei einzeiligen Texteingaben Zeichen am Zeilenende entfernt, die ein Angreifer dort einfügen könnte. Bei mehrzeiligen Eingaben werden die Zeichen am Zeilenende normalisiert. Und so weiter.

Nette behebt für Sie Sicherheitslücken, von deren Existenz die meisten Programmierer keine Ahnung haben.

Der erwähnte CSRF-Angriff besteht darin, dass ein Angreifer das Opfer dazu verleitet, eine Seite zu besuchen, die im Browser des Opfers unbemerkt eine Anfrage an den Server ausführt, bei dem das Opfer gerade angemeldet ist, und der Server glaubt, dass die Anfrage vom Opfer absichtlich gestellt wurde. Daher verhindert Nette, dass das Formular per POST von einer anderen Domäne übermittelt wird. Wenn Sie aus irgendeinem Grund den Schutz ausschalten und die Übermittlung des Formulars von einer anderen Domäne aus zulassen möchten, verwenden Sie:

$form->allowCrossOrigin(); // ACHTUNG! Schaltet den Schutz aus!

Dieser Schutz verwendet ein SameSite-Cookie namens _nss. Der SameSite-Cookie-Schutz ist möglicherweise nicht zu 100 % zuverlässig, weshalb es ratsam ist, den Token-Schutz zu aktivieren:

$form->addProtection();

Es wird dringend empfohlen, diesen Schutz auf die Formulare in einem administrativen Teil Ihrer Anwendung anzuwenden, in dem sensible Daten geändert werden. Das Framework schützt vor einem CSRF-Angriff, indem es ein Authentifizierungs-Token generiert und validiert, das in einer Session gespeichert wird (das Argument ist die Fehlermeldung, die angezeigt wird, wenn das Token abgelaufen ist). Aus diesem Grund muss vor der Anzeige des Formulars eine Session gestartet werden. Im Verwaltungsteil der Website ist die Session in der Regel bereits durch die Anmeldung des Benutzers gestartet. Ansonsten starten Sie die Session mit der Methode Nette\Http\Session::start().

Ein Formular in mehreren Presentern verwenden

Wenn Sie ein Formular in mehreren Presentern verwenden müssen, empfehlen wir Ihnen, eine Factory dafür zu erstellen, die Sie dann an den Presenter weitergeben. Ein geeigneter Ort für eine solche Klasse ist z. B. das Verzeichnis app/Forms.

Die Fabrikklasse könnte wie folgt aussehen:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Name:');
		$form->addSubmit('send', 'Log in');
		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();
	// können wir das Formular ändern, hier zum Beispiel ändern wir die Beschriftung der Schaltfläche
	$form['login']->setCaption('Continue');
	$form->onSuccess[] = [$this, 'signInFormSubmitted']; // und fügen den Handler
	return $form;
}

Der Handler für die Formularverarbeitung kann auch von der Fabrik 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 {
			// wir bearbeiten unser eingereichtes Formular hier
		};
		return $form;
	}
}

So, das war eine kurze Einführung in Formulare in Nette. Schauen Sie im Verzeichnis examples in der Distribution nach, um weitere Anregungen zu erhalten.