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"></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;
}
Alternativ können Sie auch den Konstruktor verwenden:
class RegistrationFormData
{
public function __construct(
public string $name,
public int $age,
public string $password,
) {
}
}
Eigenschaften der Datenklasse können auch Enums sein und werden automatisch zugeordnet.
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.