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.

Version: 4.0