Wiederverwendung von Formularen an mehreren Stellen
In Nette haben Sie mehrere Möglichkeiten, dasselbe Formular an mehreren Stellen zu verwenden und Code nicht zu duplizieren. In diesem Artikel zeigen wir Ihnen verschiedene Lösungen, einschließlich solcher, die Sie vermeiden sollten.
Formular-Factory
Eine der grundlegenden Herangehensweisen, dieselbe Komponente an mehreren Stellen zu verwenden, ist die Erstellung einer Methode oder Klasse, die diese Komponente generiert, und der anschließende Aufruf dieser Methode an verschiedenen Stellen der Anwendung. Eine solche Methode oder Klasse wird Factory genannt. Bitte nicht verwechseln mit dem Entwurfsmuster Factory Method, das eine spezifische Verwendung von Factories beschreibt und nichts mit diesem Thema zu tun hat.
Als Beispiel erstellen wir eine Factory, die ein Bearbeitungsformular erstellt:
use Nette\Application\UI\Form;
class FormFactory
{
public function createEditForm(): Form
{
$form = new Form;
$form->addText('title', 'Titel:');
// hier werden weitere Formularfelder hinzugefügt
$form->addSubmit('send', 'Senden');
return $form;
}
}
Nun können Sie diese Factory an verschiedenen Stellen in Ihrer Anwendung verwenden, beispielsweise in Presentern oder Komponenten. Und zwar, indem Sie sie als Abhängigkeit anfordern. Zuerst schreiben wir also die Klasse in die Konfigurationsdatei:
services:
- FormFactory
Und dann verwenden wir sie im Presenter:
class MyPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FormFactory $formFactory,
) {
}
protected function createComponentEditForm(): Form
{
$form = $this->formFactory->createEditForm();
$form->onSuccess[] = function () {
// Verarbeitung der gesendeten Daten
};
return $form;
}
}
Sie können die Formular-Factory um weitere Methoden zur Erstellung anderer Formulartypen erweitern, je nach Bedarf Ihrer Anwendung. Und natürlich können wir auch eine Methode hinzufügen, die ein Basisformular ohne Elemente erstellt, und diese werden die anderen Methoden nutzen:
class FormFactory
{
public function createForm(): Form
{
$form = new Form;
return $form;
}
public function createEditForm(): Form
{
$form = $this->createForm();
$form->addText('title', 'Titel:');
// hier werden weitere Formularfelder hinzugefügt
$form->addSubmit('send', 'Senden');
return $form;
}
}
Die Methode createForm()
tut bisher nichts Nützliches, aber das wird sich schnell ändern.
Abhängigkeiten der Factory
Mit der Zeit stellt sich heraus, dass wir Formulare mehrsprachig benötigen. Das bedeutet, dass wir allen Formularen einen
sogenannten Translator zuweisen müssen. Zu diesem Zweck passen
wir die Klasse FormFactory
so an, dass sie das Translator
-Objekt als Abhängigkeit im Konstruktor
akzeptiert und es an das Formular übergibt:
use Nette\Localization\Translator;
class FormFactory
{
public function __construct(
private Translator $translator,
) {
}
public function createForm(): Form
{
$form = new Form;
$form->setTranslator($this->translator);
return $form;
}
// ...
}
Da die Methode createForm()
auch von anderen Methoden aufgerufen wird, die spezifische Formulare erstellen,
genügt es, den Translator nur in ihr zu setzen. Und wir sind fertig. Es ist nicht nötig, den Code irgendeines Presenters oder
einer Komponente zu ändern, was großartig ist.
Mehrere Factory-Klassen
Alternativ können Sie mehrere Klassen für jedes Formular erstellen, das Sie in Ihrer Anwendung verwenden möchten. Dieser
Ansatz kann die Lesbarkeit des Codes erhöhen und die Verwaltung von Formularen erleichtern. Die ursprüngliche
FormFactory
lassen wir nur ein reines Formular mit Grundkonfiguration erstellen (z.B. mit
Übersetzungsunterstützung) und für das Bearbeitungsformular erstellen wir eine neue Factory EditFormFactory
.
class FormFactory
{
public function __construct(
private Translator $translator,
) {
}
public function create(): Form
{
$form = new Form;
$form->setTranslator($this->translator);
return $form;
}
}
// ✅ Verwendung von Komposition
class EditFormFactory
{
public function __construct(
private FormFactory $formFactory,
) {
}
public function create(): Form
{
$form = $this->formFactory->create();
// hier werden weitere Formularfelder hinzugefügt
$form->addSubmit('send', 'Senden');
return $form;
}
}
Sehr wichtig ist, dass die Bindung zwischen den Klassen FormFactory
und EditFormFactory
durch Komposition realisiert wird,
nicht durch objektorientierte
Vererbung:
// ⛔ SO NICHT! HIER GEHÖRT KEINE VERERBUNG HIN
class EditFormFactory extends FormFactory
{
public function create(): Form
{
$form = parent::create();
$form->addText('title', 'Titel:');
// hier werden weitere Formularfelder hinzugefügt
$form->addSubmit('send', 'Senden');
return $form;
}
}
Die Verwendung von Vererbung wäre in diesem Fall völlig kontraproduktiv. Sie würden sehr schnell auf Probleme stoßen. Zum
Beispiel, wenn Sie der Methode create()
Parameter hinzufügen möchten; PHP würde einen Fehler melden, dass ihre
Signatur von der der Elternklasse abweicht. Oder bei der Übergabe von Abhängigkeiten an die Klasse EditFormFactory
über den Konstruktor. Es würde eine Situation entstehen, die wir Constructor Hell nennen.
Im Allgemeinen ist es besser, Komposition der Vererbung vorzuziehen.
Formularverarbeitung
Die Verarbeitung des Formulars, die nach erfolgreichem Absenden aufgerufen wird, kann auch Teil der Factory-Klasse sein. Sie
wird so funktionieren, dass sie die gesendeten Daten zur Verarbeitung an das Modell übergibt. Eventuelle Fehler gibt sie zurück an das Formular. Das Modell wird im
folgenden Beispiel durch die Klasse Facade
repräsentiert:
class EditFormFactory
{
public function __construct(
private FormFactory $formFactory,
private Facade $facade,
) {
}
public function create(): Form
{
$form = $this->formFactory->create();
$form->addText('title', 'Titel:');
// hier werden weitere Formularfelder hinzugefügt
$form->addSubmit('send', 'Senden');
$form->onSuccess[] = [$this, 'processForm'];
return $form;
}
public function processForm(Form $form, array $data): void
{
try {
// Verarbeitung der gesendeten Daten
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
}
}
}
Die eigentliche Weiterleitung überlassen wir jedoch dem Presenter. Dieser fügt dem onSuccess
-Ereignis einen
weiteren Handler hinzu, der die Weiterleitung durchführt. Dadurch wird es möglich, das Formular in verschiedenen Presentern zu
verwenden und in jedem woandershin weiterzuleiten.
class MyPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private EditFormFactory $formFactory,
) {
}
protected function createComponentEditForm(): Form
{
$form = $this->formFactory->create();
$form->onSuccess[] = function () {
$this->flashMessage('Der Datensatz wurde gespeichert');
$this->redirect('Homepage:');
};
return $form;
}
}
Diese Lösung nutzt die Eigenschaft von Formularen, dass, wenn addError()
über dem Formular oder einem seiner
Elemente aufgerufen wird, der nächste onSuccess
-Handler nicht mehr aufgerufen wird.
Vererbung von der Klasse Form
Das erstellte Formular soll kein Nachkomme der Klasse Form
sein. Mit anderen Worten, verwenden Sie nicht diese
Lösung:
// ⛔ SO NICHT! HIER GEHÖRT KEINE VERERBUNG HIN
class EditForm extends Form
{
public function __construct(Translator $translator)
{
parent::__construct();
$this->addText('title', 'Titel:');
// hier werden weitere Formularfelder hinzugefügt
$this->addSubmit('send', 'Senden');
$this->setTranslator($translator);
}
}
Anstatt das Formular im Konstruktor zu erstellen, verwenden Sie eine Factory.
Es ist wichtig zu erkennen, dass die Klasse Form
in erster Linie ein Werkzeug zur Erstellung eines Formulars ist,
also ein Form Builder. Und das erstellte Formular kann als ihr Produkt betrachtet werden. Aber ein Produkt ist kein
spezifischer Fall eines Builders, es gibt keine is a-Beziehung zwischen ihnen, die die Grundlage der Vererbung
bildet.
Komponente mit Formular
Ein völlig anderer Ansatz ist die Erstellung einer Komponente, die ein Formular enthält. Dies eröffnet neue Möglichkeiten, zum Beispiel das Formular auf spezifische Weise zu rendern, da die Komponente auch ein Template enthält. Oder man kann Signale für die AJAX-Kommunikation und das Nachladen von Informationen in das Formular nutzen, zum Beispiel für Vorschläge usw.
use Nette\Application\UI\Form;
class EditControl extends Nette\Application\UI\Control
{
public array $onSave = [];
public function __construct(
private Facade $facade,
) {
}
protected function createComponentForm(): Form
{
$form = new Form;
$form->addText('title', 'Titel:');
// hier werden weitere Formularfelder hinzugefügt
$form->addSubmit('send', 'Senden');
$form->onSuccess[] = [$this, 'processForm'];
return $form;
}
public function processForm(Form $form, array $data): void
{
try {
// Verarbeitung der gesendeten Daten
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
return;
}
// Ereignis auslösen
$this->onSave($this, $data);
}
}
Wir erstellen noch eine Factory, die diese Komponente herstellt. Es genügt, ihr Interface zu definieren:
interface EditControlFactory
{
function create(): EditControl;
}
Und in die Konfigurationsdatei hinzufügen:
services:
- EditControlFactory
Und nun können wir die Factory anfordern und im Presenter verwenden:
class MyPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private EditControlFactory $controlFactory,
) {
}
protected function createComponentEditForm(): EditControl
{
$control = $this->controlFactory->create();
$control->onSave[] = function (EditControl $control, $data) {
$this->redirect('this');
// oder wir leiten zum Ergebnis der Bearbeitung weiter, z.B.:
// $this->redirect('detail', ['id' => $data->id]);
};
return $control;
}
}