Wiederverwendung von Formularen an mehreren Stellen

Nette bietet Ihnen mehrere Möglichkeiten, dasselbe Formular an mehreren Stellen zu verwenden, ohne Code zu duplizieren. In diesem Artikel stellen wir verschiedene Lösungen vor, einschließlich derer, die Sie vermeiden sollten.

Formular-Factory

Ein grundlegender Ansatz zur Wiederverwendung einer Komponente an mehreren Stellen besteht darin, eine Methode oder Klasse zu erstellen, die diese Komponente erzeugt, und diese Methode dann an verschiedenen Stellen der Anwendung aufzurufen. Eine solche Methode oder Klasse nennt man Factory. Verwechseln Sie sie bitte nicht mit dem Entwurfsmuster Factory Method, das eine bestimmte Art der Verwendung von Factories beschreibt und mit diesem Thema nicht zusammenhängt.

Als Beispiel erstellen wir eine Factory, die ein Bearbeitungsformular zusammensetzt:

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', 'Speichern');
		return $form;
	}
}

Nun können Sie diese Factory an verschiedenen Stellen Ihrer Anwendung verwenden, zum Beispiel in Presentern oder Komponenten. Und zwar so, dass Sie sie als Abhängigkeit anfordern. Zuerst tragen wir die Klasse also in die Konfigurationsdatei ein:

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 übermittelten Daten
		};
		return $form;
	}
}

Sie können die Formular-Factory um weitere Methoden zum Erstellen anderer Formulartypen erweitern, ganz wie es Ihre Anwendung erfordert. Und natürlich können wir auch eine Methode hinzufügen, die ein einfaches Formular ohne Elemente erstellt und die dann die übrigen 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', 'Speichern');
		return $form;
	}
}

Die Methode createForm() tut noch nichts Nützliches, aber das ändert sich gleich.

Abhängigkeiten der Factory

Mit der Zeit stellt sich heraus, dass die Formulare mehrsprachig sein müssen. Das bedeutet, dass wir für alle Formulare einen Translator setzen müssen. Zu diesem Zweck passen wir die Klasse FormFactory so an, dass sie das Objekt Translator als Abhängigkeit im Konstruktor entgegennimmt und an das erzeugte 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 den anderen Methoden aufgerufen wird, die spezifische Formulare erzeugen, genügt es, den Translator nur hier zu setzen. Und damit sind wir fertig. Es ist nicht nötig, den Code irgendeines Presenters oder einer Komponente zu ändern, was ausgezeichnet ist.

Mehrere Factory-Klassen

Alternativ können Sie für jedes Formular, das Sie in Ihrer Anwendung verwenden möchten, eine eigene Factory-Klasse erstellen. Dieser Ansatz kann die Lesbarkeit des Codes erhöhen und die Verwaltung der Formulare erleichtern. Die ursprüngliche FormFactory lassen wir nur ein einfaches Formular mit der Grundkonfiguration erzeugen (zum Beispiel 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', 'Speichern');
		return $form;
	}
}

Sehr wichtig ist, dass die Beziehung zwischen den Klassen FormFactory und EditFormFactory durch Komposition realisiert wird und nicht durch objektorientierte Vererbung:

// ⛔ NEIN! 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', 'Speichern');
		return $form;
	}
}

Vererbung wäre in diesem Fall völlig kontraproduktiv. Auf Probleme würden Sie sehr schnell stoßen. Etwa in dem Moment, in dem Sie der Methode create() Parameter hinzufügen wollten; PHP würde einen Fehler melden, weil sich ihre Signatur von der der Elternklasse unterscheidet. Oder beim Übergeben von Abhängigkeiten an die Klasse EditFormFactory über den Konstruktor. Es entstünde eine Situation, die wir Constructor Hell nennen.

Generell ist es besser, Komposition der Vererbung vorzuziehen.

Formularverarbeitung

Auch die Formularverarbeitung, die nach dem erfolgreichen Absenden aufgerufen wird, kann Teil der Factory-Klasse sein. Sie funktioniert so, dass sie die übermittelten Daten an die Modellschicht zur Verarbeitung übergibt. Eventuelle Fehler gibt sie an das Formular zurück. 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', 'Speichern');
		$form->onSuccess[] = $this->processForm(...);
		return $form;
	}

	private function processForm(Form $form, array $data): void
	{
		try {
			// Verarbeitung der übermittelten Daten
			$this->facade->process($data);

		} catch (AnyModelException $e) {
			$form->addError('...');
		}
	}
}

Die eigentliche Weiterleitung überlassen wir jedoch dem Presenter. Er fügt dem Event onSuccess einen weiteren Handler hinzu, der die Weiterleitung durchführt. Dadurch lässt sich das Formular in verschiedenen Presentern verwenden und in jedem woandershin weiterleiten.

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 aus, dass nach einem Aufruf von addError() auf dem Formular oder einem seiner Elemente die weiteren onSuccess-Handler nicht mehr aufgerufen werden.

Vererbung von der Klasse Form

Ein zusammengesetztes Formular soll kein Nachkomme der Klasse Form sein. Mit anderen Worten: Verwenden Sie diese Lösung nicht:

// ⛔ NEIN! 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', 'Speichern');
		$this->setTranslator($translator);
	}
}

Statt das Formular im Konstruktor zusammenzusetzen, verwenden Sie eine Factory.

Man muss sich bewusst machen, dass die Klasse Form in erster Linie ein Werkzeug zum Zusammensetzen von Formularen ist, also ein Form Builder. Und das zusammengesetzte Formular lässt sich als ihr Produkt verstehen. Ein Produkt ist aber kein Spezialfall eines Builders; zwischen ihnen besteht keine is a-Beziehung, die die Grundlage der Vererbung bildet.

Komponente mit Formular

Einen völlig anderen Ansatz stellt die Erstellung einer Komponente dar, die das Formular enthält. Das eröffnet neue Möglichkeiten, zum Beispiel das Formular auf eine bestimmte Art zu rendern, denn zur Komponente gehört auch ein eigenes Template. Oder man kann Signale für die AJAX-Kommunikation und das Nachladen von Informationen in das Formular nutzen, etwa 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', 'Speichern');
		$form->onSuccess[] = $this->processForm(...);

		return $form;
	}

	private function processForm(Form $form, array $data): void
	{
		try {
			// Verarbeitung der übermittelten Daten
			$this->facade->process($data);

		} catch (AnyModelException $e) {
			$form->addError('...');
			return;
		}

		// Auslösen des Events
		$this->onSave($this, $data);
	}
}

Nun erstellen wir noch eine Factory, die diese Komponente erzeugt. Es genügt, ihr Interface zu definieren:

interface EditControlFactory
{
	function create(): EditControl;
}

Und es der Konfigurationsdatei hinzuzufü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;
	}
}